22// Use of this source code is governed by a BSD-style license that can be
33// found in the LICENSE file.
44
5- import 'dart:js_interop' ;
65import 'dart:math' as math;
76
8- import 'package:ui/src/engine/dom.dart' ;
97import 'package:ui/ui.dart' as ui;
108
119import '../util.dart' ;
@@ -116,7 +114,7 @@ abstract class _CombinedFragment extends TextFragment {
116114
117115 final LineBreakType type;
118116
119- ui.TextDirection ? get textDirection => _textDirection;
117+ ui.TextDirection get textDirection => _textDirection! ;
120118 ui.TextDirection ? _textDirection;
121119
122120 final FragmentFlow fragmentFlow;
@@ -207,7 +205,7 @@ class LayoutFragment extends _CombinedFragment with _FragmentMetrics, _FragmentP
207205 start,
208206 index,
209207 LineBreakType .prohibited,
210- textDirection ,
208+ _textDirection ,
211209 fragmentFlow,
212210 span,
213211 trailingNewlines: trailingNewlines - secondTrailingNewlines,
@@ -217,7 +215,7 @@ class LayoutFragment extends _CombinedFragment with _FragmentMetrics, _FragmentP
217215 index,
218216 end,
219217 type,
220- textDirection ,
218+ _textDirection ,
221219 fragmentFlow,
222220 span,
223221 trailingNewlines: secondTrailingNewlines,
@@ -367,7 +365,7 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
367365 top,
368366 line.left + right,
369367 bottom,
370- textDirection! ,
368+ textDirection,
371369 );
372370
373371 /// Whether or not the trailing spaces of this fragment are part of trailing
@@ -384,20 +382,20 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
384382 ui.TextBox toPaintingTextBox () {
385383 if (_isPartOfTrailingSpacesInLine) {
386384 // For painting, we exclude the width of trailing spaces from the box.
387- return textDirection! == ui.TextDirection .ltr
385+ return textDirection == ui.TextDirection .ltr
388386 ? ui.TextBox .fromLTRBD (
389387 line.left + left,
390388 top,
391389 line.left + right - widthOfTrailingSpaces,
392390 bottom,
393- textDirection! ,
391+ textDirection,
394392 )
395393 : ui.TextBox .fromLTRBD (
396394 line.left + left + widthOfTrailingSpaces,
397395 top,
398396 line.left + right,
399397 bottom,
400- textDirection! ,
398+ textDirection,
401399 );
402400 }
403401 return _textBoxIncludingTrailingSpaces;
@@ -449,7 +447,7 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
449447 }
450448
451449 final double left, right;
452- if (textDirection! == ui.TextDirection .ltr) {
450+ if (textDirection == ui.TextDirection .ltr) {
453451 // Example: let's say the text is "Loremipsum" and we want to get the box
454452 // for "rem". In this case, `before` is the width of "Lo", and `after`
455453 // is the width of "ipsum".
@@ -490,7 +488,7 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
490488 top,
491489 line.left + right,
492490 bottom,
493- textDirection! ,
491+ textDirection,
494492 );
495493 }
496494
@@ -594,57 +592,49 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
594592 return x;
595593 }
596594
597- // This is the fallback graphme breaker that is only used if Intl.Segmenter()
598- // is not supported so _fromDomSegmenter can't be called. This implementation
599- // breaks the text into UTF-16 codepoints instead of graphme clusters.
600- List <int > _fallbackGraphemeStartIterable (String fragmentText) {
601- final List <int > graphemeStarts = < int > [];
602- bool precededByHighSurrogate = false ;
603- for (int i = 0 ; i < fragmentText.length; i++ ) {
604- final int maskedCodeUnit = fragmentText.codeUnitAt (i) & 0xFC00 ;
605- // Only skip `i` if it points to a low surrogate in a valid surrogate pair.
606- if (maskedCodeUnit != 0xDC00 || ! precededByHighSurrogate) {
607- graphemeStarts.add (start + i);
608- }
609- precededByHighSurrogate = maskedCodeUnit == 0xD800 ;
610- }
611- return graphemeStarts;
612- }
613-
614- // This will be called at most once to lazily populate _graphemeStarts.
615- List <int > _fromDomSegmenter (String fragmentText) {
616- final DomSegmenter domSegmenter = DomSegmenter (
617- < JSAny ? > [].toJS,
618- < String , String > {'granularity' : 'grapheme' }.toJSAnyDeep,
619- );
620- final List <int > graphemeStarts = < int > [];
621- final Iterator <DomSegment > segments = domSegmenter.segment (fragmentText).iterator ();
622- while (segments.moveNext ()) {
623- graphemeStarts.add (segments.current.index + start);
595+ // [start, end).map((index) => line.graphemeStarts[index]) gives an ascending
596+ // list of UTF16 offsets of graphemes that start in this fragment.
597+ //
598+ // Returns null if this fragment contains no grapheme starts.
599+ late final (int , int )? graphemeStartIndexRange = _getBreaksRange ();
600+ (int , int )? _getBreaksRange () {
601+ if (end == start) {
602+ return null ;
624603 }
625- assert (graphemeStarts.isEmpty || graphemeStarts.first == start);
626- return graphemeStarts;
604+ final List <int > lineGraphemeBreaks = line.graphemeStarts;
605+ assert (end > start);
606+ assert (line.graphemeStarts.isNotEmpty);
607+ final int startIndex = line.graphemeStartIndexBefore (start, 0 , lineGraphemeBreaks.length);
608+ final int endIndex = end == start + 1
609+ ? startIndex + 1
610+ : line.graphemeStartIndexBefore (end - 1 , startIndex, lineGraphemeBreaks.length) + 1 ;
611+ final int firstGraphemeStart = lineGraphemeBreaks[startIndex];
612+ return firstGraphemeStart > start
613+ ? (endIndex == startIndex + 1 ? null : (startIndex + 1 , endIndex))
614+ : (startIndex, endIndex);
627615 }
628616
629- // This List contains an ascending sequence of UTF16 offsets that points to
630- // grapheme starts within the fragment. Each UTF16 offset is relative to the
631- // start of the paragraph, instead of the start of the fragment.
632- late final List <int > _graphemeStarts = _breakFragmentText (
633- _spanometer.paragraph.plainText.substring (start, end),
634- );
635- List <int > _breakFragmentText (String text) {
636- final List <int > graphemeStarts = domIntl.Segmenter == null ? _fallbackGraphemeStartIterable (text) : _fromDomSegmenter (text);
637- // Add the end index of the fragment to the list if the text is not empty.
638- if (graphemeStarts.isNotEmpty) {
639- graphemeStarts.add (end);
640- }
641- return graphemeStarts;
617+ /// Whether the first codepoints of this fragment is not a valid grapheme start,
618+ /// and belongs in the the previous fragment.
619+ ///
620+ /// This is the result of a known bug: in rare circumstances, a grapheme is
621+ /// split into different fragments. To workaround this we ignore the trailing
622+ /// part of the grapheme during hit-testing, by adjusting the leading offset of
623+ /// a fragment to the leading edge of the first grapheme start in that fragment.
624+ //
625+ // TODO(LongCatIsLooong): Grapheme clusters should not be separately even
626+ // when they are in different runs. Also document the recommendation to use
627+ // U+25CC or U+00A0 for showing nonspacing marks in isolation.
628+ bool get hasLeadingBrokenGrapheme {
629+ final int ? graphemeStartIndexRangeStart = graphemeStartIndexRange? .$1;
630+ return graphemeStartIndexRangeStart == null || line.graphemeStarts[graphemeStartIndexRangeStart] != start;
642631 }
643632
644- // Returns the GlyphInfo within the range [_graphemeStarts [startIndex], _graphemeStarts [endIndex]),
645- // that's visually closeset to the given horizontal offset `x` (in the paragraph's coordinates).
633+ /// Returns the GlyphInfo within the range [line.graphemeStarts [startIndex] , line.graphemeStarts [endIndex] ),
634+ /// that's visually closeset to the given horizontal offset `x` (in the paragraph's coordinates).
646635 ui.GlyphInfo _getClosestCharacterInRange (double x, int startIndex, int endIndex) {
647- final ui.TextRange fullRange = ui.TextRange (start: _graphemeStarts[startIndex], end: _graphemeStarts[endIndex]);
636+ final List <int > graphemeStartIndices = line.graphemeStarts;
637+ final ui.TextRange fullRange = ui.TextRange (start: graphemeStartIndices[startIndex], end: graphemeStartIndices[endIndex]);
648638 final ui.TextBox fullBox = toTextBox (start: fullRange.start, end: fullRange.end);
649639 if (startIndex + 1 == endIndex) {
650640 return ui.GlyphInfo (fullBox.toRect (), fullRange, fullBox.direction);
@@ -678,53 +668,29 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
678668 // there can only be one writing direction in the fragment.
679669 final ui.TextRange range = switch ((fullBox.direction, x <= left)) {
680670 (ui.TextDirection .ltr, true ) || (ui.TextDirection .rtl, false ) => ui.TextRange (
681- start: _graphemeStarts [startIndex],
682- end: _graphemeStarts [startIndex + 1 ],
671+ start: graphemeStartIndices [startIndex],
672+ end: graphemeStartIndices [startIndex + 1 ],
683673 ),
684674 (ui.TextDirection .ltr, false ) || (ui.TextDirection .rtl, true ) => ui.TextRange (
685- start: _graphemeStarts [endIndex - 1 ],
686- end: _graphemeStarts [endIndex],
675+ start: graphemeStartIndices [endIndex - 1 ],
676+ end: graphemeStartIndices [endIndex],
687677 ),
688678 };
689679 assert (! range.isCollapsed);
690680 final ui.TextBox box = toTextBox (start: range.start, end: range.end);
691681 return ui.GlyphInfo (box.toRect (), range, box.direction);
692682 }
693683
694- /// Returns the UTF-16 range of the character that encloses the code unit at
695- /// the given offset.
696- ui.TextRange ? getCharacterRangeAt (int codeUnitOffset) {
697- if (end == start) {
698- return null ;
699- }
700- final List <int > graphemeStarts = _graphemeStarts;
701- assert (graphemeStarts.length >= 2 );
702-
703- int previousStart = graphemeStarts.first;
704- if (previousStart > codeUnitOffset) {
705- return null ;
706- }
707- for (int i = 1 ; i < graphemeStarts.length; i += 1 ) {
708- final int newStart = graphemeStarts[i];
709- if (newStart > codeUnitOffset) {
710- return ui.TextRange (start: previousStart, end: newStart);
711- }
712- previousStart = newStart;
713- }
714- return null ;
715- }
716-
717684 /// Returns the GlyphInfo of the character in the fragment that is closest to
718685 /// the given offset x.
719- ui.GlyphInfo ? getClosestCharacterBox (double x) {
720- if (end == start) {
721- return null ;
722- }
723- if (end - start == 1 || _graphemeStarts.length <= 2 ) {
724- final ui.TextBox box = toTextBox (start: start, end: end);
725- return ui.GlyphInfo (box.toRect (), ui.TextRange (start: start, end: end), box.direction);
726- }
727- return _getClosestCharacterInRange (x, 0 , _graphemeStarts.length - 1 );
686+ ui.GlyphInfo getClosestCharacterBox (double x) {
687+ assert (end > start);
688+ assert (graphemeStartIndexRange != null );
689+ // The force ! is safe here because this method is only called by
690+ // LayoutService.getClosestGlyphInfo which checks this fragment has at least
691+ // one grapheme start before calling this method.
692+ final (int rangeStart, int rangeEnd) = graphemeStartIndexRange! ;
693+ return _getClosestCharacterInRange (x, rangeStart, rangeEnd);
728694 }
729695}
730696
0 commit comments