Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
WordWrappedTextImager.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
6#include <cctype>
7
9
10#include "WordWrappedTextImager.h"
11
12using namespace Stroika::Foundation;
13
14using namespace Stroika::Frameworks;
15using namespace Stroika::Frameworks::Led;
16
17#if qStroika_Frameworks_Led_SupportGDI
18
19inline DistanceType LookupLengthInVector (const DistanceType* widthsVector, size_t startSoFar, size_t i)
20{
21 AssertNotNull (widthsVector);
22 if (i == 0) {
23 return 0;
24 }
25 DistanceType startPointCorrection = (startSoFar == 0) ? 0 : widthsVector[startSoFar - 1];
26 Assert (i + startSoFar >= 1);
27 return (widthsVector[i + startSoFar - 1] - startPointCorrection);
28}
29
30/*
31 ********************************************************************************
32 ********************************* WordWrappedTextImager ************************
33 ********************************************************************************
34 */
35/*
36@METHOD: WordWrappedTextImager::FillCache
37@DESCRIPTION: <p>Hook the MultiRowTextImager's FillCache () call to compute the row information for
38 the given @'Partition::PartitionMarker'.</p>
39 <p>Basicly, this is where we start the word-wrap process (on demand, per paragraph).</p>
40*/
41void WordWrappedTextImager::FillCache (PartitionMarker* pm, PartitionElementCacheInfo& cacheInfo)
42{
43 // Need to be careful about exceptions here!!! Probably good enuf to just
44 // Invalidate the caceh and then propogate exception...
45 // Perhaps better to place bogus entry in? No - probably not...
46 RequireNotNull (pm);
47
48 size_t start;
49 size_t end;
50 pm->GetRange (&start, &end);
51 size_t len = end - start;
52
53 Assert (end <= GetEnd () + 1);
54 if (end == GetEnd () + 1) {
55 --end; // don't include bogus char at end of buffer
56 --len;
57 }
58 Assert (end <= GetEnd ());
59
60 Memory::StackBuffer<Led_tChar> buf{Memory::eUninitialized, len};
61 CopyOut (start, len, buf.data ());
62
63 Tablet_Acquirer tablet (this);
64
65 cacheInfo.Clear ();
66
67 try {
68 Memory::StackBuffer<DistanceType> distanceVector{Memory::eUninitialized, len};
69 if (start != end) {
70 MeasureSegmentWidth (start, end, buf.data (), distanceVector.data ());
71 }
72
73 size_t startSoFar = 0; // make this ZERO relative from start of THIS array
74 size_t leftToGo = len - startSoFar;
75
76 /*
77 * As a minor optimization for resetting tabs, we keep track of the index of the last
78 * tab we saw during last scan. If the last tab is before our current wrap point, then
79 * no more tabs to be found. Since tabs are usually at the start of a sentence or paragraph,
80 * this is modestly helpful.
81 */
82 size_t lastTabIndex = startSoFar;
83
84 while (leftToGo != 0) {
85 cacheInfo.IncrementRowCountAndFixCacheBuffers (startSoFar, 0);
86
87 if (lastTabIndex >= startSoFar) {
88 lastTabIndex = ResetTabStops (start, buf.data (), leftToGo, distanceVector.data (), startSoFar);
89 }
90
91 DistanceType wrapWidth;
92 {
93 // NOT RIGHT - doesn't properly interpret tabstops!!! with respect to left margins!!! LGP 980908
94 CoordinateType lhsMargin;
95 CoordinateType rhsMargin;
96 GetLayoutMargins (RowReference{pm, cacheInfo.GetRowCount () - 1}, &lhsMargin, &rhsMargin);
97 Assert (lhsMargin < rhsMargin);
98 wrapWidth = rhsMargin - lhsMargin;
99 }
100 size_t bestRowLength = FindWrapPointForMeasuredText (buf.data () + startSoFar, leftToGo, wrapWidth, start + startSoFar,
101 distanceVector.data (), startSoFar);
102
103 Assert (bestRowLength != 0); // FindWrapPoint() could only do this if we gave it a zero leftToGo - but we wouldn't
104 // be in the loop in that case!!!
105
106 // Now OVERRIDE the above for soft-breaks...
107 {
108 Assert (bestRowLength > 0);
109 const Led_tChar* text = buf.data () + startSoFar;
110 const Led_tChar* textEnd = &text[min (bestRowLength + 1, leftToGo)];
111 AdjustBestRowLength (start + startSoFar, text, textEnd, &bestRowLength);
112 Assert (bestRowLength > 0);
113 }
114
115 DistanceType newRowHeight = MeasureSegmentHeight (start + startSoFar, start + startSoFar + bestRowLength);
116 cacheInfo.SetRowHeight (cacheInfo.GetRowCount () - 1, newRowHeight);
117
118 startSoFar += bestRowLength;
119 Assert (len >= startSoFar);
120 leftToGo = len - startSoFar;
121 }
122
123 // always have at least one row...even if there were no bytes in the row
124 Assert (len == 0 or cacheInfo.PeekRowCount () != 0);
125 if (cacheInfo.PeekRowCount () == 0) {
126 Assert (len == 0);
127 Assert (startSoFar == 0);
128 cacheInfo.IncrementRowCountAndFixCacheBuffers (0, MeasureSegmentHeight (start, end));
129 }
130
131 cacheInfo.SetInterLineSpace (CalculateInterLineSpace (pm));
132 Assert (cacheInfo.GetRowCount () >= 1);
133 }
134 catch (...) {
135 // If we run into exceptions filling the cache, don't leave it in an inconsistent state.
136 // NB: this isn't necessarily the BEST way to leave it. It may mean - for example - trying to draw
137 // you get an exception and never will get ANYTHING drawn.. And thats not GREAT. But perahps better
138 // than silently putting things in the WRONG state (which would never get recovered) and allow drawing
139 // the WRONG thing. But much more likely - this will happen during early init stages when the
140 // window/etc aren't yet fully built/connected (NoTablet) - LGP990209
141 if (cacheInfo.PeekRowCount () == 0) {
142 cacheInfo.IncrementRowCountAndFixCacheBuffers (0, 20);
143 }
144 throw;
145 }
146}
147
148/*
149@METHOD: WordWrappedTextImager::AdjustBestRowLength
150@DESCRIPTION: <p>Virtual method called during word-wrapping to give various subclasses a crack at overriding the measured
151 wrap point. On function entry - 'rowLength' points to the calculated row length, and on output, it must be a valid
152 row length (possibly shorter than the original value, but always greater than zero).</p>
153*/
154void WordWrappedTextImager::AdjustBestRowLength (size_t /*textStart*/, const Led_tChar* text, const Led_tChar* end, size_t* rowLength)
155{
156 Require (*rowLength > 0);
157 for (const Led_tChar* cur = &text[0]; cur < end; cur = Led_NextChar (cur)) {
158 if (*cur == kSoftLineBreakChar) {
159 size_t newBestRowLength = (cur - text) + 1;
160 Assert (newBestRowLength <= *rowLength + 1); // Assure newBestRowLength is less than it would have been without the
161 // softlinebreak character, EXCEPT if the softlinebreak char is already
162 // at the spot we would have broken - then the row gets bigger by the
163 // one softlinebreak char length...
164 // LGP 2001-05-09 (see SPR707 test file-SimpleAlignDivTest.html)
165 Assert (newBestRowLength >= 1);
166 *rowLength = newBestRowLength;
167 break;
168 }
169 }
170}
171
172/*
173@METHOD: WordWrappedTextImager::ContainsMappedDisplayCharacters
174@DESCRIPTION: <p>Override @'TextImager::ContainsMappedDisplayCharacters' to hide kSoftLineBreakChar characters.
175 See @'qDefaultLedSoftLineBreakChar'.</p>
176*/
177bool WordWrappedTextImager::ContainsMappedDisplayCharacters (const Led_tChar* text, size_t nTChars) const
178{
179 return ContainsMappedDisplayCharacters_HelperForChar (text, nTChars, kSoftLineBreakChar) or
180 inherited::ContainsMappedDisplayCharacters (text, nTChars);
181}
182
183/*
184@METHOD: WordWrappedTextImager::RemoveMappedDisplayCharacters
185@DESCRIPTION: <p>Override @'TextImager::RemoveMappedDisplayCharacters' to hide kSoftLineBreakChar characters.</p>
186*/
187size_t WordWrappedTextImager::RemoveMappedDisplayCharacters (Led_tChar* copyText, size_t nTChars) const
188{
189 size_t newLen = inherited::RemoveMappedDisplayCharacters (copyText, nTChars);
190 Assert (newLen <= nTChars);
191 size_t newerLen = RemoveMappedDisplayCharacters_HelperForChar (copyText, newLen, kSoftLineBreakChar);
192 Assert (newerLen <= newLen);
193 Assert (newerLen <= nTChars);
194 return newerLen;
195}
196
197/*
198@METHOD: WordWrappedTextImager::PatchWidthRemoveMappedDisplayCharacters
199@DESCRIPTION: <p>Override @'TextImager::PatchWidthRemoveMappedDisplayCharacters' to hide kSoftLineBreakChar characters.</p>
200*/
201void WordWrappedTextImager::PatchWidthRemoveMappedDisplayCharacters (const Led_tChar* srcText, DistanceType* distanceResults, size_t nTChars) const
202{
203 inherited::PatchWidthRemoveMappedDisplayCharacters (srcText, distanceResults, nTChars);
204 PatchWidthRemoveMappedDisplayCharacters_HelperForChar (srcText, distanceResults, nTChars, kSoftLineBreakChar);
205}
206
207/*
208@METHOD: WordWrappedTextImager::FindWrapPointForMeasuredText
209@DESCRIPTION: <p>Helper for word wrapping text. This function is given text, and pre-computed text measurements for the
210 width of each character (Led_tChar, more accurately). Before calling this, the offsets have been adjused for tabstops.
211 This just computes the appropriate place to break the line into rows (just first row).</p>
212*/
213size_t WordWrappedTextImager::FindWrapPointForMeasuredText (const Led_tChar* text, size_t length, DistanceType wrapWidth,
214 size_t offsetToMarkerCoords, const DistanceType* widthsVector, size_t startSoFar)
215{
216 RequireNotNull (widthsVector);
217 Require (wrapWidth >= 1);
218 size_t bestRowLength = 0;
219
220 /*
221 * SUBTLE!
222 *
223 * Because we measure the text widths on an entire paragraph at a time (for efficiency),
224 * we run into the possability of chosing a wrap point wrongly by a fraction of a pixel.
225 * This is because characters widths don't necessarily fit on pixel boundaries. They can
226 * overlap one another, and due to things like kerning, the width from one point in the string
227 * to another can not EXACTLY reflect the length of that string (in pixels) - from measuretext.
228 *
229 * The problem occurs because offsets in the MIDDLE of a paragraph we re-interpret the offset
230 * as being zero, and so lose a fraction of a pixel. This means that a row of text can be either
231 * a fraction of a pixel LONGER than we measure or a fraction of a pixel too short.
232 *
233 * This COULD - in principle - mean that we get a small fraction of a character drawn cut-off.
234 * To correct for this, when we are on any starting point in the middle of the paragraph, we lessen
235 * the wrap-width by one pixel. This guarantees that we never cut anything off. Though in the worse
236 * case it could mean we word wrap a little over a pixel too soon. And we will generally word-wrap
237 * a pixel too soon (for rows after the first). It is my judgement - at least for now - that this
238 * is acceptable, and not generally noticable. Certiainly much less noticable than when a chracter
239 * gets cut off.
240 *
241 * For more info, see SPR#435.
242 */
243 if (startSoFar != 0) {
244 --wrapWidth;
245 }
246
247 /*
248 * Try to avoid sweeping the whole line looking for line breaks by
249 * first checking near the end of the line
250 */
251 size_t guessIndex = 0;
252
253 const size_t kCharsFromEndToSearchFrom = 5; // should be half of average word size (or so)
254 size_t bestBreakPointIndex = 1;
255 for (; bestBreakPointIndex <= length; ++bestBreakPointIndex) {
256 DistanceType guessWidth = LookupLengthInVector (widthsVector, startSoFar, bestBreakPointIndex);
257 if (guessWidth > wrapWidth) {
258 if (bestBreakPointIndex > 1) {
259 --bestBreakPointIndex; // because overshot above
260 }
261
262 if (bestBreakPointIndex > (kCharsFromEndToSearchFrom + 5)) { // no point on a short search
263 Assert (bestBreakPointIndex > kCharsFromEndToSearchFrom);
264 guessIndex = bestBreakPointIndex - kCharsFromEndToSearchFrom;
265 }
266 break;
267 }
268 }
269
270 if (bestBreakPointIndex >= length) {
271 Assert (bestBreakPointIndex <= length + 1); // else last char in text with be 1/2 dbcs char
272 bestRowLength = length;
273 Assert (guessIndex == 0);
274 }
275 else {
276 size_t wordWrapMax = (Led_NextChar (&text[bestBreakPointIndex]) - text);
277 Assert (wordWrapMax <= length); // cuz only way could fail is if we had split character, or were already at end, in which case
278 // we'd be in other part of if-test.
279
280 if (guessIndex != 0) {
281 bestRowLength = TryToFindWrapPointForMeasuredText1 (text, length, wrapWidth, offsetToMarkerCoords, widthsVector, startSoFar,
282 guessIndex, wordWrapMax);
283
284 if (bestRowLength == 0) {
285 if (bestBreakPointIndex > (kCharsFromEndToSearchFrom * 3 + 5)) { // no point on a short search
286 guessIndex = bestBreakPointIndex - kCharsFromEndToSearchFrom * 3;
287 bestRowLength = TryToFindWrapPointForMeasuredText1 (text, length, wrapWidth, offsetToMarkerCoords, widthsVector,
288 startSoFar, guessIndex, wordWrapMax);
289 }
290 }
291 }
292 if (bestRowLength == 0) {
293 bestRowLength = TryToFindWrapPointForMeasuredText1 (text, length, wrapWidth, offsetToMarkerCoords, widthsVector, startSoFar, 0, wordWrapMax);
294
295 if (bestRowLength == 0) {
296 /*
297 * If we got here then there was no good breaking point - we must have one VERY long word
298 * (or a relatively narrow layout width).
299 */
300 Assert (bestBreakPointIndex ==
301 FindWrapPointForOneLongWordForMeasuredText (text, length, wrapWidth, offsetToMarkerCoords, widthsVector, startSoFar));
302 bestRowLength = bestBreakPointIndex;
303 }
304 }
305 }
306
307 Assert (bestRowLength > 0);
308 return (bestRowLength);
309}
310
311size_t WordWrappedTextImager::TryToFindWrapPointForMeasuredText1 (const Led_tChar* text, size_t length, DistanceType wrapWidth,
312 size_t /*offsetToMarkerCoords*/, const DistanceType* widthsVector,
313 size_t startSoFar, size_t searchStart, size_t wrapLength)
314{
315 AssertNotNull (widthsVector);
316
317 Assert (wrapLength <= length);
318
319 shared_ptr<TextBreaks> breaker = GetTextStore ().GetTextBreaker ();
320
321 /*
322 * We take a bit of text here - and decide the proper position in the text to make the break.
323 * return 0 for bestRowLength if we could not find a good breaking point
324 */
325 AssertNotNull (text);
326 size_t bestRowLength = 0;
327 DistanceType width = 0;
328 size_t wordEnd = 0;
329 bool wordReal = false;
330 size_t lastLineTest = 0;
331 for (size_t i = searchStart; i < wrapLength;) {
332 // skip nonwhitespace, characters - then measure distance.
333 lastLineTest = i;
334 breaker->FindLineBreaks (text, wrapLength, i, &wordEnd, &wordReal);
335
336 Assert (i < wordEnd);
337 width = LookupLengthInVector (widthsVector, startSoFar, wordReal ? wordEnd : i);
338 i = wordEnd;
339
340 Assert (i > 0);
341 Assert (i <= wrapLength);
342
343 /*
344 * This code to only break if wordReal has the effect of "eating" up a string of
345 * spaces at the end of the row. I'm not 100% this is always the "right" thing
346 * todo, but seems ot be what is done most of the time in most editors. We will
347 * do so unconditionarlly - at least for now.
348 */
349 if (width > wrapWidth) {
350 break;
351 }
352 else {
353 bestRowLength = i;
354 }
355 }
356
357 if ((not wordReal) and (wrapLength < length) and (bestRowLength != 0)) {
358 // may be a lot of trailing whitespace that could be lost
359 breaker->FindLineBreaks (text, length, lastLineTest, &wordEnd, &wordReal);
360 Assert (not wordReal);
361 bestRowLength = wordEnd;
362 }
363
364 return bestRowLength;
365}
366
367size_t WordWrappedTextImager::FindWrapPointForOneLongWordForMeasuredText (const Led_tChar* /*text*/, size_t length, DistanceType wrapWidth,
368 size_t offsetToMarkerCoords, const DistanceType* widthsVector, size_t startSoFar)
369{
370 size_t bestRowLength = 0;
371
372 // Try binary search to find the best point to break up the
373 // really big word. But don't bother with all the fancy stuff. Just take the charwidth of
374 // the first character as an estimate, and then spin up or down til we get just the
375 // right length...
376 [[maybe_unused]] size_t secondCharIdx = FindNextCharacter (offsetToMarkerCoords + 0);
377 Assert (secondCharIdx >= offsetToMarkerCoords);
378 DistanceType fullWordWidth = LookupLengthInVector (widthsVector, startSoFar, length);
379
380 Assert (length >= 1);
381 size_t guessIdx = size_t ((length - 1) * (float (wrapWidth) / float (fullWordWidth)));
382 Assert (guessIdx < length);
383
384 /*
385 * Note - at this point guessIdx may not be on an even character boundary.
386 * So our first job is to make sure it doesn't split a character.
387 */
388 Assert (guessIdx < length);
389
390 DistanceType guessWidth = LookupLengthInVector (widthsVector, startSoFar, guessIdx);
391 bestRowLength = guessIdx;
392
393 if (guessWidth > wrapWidth) {
394 // keeping going down til we are fit.
395 for (size_t j = guessIdx; j >= 1; j = FindPreviousCharacter (offsetToMarkerCoords + j) - offsetToMarkerCoords) {
396 Assert (j < length); // no wrap
397 DistanceType smallerWidth = LookupLengthInVector (widthsVector, startSoFar, j);
398 bestRowLength = j;
399 if (smallerWidth <= wrapWidth) {
400 break;
401 }
402 }
403 }
404 else {
405 // keeping going down til we are fit.
406 for (size_t j = guessIdx; j < length; j = FindNextCharacter (offsetToMarkerCoords + j) - offsetToMarkerCoords) {
407 Assert (j < length); // no wrap
408 DistanceType smallerWidth = LookupLengthInVector (widthsVector, startSoFar, j);
409 if (smallerWidth > wrapWidth) {
410 break;
411 }
412 bestRowLength = j;
413 }
414 }
415
416 // Must always consume at least one character, even if it won't fit entirely
417 if (bestRowLength == 0) {
418 bestRowLength = 1;
419 }
420
421 return (bestRowLength);
422}
423
424#endif
#define AssertNotNull(p)
Definition Assertions.h:333
#define RequireNotNull(p)
Definition Assertions.h:347
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...