Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit d5f36ae

Browse files
authored
[web] Separate text fragmenting from layout (#34085)
1 parent 2875385 commit d5f36ae

24 files changed

+2542
-1936
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,8 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/svg.dart
19261926
FILE: ../../../flutter/lib/web_ui/lib/src/engine/test_embedding.dart
19271927
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
19281928
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/font_collection.dart
1929+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/fragmenter.dart
1930+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/layout_fragmenter.dart
19291931
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/layout_service.dart
19301932
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_break_properties.dart
19311933
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_breaker.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ export 'engine/svg.dart';
148148
export 'engine/test_embedding.dart';
149149
export 'engine/text/canvas_paragraph.dart';
150150
export 'engine/text/font_collection.dart';
151+
export 'engine/text/fragmenter.dart';
152+
export 'engine/text/layout_fragmenter.dart';
151153
export 'engine/text/layout_service.dart';
152154
export 'engine/text/line_break_properties.dart';
153155
export 'engine/text/line_breaker.dart';

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,8 @@ extension DomCanvasRenderingContext2DExtension on DomCanvasRenderingContext2D {
634634
external set fillStyle(Object? style);
635635
external String get font;
636636
external set font(String value);
637+
external String get direction;
638+
external set direction(String value);
637639
external set lineWidth(num? value);
638640
external set strokeStyle(Object? value);
639641
external Object? get strokeStyle;

lib/web_ui/lib/src/engine/html/bitmap_canvas.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -913,9 +913,11 @@ class BitmapCanvas extends EngineCanvas {
913913
_cachedLastCssFont = null;
914914
}
915915

916-
void setCssFont(String cssFont) {
916+
void setCssFont(String cssFont, ui.TextDirection textDirection) {
917+
final DomCanvasRenderingContext2D ctx = _canvasPool.context;
918+
ctx.direction = textDirection == ui.TextDirection.ltr ? 'ltr' : 'rtl';
919+
917920
if (cssFont != _cachedLastCssFont) {
918-
final DomCanvasRenderingContext2D ctx = _canvasPool.context;
919921
ctx.font = cssFont;
920922
_cachedLastCssFont = cssFont;
921923
}

lib/web_ui/lib/src/engine/text/canvas_paragraph.dart

Lines changed: 90 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import '../embedder.dart';
99
import '../html/bitmap_canvas.dart';
1010
import '../profiler.dart';
1111
import '../util.dart';
12+
import 'layout_fragmenter.dart';
1213
import 'layout_service.dart';
1314
import 'paint_service.dart';
1415
import 'paragraph.dart';
1516
import 'word_breaker.dart';
1617

1718
const ui.Color _defaultTextColor = ui.Color(0xFFFF0000);
1819

20+
final String _placeholderChar = String.fromCharCode(0xFFFC);
21+
1922
/// A paragraph made up of a flat list of text spans and placeholders.
2023
///
2124
/// [CanvasParagraph] doesn't use a DOM element to represent the structure of
@@ -32,7 +35,7 @@ class CanvasParagraph implements ui.Paragraph {
3235
required this.plainText,
3336
required this.placeholderCount,
3437
required this.canDrawOnCanvas,
35-
});
38+
}) : assert(spans.isNotEmpty);
3639

3740
/// The flat list of spans that make up this paragraph.
3841
final List<ParagraphSpan> spans;
@@ -168,38 +171,28 @@ class CanvasParagraph implements ui.Paragraph {
168171

169172
// 2. Append all spans to the paragraph.
170173

171-
DomHTMLElement? lastSpanElement;
172174
for (int i = 0; i < lines.length; i++) {
173175
final ParagraphLine line = lines[i];
174-
final List<RangeBox> boxes = line.boxes;
175-
final StringBuffer buffer = StringBuffer();
176-
177-
int j = 0;
178-
while (j < boxes.length) {
179-
final RangeBox box = boxes[j++];
180-
181-
if (box is SpanBox) {
182-
lastSpanElement = domDocument.createElement('flt-span') as
183-
DomHTMLElement;
184-
applyTextStyleToElement(
185-
element: lastSpanElement,
186-
style: box.span.style,
187-
isSpan: true,
188-
);
189-
_positionSpanElement(lastSpanElement, line, box);
190-
lastSpanElement.appendText(box.toText());
191-
rootElement.append(lastSpanElement);
192-
buffer.write(box.toText());
193-
} else if (box is PlaceholderBox) {
194-
lastSpanElement = null;
195-
} else {
196-
throw UnimplementedError('Unknown box type: ${box.runtimeType}');
176+
for (final LayoutFragment fragment in line.fragments) {
177+
if (fragment.isPlaceholder) {
178+
continue;
179+
}
180+
181+
final String text = fragment.getText(this);
182+
if (text.isEmpty) {
183+
continue;
197184
}
198-
}
199185

200-
final String? ellipsis = line.ellipsis;
201-
if (ellipsis != null) {
202-
(lastSpanElement ?? rootElement).appendText(ellipsis);
186+
final DomHTMLElement spanElement = domDocument.createElement('flt-span') as DomHTMLElement;
187+
applyTextStyleToElement(
188+
element: spanElement,
189+
style: fragment.style,
190+
isSpan: true,
191+
);
192+
_positionSpanElement(spanElement, line, fragment);
193+
194+
spanElement.appendText(text);
195+
rootElement.append(spanElement);
203196
}
204197
}
205198

@@ -283,8 +276,8 @@ class CanvasParagraph implements ui.Paragraph {
283276
}
284277
}
285278

286-
void _positionSpanElement(DomElement element, ParagraphLine line, RangeBox box) {
287-
final ui.Rect boxRect = box.toTextBox(line, forPainting: true).toRect();
279+
void _positionSpanElement(DomElement element, ParagraphLine line, LayoutFragment fragment) {
280+
final ui.Rect boxRect = fragment.toPaintingTextBox().toRect();
288281
element.style
289282
..position = 'absolute'
290283
..top = '${boxRect.top}px'
@@ -304,6 +297,9 @@ abstract class ParagraphSpan {
304297

305298
/// The index of the end of the range of text represented by this span.
306299
int get end;
300+
301+
/// The resolved style of the span.
302+
EngineTextStyle get style;
307303
}
308304

309305
/// Represent a span of text in the paragraph.
@@ -323,7 +319,7 @@ class FlatTextSpan implements ParagraphSpan {
323319
required this.end,
324320
});
325321

326-
/// The resolved style of the span.
322+
@override
327323
final EngineTextStyle style;
328324

329325
@override
@@ -341,14 +337,24 @@ class FlatTextSpan implements ParagraphSpan {
341337

342338
class PlaceholderSpan extends ParagraphPlaceholder implements ParagraphSpan {
343339
PlaceholderSpan(
344-
int index,
345-
super.width,
346-
super.height,
347-
super.alignment, {
348-
required super.baselineOffset,
349-
required super.baseline,
350-
}) : start = index,
351-
end = index;
340+
this.style,
341+
this.start,
342+
this.end,
343+
double width,
344+
double height,
345+
ui.PlaceholderAlignment alignment, {
346+
required double baselineOffset,
347+
required ui.TextBaseline baseline,
348+
}) : super(
349+
width,
350+
height,
351+
alignment,
352+
baselineOffset: baselineOffset,
353+
baseline: baseline,
354+
);
355+
356+
@override
357+
final EngineTextStyle style;
352358

353359
@override
354360
final int start;
@@ -624,10 +630,19 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
624630
alignment == ui.PlaceholderAlignment.belowBaseline ||
625631
alignment == ui.PlaceholderAlignment.baseline) || baseline != null);
626632

633+
final int start = _plainTextBuffer.length;
634+
_plainTextBuffer.write(_placeholderChar);
635+
final int end = _plainTextBuffer.length;
636+
637+
final EngineTextStyle style = _currentStyleNode.resolveStyle();
638+
_updateCanDrawOnCanvas(style);
639+
627640
_placeholderCount++;
628641
_placeholderScales.add(scale);
629642
_spans.add(PlaceholderSpan(
630-
_plainTextBuffer.length,
643+
style,
644+
start,
645+
end,
631646
width * scale,
632647
height * scale,
633648
alignment,
@@ -652,37 +667,54 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
652667

653668
@override
654669
void addText(String text) {
655-
final EngineTextStyle style = _currentStyleNode.resolveStyle();
656670
final int start = _plainTextBuffer.length;
657671
_plainTextBuffer.write(text);
658672
final int end = _plainTextBuffer.length;
659673

660-
if (_canDrawOnCanvas) {
661-
final ui.TextDecoration? decoration = style.decoration;
662-
if (decoration != null && decoration != ui.TextDecoration.none) {
663-
_canDrawOnCanvas = false;
664-
}
674+
final EngineTextStyle style = _currentStyleNode.resolveStyle();
675+
_updateCanDrawOnCanvas(style);
676+
677+
_spans.add(FlatTextSpan(style: style, start: start, end: end));
678+
}
679+
680+
void _updateCanDrawOnCanvas(EngineTextStyle style) {
681+
if (!_canDrawOnCanvas) {
682+
return;
665683
}
666684

667-
if (_canDrawOnCanvas) {
668-
final List<ui.FontFeature>? fontFeatures = style.fontFeatures;
669-
if (fontFeatures != null && fontFeatures.isNotEmpty) {
670-
_canDrawOnCanvas = false;
671-
}
685+
final ui.TextDecoration? decoration = style.decoration;
686+
if (decoration != null && decoration != ui.TextDecoration.none) {
687+
_canDrawOnCanvas = false;
688+
return;
672689
}
673690

674-
if (_canDrawOnCanvas) {
675-
final List<ui.FontVariation>? fontVariations = style.fontVariations;
676-
if (fontVariations != null && fontVariations.isNotEmpty) {
677-
_canDrawOnCanvas = false;
678-
}
691+
final List<ui.FontFeature>? fontFeatures = style.fontFeatures;
692+
if (fontFeatures != null && fontFeatures.isNotEmpty) {
693+
_canDrawOnCanvas = false;
694+
return;
679695
}
680696

681-
_spans.add(FlatTextSpan(style: style, start: start, end: end));
697+
final List<ui.FontVariation>? fontVariations = style.fontVariations;
698+
if (fontVariations != null && fontVariations.isNotEmpty) {
699+
_canDrawOnCanvas = false;
700+
return;
701+
}
682702
}
683703

684704
@override
685705
CanvasParagraph build() {
706+
if (_spans.isEmpty) {
707+
// In case `addText` and `addPlaceholder` were never called.
708+
//
709+
// We want the paragraph to always have a non-empty list of spans to match
710+
// the expectations of the [LayoutFragmenter].
711+
_spans.add(FlatTextSpan(
712+
style: _rootStyleNode.resolveStyle(),
713+
start: 0,
714+
end: 0,
715+
));
716+
}
717+
686718
return CanvasParagraph(
687719
_spans,
688720
paragraphStyle: _paragraphStyle,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// Splits [text] into a list of [TextFragment]s.
6+
///
7+
/// Various subclasses can perform the fragmenting based on their own criteria.
8+
///
9+
/// See:
10+
///
11+
/// - [LineBreakFragmenter]: Fragments text based on line break opportunities.
12+
/// - [BidiFragmenter]: Fragments text based on directionality.
13+
abstract class TextFragmenter {
14+
const TextFragmenter(this.text);
15+
16+
/// The text to be fragmented.
17+
final String text;
18+
19+
/// Performs the fragmenting of [text] and returns a list of [TextFragment]s.
20+
List<TextFragment> fragment();
21+
}
22+
23+
/// Represents a fragment produced by [TextFragmenter].
24+
abstract class TextFragment {
25+
const TextFragment(this.start, this.end);
26+
27+
final int start;
28+
final int end;
29+
30+
/// Whether this fragment's range overlaps with the range from [start] to [end].
31+
bool overlapsWith(int start, int end) {
32+
return start < this.end && this.start < end;
33+
}
34+
}

0 commit comments

Comments
 (0)