Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
PartitioningTextImager.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4
5#include "PartitioningTextImager.h"
6
7using namespace Stroika::Foundation;
8
9using namespace Stroika::Frameworks;
10using namespace Stroika::Frameworks::Led;
11
12using PartitionMarker = Partition::PartitionMarker;
13
14/*
15 ********************************************************************************
16 ************************************ Partition *********************************
17 ********************************************************************************
18 */
19Partition::Partition (TextStore& textStore)
20 :
22 fFinalConstructCalled{false}
23 ,
24#endif
25 fTextStore{textStore}
26 , fFindContainingPMCache{nullptr}
27 , fPartitionWatchers{}
28 , fMarkersToBeDeleted{}
30 , fPartitionMarkerCount{0}
31#endif
32 , fPartitionMarkerFirst{nullptr}
33 , fPartitionMarkerLast{nullptr}
34{
35 fTextStore.AddMarkerOwner (this);
36}
37
38Partition::~Partition ()
39{
40 // This was commented out as of 20000119 - not sure why... If it causes trouble maybe
41 // OK to just force empty here/now. But understand if so/why...
42 Assert (fMarkersToBeDeleted.IsEmpty ()); // these better be deleted by now!
43
44 /*
45 * We could simply loop and remove all markers with RemoveMarker().
46 * But using RemoveMarkers() is much faster. The trouble is we may
47 * not have enough memory to do the removeal, and an exception here
48 * would be very bad (deleting object shouldn't fail cuz not enuf memory!).
49 *
50 * So we compromise, and use a small stack buffer to delete as much
51 * as we can at a time. For must cases, this will be plenty.
52 *
53 * The alternative was to use a Led_Array - and if we ran out of memory,
54 * then to fall back to old slow strategy. That should work fine too, but I think
55 * this is slightly simpler.
56 */
57 for (PartitionMarker* cur = fPartitionMarkerFirst; cur != nullptr;) {
58 Marker* markersToRemoveAtATime[1000];
59 const size_t kMaxBufMarkers = Memory::NEltsOf (markersToRemoveAtATime);
60 size_t i = 0;
61 for (; i < kMaxBufMarkers and cur != nullptr; ++i, cur = cur->fNext) {
62 markersToRemoveAtATime[i] = cur;
63 }
64 fTextStore.RemoveMarkers (markersToRemoveAtATime, i);
65#if qStroika_Foundation_Debug_AssertionsChecked
66 fPartitionMarkerCount -= i;
67#endif
68 for (; i != 0; --i) {
69 delete (markersToRemoveAtATime[i - 1]);
70 }
71 }
72 fTextStore.RemoveMarkerOwner (this);
73#if qStroika_Foundation_Debug_AssertionsChecked
74 Ensure (fPartitionMarkerCount == 0);
75#endif
76}
77
78/*
79@METHOD: Partition::FinalConstruct
80@DESCRIPTION: <p>Logically this should be part of the constructor. But since it needs to call a method
81 (@'Partition::MakeNewPartitionMarker ()') which is virtual, and must be bound in the final complete
82 class. So it must be done as a separate call. It is illegal to call this more than once, or to call any
83 of the other class methods without having called it. These errors will be detected (in debug builds)
84 where possible.</p>
85 */
86void Partition::FinalConstruct ()
87{
88#if qStroika_Foundation_Debug_AssertionsChecked
89 Require (not fFinalConstructCalled);
90 fFinalConstructCalled = true;
91 Assert (fPartitionMarkerCount == 0);
92#endif
93 Assert (fPartitionMarkerFirst == nullptr);
94 PartitionMarker* pm = MakeNewPartitionMarker (nullptr);
95#if qStroika_Foundation_Debug_AssertionsChecked
96 ++fPartitionMarkerCount;
97#endif
98 fTextStore.AddMarker (pm, 0, fTextStore.GetLength () + 1, this); // include ALL text
99 Assert (fPartitionMarkerFirst == pm);
100 Assert (fPartitionMarkerLast == pm);
101}
102
103TextStore* Partition::PeekAtTextStore () const
104{
105 return &fTextStore;
106}
107
108/*
109 @METHOD: Partition::GetPartitionMarkerContainingPosition
110 @DESCRIPTION: <p>Finds the @'PartitioningTextImager::PartitionMarker' which contains the given character#.
111 Note, the use of 'charPosition' rather than markerpos is to disambiguiate the case where we are at the boundary
112 between two partition elements.</p>
113 */
114PartitionMarker* Partition::GetPartitionMarkerContainingPosition (size_t charPosition) const
115{
116#if qStroika_Foundation_Debug_AssertionsChecked
117 Require (fFinalConstructCalled);
118#endif
119 Require (charPosition <= GetEnd () + 1); // cuz last PM contains bogus char past end of buffer
120
121 /*
122 * Based on cached value, either search forwards from there or back.
123 */
124 PartitionMarker* pm = fFindContainingPMCache;
125 if (pm == nullptr) {
126 pm = fPartitionMarkerFirst; // could be first time, or could have deleted cached value
127 }
128 AssertNotNull (pm);
129
130 if (charPosition < 10) {
131 pm = fPartitionMarkerFirst;
132 }
133 else if (charPosition + 10 > GetEnd ()) {
134 pm = fPartitionMarkerLast;
135 }
136
137 bool loopForwards = pm->GetStart () <= charPosition;
138 for (; pm != nullptr; pm = loopForwards ? pm->fNext : pm->fPrevious) {
139 if (Contains (*pm, charPosition)) {
140 if (pm != fPartitionMarkerFirst and pm != fPartitionMarkerLast) {
141 fFindContainingPMCache = pm;
142 }
143 EnsureNotNull (pm);
144 return (pm);
145 }
146 }
148 return (nullptr);
149}
150
151/*
152 @METHOD: Partition::MakeNewPartitionMarker
153 @DESCRIPTION: <p>Method which is called to construct new partition elements. Override this if you subclass
154 @'PartitioningTextImager', and want to provide your own subtype of @'PartitioningTextImager::PartitionMarker'.</p>
155 */
156PartitionMarker* Partition::MakeNewPartitionMarker (PartitionMarker* insertAfterMe)
157{
158#if qStroika_Foundation_Debug_AssertionsChecked
159 Require (fFinalConstructCalled);
160#endif
161 return new PartitionMarker{*this, insertAfterMe};
162}
163
164/*
165 @METHOD: Partition::Split
166 @DESCRIPTION: <p>Split the given PartitionMarker at position 'at' - which must fall within the partition element.
167 This method always produces a new partition element, and inserts it appropriately into the partition, adjusting
168 the sizes of surrounding elements appropriately.</p>
169 <p>Ensure that the split happens so that PM continues to point to the beginning of the range and the new PMs created
170 are at the end of the range</p>
171 <p>This method is typically called by Partition subclasses OVERRIDE of @'Partition::UpdatePartitions'.</p>
172 */
173void Partition::Split (PartitionMarker* pm, size_t at)
174{
175#if qStroika_Foundation_Debug_AssertionsChecked
176 Require (fFinalConstructCalled);
177#endif
178 RequireNotNull (pm);
179 Require (pm->GetStart () < at);
180 Require (pm->GetEnd () > at);
181
182 vector<void*> watcherInfos;
183 DoAboutToSplitCalls (pm, at, &watcherInfos);
184
185 size_t start = 0;
186 size_t end = 0;
187 pm->GetRange (&start, &end);
188 Assert (at >= start and at <= end);
189
190 // try to cleanup as best as we can if we fail in the middle of adding a new PM.
191 PartitionMarker* newPM = MakeNewPartitionMarker (pm);
192 Assert (pm->GetNext () == newPM);
193 Assert (newPM->GetPrevious () == pm);
194
195 // NB: we set marker range before adding new marker for no good reason, except that
196 // for quirky reasons I don't understand, reading in files is MUCH faster that
197 // way - I really don't understand the idiocyncracies of the ChunkedArrayTextStore
198 // performance... but luckily - no real need to...
199 // LGP 960515
200 fTextStore.SetMarkerEnd (pm, at);
201 try {
202 fTextStore.AddMarker (newPM, at, end - at, this);
203 }
204 catch (...) {
205 // the only item above that can throw is the AddMarker () call. So at this point, all we need todo
206 // is remove newPM from our linked list, and then delete it. Of course, our Partition elements won't be
207 // quite right. But with no memory, this is the best we can do. At least we still have a partition!
208 AssertNotNull (newPM);
209 if (pm == nullptr) {
210 fPartitionMarkerFirst = newPM->fNext;
211 }
212 else {
213 pm->fNext = newPM->fNext;
214 }
215 // Now patch pm's old and new successor (before and after newPM briefly was it)
216 if (newPM->fNext != nullptr) {
217 Assert (newPM->fNext->fPrevious == newPM); // but thats getting deleted...
218 newPM->fNext->fPrevious = pm;
219 }
220 if (newPM == fPartitionMarkerLast) {
221 fPartitionMarkerLast = newPM->fPrevious;
222 }
223
224 // Since we set this BEFORE the AddMarker, we must set it back. Thats the only
225 // cost in the above '960515' performance hack - and it only occurs EXTREMELY
226 // RARELY and in a case I don't care about.
227 // LGP 960515
228 fTextStore.SetMarkerRange (pm, start, end);
229
230 throw;
231 }
232#if qStroika_Foundation_Debug_AssertionsChecked
233 ++fPartitionMarkerCount;
234#endif
235 DoDidSplitCalls (watcherInfos);
236}
237
238/*
239@METHOD: Partition::Coalece
240@DESCRIPTION: <p>Coalece the given PartitionMarker with the one which follows it (if any exists). If coalesced
241 with the following element, the given 'pm' is deleted. Frequently this doesn't happen immediately (mainly
242 for performance reasons, but possibly also to avoid bugs where the marker is still in some list being iterated over
243 and having DidUpdate() etc methods called on it). It is accumulated for later deletion using
244 @'Partition::AccumulateMarkerForDeletion'.</p>
245 <p>This method is typically called by Partition subclasses OVERRIDE of @'Partition::UpdatePartitions'.</p>
246*/
247void Partition::Coalece (PartitionMarker* pm)
248{
249#if qStroika_Foundation_Debug_AssertionsChecked
250 Require (fFinalConstructCalled);
251#endif
252 AssertNotNull (pm);
253 vector<void*> watcherInfos;
254 DoAboutToCoaleceCalls (pm, &watcherInfos);
255 if (pm->fNext != nullptr) { // We don't do anything to coalesce the last item - nothing to coalesce with!
256 size_t start = 0;
257 size_t end = 0;
258 pm->GetRange (&start, &end);
259 size_t lengthToAdd = end - start;
260
261 PartitionMarker* successor = pm->fNext;
262 AssertNotNull (successor);
263 successor->GetRange (&start, &end);
264 Assert (start >= lengthToAdd);
265 fTextStore.SetMarkerStart (successor, start - lengthToAdd);
266
267 Assert (successor->fPrevious == pm);
268 successor->fPrevious = pm->fPrevious;
269 Assert ((pm->fPrevious == nullptr) or (pm->fPrevious->fNext == pm));
270 if (pm->fPrevious == nullptr) {
271 Assert (fPartitionMarkerFirst == pm);
272 fPartitionMarkerFirst = pm->fNext;
273 }
274 else {
275 pm->fPrevious->fNext = pm->fNext;
276 }
277 if (pm->fNext == nullptr) {
278 Assert (fPartitionMarkerLast == pm);
279 fPartitionMarkerLast = pm->fPrevious;
280 }
281#if qStroika_Foundation_Debug_AssertionsChecked
282 --fPartitionMarkerCount;
283#endif
284 AccumulateMarkerForDeletion (pm);
285 }
286 DoDidCoaleceCalls (watcherInfos);
287}
288
289/*
290@METHOD: Partition::AccumulateMarkerForDeletion
291@DESCRIPTION: <p>Wrap @'MarkerMortuary<MARKER>::AccumulateMarkerForDeletion' - and make sure our cache
292 isn't pointing to a deleted marker.</p>
293*/
294void Partition::AccumulateMarkerForDeletion (PartitionMarker* m)
295{
296#if qStroika_Foundation_Debug_AssertionsChecked
297 Require (fFinalConstructCalled);
298#endif
299 AssertNotNull (m);
300 Assert (&m->GetOwner () == this);
301 fMarkersToBeDeleted.AccumulateMarkerForDeletion (m);
302 if (fFindContainingPMCache == m) {
303 fFindContainingPMCache = nullptr;
304 }
305}
306
307void Partition::AboutToUpdateText (const UpdateInfo& updateInfo)
308{
309#if qStroika_Foundation_Debug_AssertionsChecked
310 Require (fFinalConstructCalled);
311#endif
312 Assert (fMarkersToBeDeleted.IsEmpty ()); // would be bad to do a replace with any of these not
313 // yet finalized since they would then appear in the
314 // CollectAllMarkersInRange() and get DidUpdate calls!
315 Invariant ();
316 inherited::AboutToUpdateText (updateInfo);
317}
318
319void Partition::DidUpdateText (const UpdateInfo& updateInfo) noexcept
320{
321#if qStroika_Foundation_Debug_AssertionsChecked
322 Require (fFinalConstructCalled);
323#endif
324 fMarkersToBeDeleted.FinalizeMarkerDeletions ();
325 inherited::DidUpdateText (updateInfo);
326 Invariant ();
327}
328
329#if qStroika_Foundation_Debug_AssertionsChecked
330void Partition::Invariant_ () const
331{
332#if qStroika_Foundation_Debug_AssertionsChecked
333 Require (fFinalConstructCalled);
334#endif
335 size_t lastCharDrawn = 0;
336 Assert (fPartitionMarkerCount != 0);
337 size_t realPMCount = 0;
338 for (PartitionMarker* cur = fPartitionMarkerFirst; cur != nullptr; cur = cur->fNext) {
339 PartitionMarker* pm = cur;
340 AssertNotNull (pm);
341 Assert (&pm->GetOwner () == this);
342 size_t start = pm->GetStart ();
343 size_t end = pm->GetEnd ();
344 Assert (start == lastCharDrawn);
345 Assert (end <= GetEnd () + 1); // +1 for extra bogus space so we always get autoexpanded
346 lastCharDrawn = end;
347 ++realPMCount;
348 Assert (realPMCount <= fPartitionMarkerCount);
349 Assert (static_cast<bool> (fPartitionMarkerLast == cur) == static_cast<bool> (cur->fNext == nullptr));
350 }
351 Assert (realPMCount == fPartitionMarkerCount);
352 Assert (lastCharDrawn == GetEnd () + 1);
353}
354#endif
355
356#if qStroika_Frameworks_Led_SupportGDI
357/*
358 ********************************************************************************
359 ****************************** PartitioningTextImager **************************
360 ********************************************************************************
361 */
362PartitioningTextImager::PartitioningTextImager ()
363 : fPartition{nullptr}
364#if qCacheTextMeasurementsForPM
365 , fMeasureTextCache ()
366#endif
367{
368}
369
370PartitioningTextImager::~PartitioningTextImager ()
371{
372 Require (fPartition.get () == nullptr);
373}
374
375/*
376@METHOD: PartitioningTextImager::SetPartition
377@DESCRIPTION: <p>Associates the given parition (@'PartitionPtr') with this @'PartitioningTextImager'.
378 It can be nullptr.</p>
379 <p>The method is virtual, in case you need to hook partition changes by updating
380 other derived/cached information. But if overridding, be sure to always call the inherited version.</p>
381*/
382void PartitioningTextImager::SetPartition (const PartitionPtr& partitionPtr)
383{
384#if qCacheTextMeasurementsForPM
385 fMeasureTextCache.reset ();
386#endif
387 fPartition = partitionPtr;
388#if qCacheTextMeasurementsForPM
389 if (partitionPtr.get () != nullptr) {
390 fMeasureTextCache = unique_ptr<MeasureTextCache> (new MeasureTextCache (partitionPtr));
391 }
392#endif
393}
394
395#if qCacheTextMeasurementsForPM
396/*
397@METHOD: PartitioningTextImager::InvalidateAllCaches
398@DESCRIPTION: <p>Hook the @'TextImager::InvalidateAllCaches' message to free some additional caches.</p>
399*/
400void PartitioningTextImager::InvalidateAllCaches ()
401{
402 inherited::InvalidateAllCaches ();
403 if (fMeasureTextCache.get () != nullptr) {
404 fMeasureTextCache->ClearAll ();
405 }
406}
407#endif
408
409/*
410@METHOD: PartitioningTextImager::GetPrimaryPartitionTextDirection
411@DESCRIPTION: <p>Return the primary text direction for the partition. This COULD be specified by some external
412 user setting, or - by default - simply computed from the text direction of the first row of the
413 paragraph.</p>
414*/
415TextDirection PartitioningTextImager::GetPrimaryPartitionTextDirection (size_t rowContainingCharPosition) const
416{
417 return GetTextDirection (GetStartOfPartitionContainingPosition (rowContainingCharPosition));
418}
419
420TextLayoutBlock_Copy PartitioningTextImager::GetTextLayoutBlock (size_t rowStart, size_t rowEnd) const
421{
422 if (rowStart == GetStartOfPartitionContainingPosition (rowStart)) {
423 return inherited::GetTextLayoutBlock (rowStart, rowEnd);
424 }
425 else {
426 size_t rowLen = rowEnd - rowStart;
427 Memory::StackBuffer<Led_tChar> rowBuf{Memory::eUninitialized, rowLen};
428 CopyOut (rowStart, rowLen, rowBuf.data ());
429 return TextLayoutBlock_Copy (TextLayoutBlock_Basic{rowBuf.data (), rowBuf.data () + rowLen, GetPrimaryPartitionTextDirection (rowStart)});
430 }
431}
432
433/*
434@METHOD: PartitioningTextImager::GetTextDirection
435@DESCRIPTION: <p>Implementation of abstract interface @'TextImager::GetTextDirection'</p>
436*/
437TextDirection PartitioningTextImager::GetTextDirection (size_t charPosition) const
438{
439 size_t startOfRow = GetStartOfRowContainingPosition (charPosition);
440 size_t endOfRow = GetEndOfRowContainingPosition (startOfRow);
441 if (charPosition == endOfRow) {
442 return eLeftToRight; //??Not sure what todo here??
443 // should probably grab from ambient setting for paragraph direction or something like that
444 }
445 else {
446 return GetTextLayoutBlock (startOfRow, endOfRow).GetCharacterDirection (charPosition - startOfRow);
447 }
448}
449
450/*
451@METHOD: PartitioningTextImager::CalcSegmentSize
452@DESCRIPTION: <p>Implementation of abstract interface @'TextImager::CalcSegmentSize'</p>
453 <p>Note that internally - this must grab the ENTIRE ROW to measure (or nearly so).
454 To assure ligatures are measured properly (to could be part of a ligature) we need the characters just past
455 'to' to measure it properly. And - to measure tabs properly - we need to go before 'from' all the way to
456 the start of the row. So - we just always goto the starts and ends of rows. Since
457 @'PartitioningTextImager::CalcSegmentSize_CACHING' caches these values - this isn't a great cost.</p>
458*/
459DistanceType PartitioningTextImager::CalcSegmentSize (size_t from, size_t to) const
460{
461#if !qCacheTextMeasurementsForPM || qStroika_Foundation_Debug_AssertionsChecked
462 DistanceType referenceValue = CalcSegmentSize_REFERENCE (from, to);
463#endif
464
465#if qCacheTextMeasurementsForPM
466 DistanceType value = CalcSegmentSize_CACHING (from, to);
467#if qStroika_Foundation_Debug_AssertionsChecked
468 Assert (value == referenceValue);
469#endif
470 return value;
471#else
472 return referenceValue;
473#endif
474}
475
476/*
477@METHOD: PartitioningTextImager::CalcSegmentSize_REFERENCE
478@ACCESS: private
479@DESCRIPTION: <p>Similar to @'PartitioningTextImager::CalcSegmentSize_CACHING' but recalculating the measurements
480 each time as a check that the cache has not somehow (undetected) become invalid (say cuz a font changed
481 and we weren't notified?).</p>
482*/
483DistanceType PartitioningTextImager::CalcSegmentSize_REFERENCE (size_t from, size_t to) const
484{
485 Require (from <= to);
486
487 if (from == to) {
488 return 0;
489 }
490 else {
491 size_t startOfRow = GetStartOfRowContainingPosition (from);
492 size_t rowEnd = GetEndOfRowContainingPosition (startOfRow);
493 Require (startOfRow <= from); // WE REQUIRE from/to be contained within a single row!!!
494 Require (to <= rowEnd); // ''
495 size_t rowLen = rowEnd - startOfRow;
496 Memory::StackBuffer<DistanceType> distanceVector{Memory::eUninitialized, rowLen};
497 CalcSegmentSize_FillIn (startOfRow, rowEnd, distanceVector.data ());
498 Assert (to > startOfRow); // but from could be == startOfRow, so must be careful of that...
499 Assert (to - startOfRow - 1 < (GetEndOfRowContainingPosition (startOfRow) - startOfRow)); // now buffer overflows!
500 DistanceType result = distanceVector[to - startOfRow - 1];
501 if (from != startOfRow) {
502 result -= distanceVector[from - startOfRow - 1];
503 }
504 return (result);
505 }
506}
507
508#if qCacheTextMeasurementsForPM
509/*
510@METHOD: PartitioningTextImager::CalcSegmentSize_CACHING
511@ACCESS: private
512@DESCRIPTION: <p>Caching implementation of @'PartitioningTextImager::CalcSegmentSize'. Values checked by
513 calls to related @'PartitioningTextImager::CalcSegmentSize_REFERENCE'.</p>
514*/
515DistanceType PartitioningTextImager::CalcSegmentSize_CACHING (size_t from, size_t to) const
516{
517 Require (from <= to);
518
519 if (from == to) {
520 return 0;
521 }
522 PartitionMarker* pm = GetPartitionMarkerContainingPosition (from);
523 Require (pm->GetEnd () == to or pm == GetPartitionMarkerContainingPosition (to)); // since must be in same row, must be in same PM.
524
525 size_t startOfRow = GetStartOfRowContainingPosition (from);
526 Require (GetEndOfRowContainingPosition (startOfRow) >= to); // WE REQUIRE from/to be contained within a single row!!!
527
528 MeasureTextCache::CacheElt ce = fMeasureTextCache->LookupValue (pm, startOfRow, [this] (PartitionMarker* pm, size_t startOfRow) {
529 MeasureTextCache::CacheElt newCE{MeasureTextCache::CacheElt::COMPARE_ITEM (pm, startOfRow)};
530 size_t rowEnd = GetEndOfRowContainingPosition (startOfRow);
531 size_t rowLen = rowEnd - startOfRow;
532 newCE.fMeasurementsCache.GrowToSize (rowLen);
533 CalcSegmentSize_FillIn (startOfRow, rowEnd, newCE.fMeasurementsCache.data ());
534 return newCE;
535 });
536 const DistanceType* measurementsCache = ce.fMeasurementsCache.data ();
537
538 Assert (to > startOfRow); // but from could be == startOfRow, so must be careful of that...
539 Assert (to - startOfRow - 1 < (GetEndOfRowContainingPosition (startOfRow) - startOfRow)); // now buffer overflows!
540 DistanceType result = measurementsCache[to - startOfRow - 1];
541 if (from != startOfRow) {
542 result -= measurementsCache[from - startOfRow - 1];
543 }
544 return (result);
545}
546#endif
547
548/*
549@METHOD: PartitioningTextImager::CalcSegmentSize_FillIn
550@ACCESS: private
551@DESCRIPTION: <p>The 'rowStart' argument MUST start a row, and rowEnd must END that same row. 'distanceVector' must be a
552 non-null array whose size is set to at least (rowEnd-rowStart) elements.</p>
553*/
554void PartitioningTextImager::CalcSegmentSize_FillIn (size_t rowStart, size_t rowEnd, DistanceType* distanceVector) const
555{
556 Require (rowStart == GetStartOfRowContainingPosition (rowStart)); // must already be a rowstart
557 Require (rowEnd == GetEndOfRowContainingPosition (rowStart)); // ""
558 RequireNotNull (distanceVector);
559 Require (rowStart <= rowEnd);
560
561 // we must re-snag the text to get the width/tab alignment of the initial segment (for reset tabstops)- a bit more complicated ...
562 size_t len = rowEnd - rowStart;
563
564 Memory::StackBuffer<Led_tChar> fullRowTextBuf{Memory::eUninitialized, len};
565 CopyOut (rowStart, len, fullRowTextBuf.data ());
566
567 MeasureSegmentWidth (rowStart, rowEnd, fullRowTextBuf.data (), distanceVector);
568 (void)ResetTabStops (rowStart, fullRowTextBuf.data (), len, distanceVector, 0);
569}
570
571/*
572@METHOD: PartitioningTextImager::GetRowRelativeCharLoc
573@DESCRIPTION: <p>Implementation of abstract interface @'TextImager::GetRowRelativeCharLoc'</p>
574*/
575void PartitioningTextImager::GetRowRelativeCharLoc (size_t charLoc, DistanceType* lhs, DistanceType* rhs) const
576{
577 Require (charLoc <= GetEnd ());
578 RequireNotNull (lhs);
579 RequireNotNull (rhs);
580
581 /*
582 * Note that this algoritm assumes that TextImager::CalcSegmentSize () measures the VIRTUAL characters,
583 * including any mapped characters (mirroring) that correspond to the argument REAL character range.
584 */
585 size_t rowStart = GetStartOfRowContainingPosition (charLoc);
586 size_t rowEnd = GetEndOfRowContainingPosition (charLoc);
587 [[maybe_unused]] size_t rowLen = rowEnd - rowStart;
588 TextLayoutBlock_Copy rowText = GetTextLayoutBlock (rowStart, rowEnd);
589
590 using ScriptRunElt = TextLayoutBlock::ScriptRunElt;
591 vector<ScriptRunElt> runs = rowText.GetScriptRuns ();
592
593 /*
594 * Walk through the runs in VIRTUAL order (screen left to right). Find the run whose charLoc is inside
595 * the "REAL" run span. Stop measuring there.
596 */
597 Assert (not runs.empty () or (rowLen == 0));
598 if (runs.size () > 1) {
599 // sort by virtual start
600 sort (runs.begin (), runs.end (), TextLayoutBlock::LessThanVirtualStart ());
601 }
602 size_t rowRelCharLoc = charLoc - rowStart;
603 DistanceType spannedSoFar = 0;
604 for (auto i = runs.begin (); i != runs.end (); ++i) {
605 const ScriptRunElt& se = *i;
606 size_t runLength = se.fRealEnd - se.fRealStart;
607 // See if we are STRICTLY inside the run, or maybe at the last character of the last run
608 if ((se.fRealStart <= rowRelCharLoc) and ((rowRelCharLoc < se.fRealEnd) or ((rowRelCharLoc == se.fRealEnd) and (i + 1 == runs.end ())))) {
609 size_t absoluteSegStart = rowStart + se.fRealStart;
610 size_t subSegLen = rowRelCharLoc - se.fRealStart;
611 Assert (subSegLen <= runLength);
612
613 size_t nextPosition = FindNextCharacter (charLoc);
614 bool emptyChar = (nextPosition == charLoc);
615 if (not emptyChar) {
616 Led_tChar lastChar = '\0';
617 CopyOut (charLoc, 1, &lastChar);
618 emptyChar = (RemoveMappedDisplayCharacters (&lastChar, 1) == 0);
619 }
620
621 // EXPLAIN LOGIC (Once I have it right) ;-)
622 if (se.fDirection == eLeftToRight) {
623 *lhs = spannedSoFar + CalcSegmentSize (absoluteSegStart, absoluteSegStart + subSegLen);
624 if (emptyChar) {
625 *rhs = *lhs;
626 }
627 else {
628 *rhs = spannedSoFar + CalcSegmentSize (absoluteSegStart, FindNextCharacter (absoluteSegStart + subSegLen));
629 }
630 }
631 else {
632 CoordinateType segRHS = spannedSoFar + CalcSegmentSize (absoluteSegStart, absoluteSegStart + runLength);
633 *rhs = segRHS - CalcSegmentSize (absoluteSegStart, absoluteSegStart + subSegLen);
634 if (emptyChar) {
635 *lhs = *rhs;
636 }
637 else {
638 *lhs = segRHS - CalcSegmentSize (absoluteSegStart, FindNextCharacter (absoluteSegStart + subSegLen));
639 }
640 }
641 break;
642 }
643 else {
644 spannedSoFar += CalcSegmentSize (rowStart + se.fRealStart, rowStart + se.fRealEnd);
645 }
646 }
647 Ensure (*lhs <= *rhs); // can be equal for case like 'RemoveMappedDisplayCharacters'
648}
649
650/*
651@METHOD: PartitioningTextImager::GetRowRelativeCharAtLoc
652@DESCRIPTION: <p>Implementation of abstract interface @'TextImager::GetRowRelativeCharAtLoc'</p>
653*/
654size_t PartitioningTextImager::GetRowRelativeCharAtLoc (CoordinateType hOffset, size_t rowStart) const
655{
656 Require (rowStart == GetStartOfRowContainingPosition (rowStart));
657
658 /*
659 * Note that this algorithm assumes that TextImager::CalcSegmentSize () measures the VIRTUAL characters,
660 * including any mapped characters (mirroring) that correspond to the argument REAL character range.
661 */
662 size_t rowEnd = GetEndOfRowContainingPosition (rowStart);
663 [[maybe_unused]] size_t rowLen = rowEnd - rowStart;
664 TextLayoutBlock_Copy rowText = GetTextLayoutBlock (rowStart, rowEnd);
665
666 using ScriptRunElt = TextLayoutBlock::ScriptRunElt;
667 vector<ScriptRunElt> runs = rowText.GetScriptRuns ();
668
669 /*
670 * Walk through the runs in VIRTUAL order (screen left to right). Find the run whose hOffset is inside
671 * Then find the character which hOffset resides in (within that run).
672 */
673 Assert (not runs.empty () or (rowLen == 0));
674 if (runs.size () > 1) {
675 // sort by virtual start
676 sort (runs.begin (), runs.end (), TextLayoutBlock::LessThanVirtualStart{});
677 }
678 DistanceType spannedSoFar = 0;
679 TextDirection lastRunDir = eLeftToRight;
680 for (auto i = runs.begin (); i != runs.end (); ++i) {
681 const ScriptRunElt& se = *i;
682 DistanceType thisSpanWidth = CalcSegmentSize (rowStart + se.fRealStart, rowStart + se.fRealEnd);
683 DistanceType segVisStart = spannedSoFar;
684 DistanceType segVisEnd = segVisStart + thisSpanWidth;
685
686 lastRunDir = se.fDirection;
687
688 if (hOffset < static_cast<CoordinateType> (segVisEnd)) {
689 /*
690 * Must be this segment. NB: this takes care of special case where mouseLoc is BEFORE first segment in which case
691 * we treat as at the start of that segment...
692 *
693 * Now walk through the segment and see when we have enough chars gone by within the segment to
694 * get us past 'hOffset'
695 */
696 size_t absoluteSegStart = rowStart + se.fRealStart;
697 //size_t segLen = se.fRealEnd - se.fRealStart;
698
699 size_t prevEnd = rowStart + se.fRealStart;
700 size_t segEnd = rowStart + se.fRealEnd;
701 for (size_t curEnd = FindNextCharacter (prevEnd); curEnd < segEnd; (prevEnd = curEnd), (curEnd = FindNextCharacter (curEnd))) {
702 DistanceType hSize = CalcSegmentSize (absoluteSegStart, curEnd);
703 if (se.fDirection == eLeftToRight) {
704 if (static_cast<CoordinateType> (hSize + spannedSoFar) > hOffset) {
705 return prevEnd;
706 }
707 }
708 else {
709 if (static_cast<CoordinateType> (segVisEnd) - static_cast<CoordinateType> (hSize) < hOffset) {
710 return prevEnd;
711 }
712 }
713 }
714 return prevEnd;
715 }
716
717 spannedSoFar += thisSpanWidth;
718 }
719
720 Assert (hOffset > 0 or runs.size () == 0);
721 if (lastRunDir == eLeftToRight) {
722 return rowEnd;
723 }
724 else {
725 return rowStart;
726 }
727}
728
729/*
730@METHOD: PartitioningTextImager::ResetTabStops
731@DESCRIPTION: <p>Patch the charLocations for tab-stop locations, now that we know our previous wrap point.
732 Returns index of last tab found (allows for optimizations). Returns a number <= from if
733 no tabs found.</p>
734 <p>'from' defines where in the 'textStore' all these things are being measured relative to (typically the start of
735 a partition).</p>
736 <p>'text' is the text starting in the partition at position 'from', and extending 'nTChars' Led_tChars in length.</p>
737 <p>'charLocations' is a vector - of the widths, measured from @'TextImager::MeasureSegmentWidth'. As such, it
738 measures distances from 'from' to a position 'i' after from, with the distance of from..from being implied zero, so the first
739 elt of the array left out (in other words to find distance from..i, you say charLocations[i-from-1]).</p>
740 <p>'startSoFar' is an arg so you can only reset the tabstops for a given subset of the text - ignoring the initial portion.
741 So, if startSoFar==0, then it is assumed we are starting at the beginning of the charLocations array, but if startSoFar != 0,
742 we assume we can (and must) snag our starting width from what is already in the array at charLocations[startSoFar-1].</p>
743*/
744size_t PartitioningTextImager::ResetTabStops (size_t from, const Led_tChar* text, size_t nTChars, DistanceType* charLocations, size_t startSoFar) const
745{
746 RequireNotNull (charLocations);
747 size_t lastTabIndex = 0;
748 CoordinateType tabAdjust = 0;
749 DistanceType widthAtStart = (startSoFar == 0 ? 0 : charLocations[startSoFar - 1]);
750 for (size_t i = startSoFar; i < startSoFar + nTChars; i++) {
751 if (text[i] == '\t') {
752 DistanceType widthSoFar = (i == 0 ? 0 : charLocations[i - 1]);
753 tabAdjust = widthAtStart + GetTabStopList (from).ComputeTabStopAfterPosition (Tablet_Acquirer (this), widthSoFar - widthAtStart) -
754 charLocations[i];
755 lastTabIndex = i;
756 }
757 charLocations[i] += tabAdjust;
758 }
759 return (lastTabIndex);
760}
761
762#if qStroika_Foundation_Debug_AssertionsChecked
763void PartitioningTextImager::Invariant_ () const
764{
765 if (fPartition.get () != nullptr) {
766 fPartition->Invariant ();
767 Assert (fPartition->PeekAtTextStore () == PeekAtTextStore ());
768 }
769}
770#endif
771
772#if qCacheTextMeasurementsForPM
773/*
774 ********************************************************************************
775 ************* PartitioningTextImager::MeasureTextCache *************************
776 ********************************************************************************
777 */
778PartitioningTextImager::MeasureTextCache::MeasureTextCache (const PartitionPtr& partition)
779 : fPartition (partition)
780 , fCache (1)
781{
782 Assert (partition.get () != nullptr);
783 fPartition->AddPartitionWatcher (this);
784 TextStore& ts = partition->GetTextStore ();
785 ts.AddMarkerOwner (this);
786}
787
788PartitioningTextImager::MeasureTextCache::~MeasureTextCache ()
789{
790 fPartition->RemovePartitionWatcher (this);
791 TextStore& ts = fPartition->GetTextStore ();
792 ts.RemoveMarkerOwner (this);
793}
794
795void PartitioningTextImager::MeasureTextCache::AboutToSplit (PartitionMarker* pm, size_t /*at*/, void** infoRecord) const noexcept
796{
797 *infoRecord = pm;
798}
799
800void PartitioningTextImager::MeasureTextCache::DidSplit (void* infoRecord) const noexcept
801{
802 PartitionMarker* pm = reinterpret_cast<PartitionMarker*> (infoRecord);
803 fCache.clear ([pm] (const CacheElt::COMPARE_ITEM& c) { return pm == c.fPM; });
804}
805
806void PartitioningTextImager::MeasureTextCache::AboutToCoalece (PartitionMarker* pm, void** infoRecord) const noexcept
807{
808 RequireNotNull (infoRecord);
809 RequireNotNull (pm);
810 *infoRecord = pm;
811}
812
813void PartitioningTextImager::MeasureTextCache::DidCoalece (void* infoRecord) const noexcept
814{
815 PartitionMarker* pm = reinterpret_cast<PartitionMarker*> (infoRecord);
816 fCache.clear ([pm] (const CacheElt::COMPARE_ITEM& c) { return pm == c.fPM; });
817}
818
819TextStore* PartitioningTextImager::MeasureTextCache::PeekAtTextStore () const
820{
821 return fPartition->PeekAtTextStore ();
822}
823
824void PartitioningTextImager::MeasureTextCache::EarlyDidUpdateText (const UpdateInfo& updateInfo) noexcept
825{
826 {
827 size_t cacheSize = 3;
828 size_t bufLen = GetTextStore ().GetLength ();
829 const size_t kBigThresh1_ = 1024;
830 const size_t kBigThresh2_ = 10 * 1024;
831 const size_t kBigThresh3_ = 20 * 1024;
832 if (bufLen > kBigThresh1_) {
833 cacheSize = 5;
834 if (bufLen > kBigThresh2_) {
835 cacheSize = 10;
836 if (bufLen > kBigThresh3_) {
837 cacheSize = 75;
838 }
839 }
840 }
841 fCache.SetMaxCacheSize (cacheSize);
842 }
843
844 /*
845 * iterate over all markers in the range updated, and for each one - see if they intersect with
846 * any of the cache elements.
847 */
848 for (PartitionMarker* pm = fPartition->GetPartitionMarkerContainingPosition (FindPreviousCharacter (updateInfo.fReplaceFrom));
849 pm != nullptr; pm = pm->GetNext ()) {
850 if (pm->GetStart () > updateInfo.GetResultingRHS ()) {
851 break;
852 }
853 // Could optimize this further.... (MUCH)
854 fCache.clear ([pm] (const CacheElt::COMPARE_ITEM& c) { return pm == c.fPM; });
855 }
856 MarkerOwner::EarlyDidUpdateText (updateInfo);
857}
858#endif
859#endif
860
861/*
862 ********************************************************************************
863 ********************************** PartitionMarker *****************************
864 ********************************************************************************
865 */
866void PartitionMarker::DidUpdateText (const UpdateInfo& updateInfo) noexcept
867{
868 inherited::DidUpdateText (updateInfo);
869 GetOwner ().UpdatePartitions (this, updateInfo);
870}
#define AssertNotNull(p)
Definition Assertions.h:333
#define EnsureNotNull(p)
Definition Assertions.h:340
#define qStroika_Foundation_Debug_AssertionsChecked
The qStroika_Foundation_Debug_AssertionsChecked flag determines if assertions are checked and validat...
Definition Assertions.h:48
#define RequireNotNull(p)
Definition Assertions.h:347
#define AssertNotReached()
Definition Assertions.h:355
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...