Skip to content

Commit cce24b8

Browse files
Rusinomdebbar
andauthored
[WebParagraph] Support for more styles, placeholders, decorations, etc (#172853)
This is the second version of WebParagraph. It includes pretty much all SkParagraph functionality except struts and justifications (eventually they will be implemented, too). Part of flutter/flutter#172561 --------- Co-authored-by: Mouad Debbar <mdebbar@google.com>
1 parent 75caa52 commit cce24b8

28 files changed

+5609
-1049
lines changed

engine/src/flutter/lib/web_ui/lib/src/engine.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,13 @@ export 'engine/view_embedder/flutter_view_manager.dart';
157157
export 'engine/view_embedder/global_html_attributes.dart';
158158
export 'engine/view_embedder/hot_restart_cache_handler.dart';
159159
export 'engine/view_embedder/style_manager.dart';
160+
export 'engine/web_paragraph/bidi.dart';
160161
export 'engine/web_paragraph/code_unit_flags.dart';
161162
export 'engine/web_paragraph/debug.dart';
162163
export 'engine/web_paragraph/font_collection.dart';
163164
export 'engine/web_paragraph/layout.dart';
164165
export 'engine/web_paragraph/paint.dart';
166+
export 'engine/web_paragraph/painter.dart';
165167
export 'engine/web_paragraph/paragraph.dart';
166168
export 'engine/web_paragraph/wrapper.dart';
167169
export 'engine/window.dart';

engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,7 @@ extension type BidiIndex(JSObject _) implements JSObject {
17961796

17971797
extension type BidiNamespace(JSObject _) implements JSObject {
17981798
@JS('getBidiRegions')
1799+
// TODO(jlavrova): Use a JSInt32Array return type instead of `List<BidiIndex>`
17991800
external JSArray<JSAny?> _getBidiRegions(String text, SkTextDirection dir);
18001801
List<BidiRegion> getBidiRegions(String text, ui.TextDirection dir) =>
18011802
_getBidiRegions(text, toSkTextDirection(dir)).toDart.cast<BidiRegion>();

engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,29 @@ class CanvasKitRenderer extends Renderer {
312312
List<ui.FontFeature>? fontFeatures,
313313
List<ui.FontVariation>? fontVariations,
314314
}) => isExperimentalWebParagraph
315-
? WebTextStyle(fontFamily: fontFamily, fontSize: fontSize, color: color)
315+
? WebTextStyle(
316+
color: color,
317+
decoration: decoration,
318+
decorationColor: decorationColor,
319+
decorationStyle: decorationStyle,
320+
decorationThickness: decorationThickness,
321+
fontWeight: fontWeight,
322+
fontStyle: fontStyle,
323+
textBaseline: textBaseline,
324+
fontFamily: fontFamily,
325+
fontFamilyFallback: fontFamilyFallback,
326+
fontSize: fontSize,
327+
letterSpacing: letterSpacing,
328+
wordSpacing: wordSpacing,
329+
height: height,
330+
leadingDistribution: leadingDistribution,
331+
locale: locale,
332+
background: background as CkPaint?,
333+
foreground: foreground as CkPaint?,
334+
shadows: shadows,
335+
fontFeatures: fontFeatures,
336+
fontVariations: fontVariations,
337+
)
316338
: CkTextStyle(
317339
color: color,
318340
decoration: decoration,
@@ -353,10 +375,18 @@ class CanvasKitRenderer extends Renderer {
353375
ui.Locale? locale,
354376
}) => isExperimentalWebParagraph
355377
? WebParagraphStyle(
356-
textDirection: textDirection,
357378
textAlign: textAlign,
379+
textDirection: textDirection,
380+
maxLines: maxLines,
358381
fontFamily: fontFamily,
359382
fontSize: fontSize,
383+
height: height,
384+
textHeightBehavior: textHeightBehavior,
385+
fontWeight: fontWeight,
386+
fontStyle: fontStyle,
387+
strutStyle: strutStyle as WebStrutStyle?,
388+
ellipsis: ellipsis,
389+
locale: locale,
360390
)
361391
: CkParagraphStyle(
362392
textAlign: textAlign,
@@ -385,7 +415,17 @@ class CanvasKitRenderer extends Renderer {
385415
ui.FontStyle? fontStyle,
386416
bool? forceStrutHeight,
387417
}) => isExperimentalWebParagraph
388-
? WebStrutStyle()
418+
? WebStrutStyle(
419+
fontFamily: fontFamily,
420+
fontFamilyFallback: fontFamilyFallback,
421+
fontSize: fontSize,
422+
height: height,
423+
leadingDistribution: leadingDistribution,
424+
leading: leading,
425+
fontWeight: fontWeight,
426+
fontStyle: fontStyle,
427+
forceStrutHeight: forceStrutHeight,
428+
)
389429
: CkStrutStyle(
390430
fontFamily: fontFamily,
391431
fontFamilyFallback: fontFamilyFallback,

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,19 @@ extension type DomCanvasRenderingContext2D._(JSObject _) implements JSObject {
896896
set fillStyle(Object? style) => _fillStyle = style?.toJSAnyShallow;
897897

898898
external String font;
899+
external String fontWeight;
899900
external String direction;
901+
external String letterSpacing;
902+
external String wordSpacing;
903+
external String textRendering;
904+
external String fontKerning;
905+
external String fontVariantCaps;
900906
external set lineWidth(num? value);
901907

908+
@JS('setLineDash')
909+
external void _setLineDash(JSFloat32Array? value);
910+
void setLineDash(Float32List? value) => _setLineDash(value?.toJS);
911+
902912
@JS('strokeStyle')
903913
external set _strokeStyle(JSAny? value);
904914
set strokeStyle(Object? value) => _strokeStyle = value?.toJSAnyShallow;
@@ -993,6 +1003,7 @@ extension type DomCanvasRenderingContext2D._(JSObject _) implements JSObject {
9931003
external void rect(num x, num y, num width, num height);
9941004
external void resetTransform();
9951005
external void restore();
1006+
external void reset();
9961007
external void setTransform(num a, num b, num c, num d, num e, num f);
9971008
external void transform(num a, num b, num c, num d, num e, num f);
9981009

@@ -1039,7 +1050,16 @@ extension type DomCanvasRenderingContext2D._(JSObject _) implements JSObject {
10391050
external void strokeText(String text, num x, num y);
10401051
external set globalAlpha(num? value);
10411052

1042-
external void fillTextCluster(DomTextCluster textCluster, double x, double y);
1053+
@JS('fillTextCluster')
1054+
external void _fillTextCluster(JSAny? textCluster, double x, double y, [JSAny? options]);
1055+
1056+
void fillTextCluster(DomTextCluster textCluster, double x, double y, [Object? options]) {
1057+
if (options == null) {
1058+
return _fillTextCluster(textCluster.toJSAnyDeep, x, y);
1059+
} else {
1060+
return _fillTextCluster(textCluster.toJSAnyDeep, x, y, options.toJSAnyDeep);
1061+
}
1062+
}
10431063
}
10441064

10451065
@JS('ImageBitmapRenderingContext')

engine/src/flutter/lib/web_ui/lib/src/engine/text/paragraph.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ class EngineLineMetrics implements ui.LineMetrics {
9797
}
9898
}
9999

100+
extension FontStyleExtension on ui.FontStyle {
101+
/// Converts a [ui.FontStyle] value to its CSS equivalent.
102+
String toCssString() {
103+
return this == ui.FontStyle.normal ? 'normal' : 'italic';
104+
}
105+
}
106+
107+
extension FontWeightExtension on ui.FontWeight {
108+
/// Converts a [ui.FontWeight] value to its CSS equivalent.
109+
String toCssString() {
110+
return fontWeightIndexToCss(fontWeightIndex: index);
111+
}
112+
}
113+
100114
String fontWeightIndexToCss({int fontWeightIndex = 3}) {
101115
switch (fontWeightIndex) {
102116
case 0:
@@ -158,3 +172,35 @@ String textAlignToCssValue(ui.TextAlign? align, ui.TextDirection textDirection)
158172
return '';
159173
}
160174
}
175+
176+
String fontFeatureListToCss(List<ui.FontFeature> fontFeatures) {
177+
assert(fontFeatures.isNotEmpty);
178+
179+
// For more details, see:
180+
// * https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings
181+
final StringBuffer sb = StringBuffer();
182+
final int len = fontFeatures.length;
183+
for (int i = 0; i < len; i++) {
184+
if (i != 0) {
185+
sb.write(',');
186+
}
187+
final ui.FontFeature fontFeature = fontFeatures[i];
188+
sb.write('"${fontFeature.feature}" ${fontFeature.value}');
189+
}
190+
return sb.toString();
191+
}
192+
193+
String fontVariationListToCss(List<ui.FontVariation> fontVariations) {
194+
assert(fontVariations.isNotEmpty);
195+
196+
final StringBuffer sb = StringBuffer();
197+
final int len = fontVariations.length;
198+
for (int i = 0; i < len; i++) {
199+
if (i != 0) {
200+
sb.write(',');
201+
}
202+
final ui.FontVariation fontVariation = fontVariations[i];
203+
sb.write('"${fontVariation.axis}" ${fontVariation.value}');
204+
}
205+
return sb.toString();
206+
}

engine/src/flutter/lib/web_ui/lib/src/engine/util.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,30 @@ bool unorderedListEqual<T>(List<T>? a, List<T>? b) {
523523
return wordCounts.isEmpty;
524524
}
525525

526+
bool paintEquals(ui.Paint? a, ui.Paint? b) {
527+
if (identical(a, b)) {
528+
// They are both the same instance or both null.
529+
return true;
530+
}
531+
if (a == null || b == null) {
532+
return false;
533+
}
534+
return a.blendMode == b.blendMode &&
535+
a.color == b.color &&
536+
a.colorFilter == b.colorFilter &&
537+
a.filterQuality == b.filterQuality &&
538+
a.imageFilter == b.imageFilter &&
539+
a.invertColors == b.invertColors &&
540+
a.isAntiAlias == b.isAntiAlias &&
541+
a.maskFilter == b.maskFilter &&
542+
a.shader == b.shader &&
543+
a.strokeCap == b.strokeCap &&
544+
a.strokeJoin == b.strokeJoin &&
545+
a.strokeMiterLimit == b.strokeMiterLimit &&
546+
a.strokeWidth == b.strokeWidth &&
547+
a.style == b.style;
548+
}
549+
526550
/// Extensions to [Map] that make it easier to treat it as a JSON object. The
527551
/// keys are `dynamic` because when JSON is deserialized from method channels
528552
/// it arrives as `Map<dynamic, dynamic>`.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
import 'dart:typed_data';
6+
7+
import '../canvaskit/canvaskit_api.dart';
8+
import 'paragraph.dart';
9+
10+
class BidiRun {
11+
BidiRun(this.clusterRange, this.bidiLevel);
12+
13+
final int bidiLevel;
14+
final ClusterRange clusterRange;
15+
16+
bool get isLtr => bidiLevel.isEven;
17+
bool get isRtl => !isLtr;
18+
}
19+
20+
extension VisualOrder on List<BidiRun> {
21+
/// Returns a sublist of [BidiRun] that's ordered visually.
22+
///
23+
/// [start] inclusive, [end] exclusive.
24+
Iterable<BidiRun> inVisualOrder(int start, int end) {
25+
final levels = Uint8List(end - start);
26+
for (int i = 0; i < levels.length; i++) {
27+
levels[i] = this[start + i].bidiLevel;
28+
}
29+
// TODO(jlavrova): We need to think about how to support this for Skwasm without calling Canvaskit.
30+
final visuals = canvasKit.Bidi.reorderVisual(levels);
31+
return visuals.map((BidiIndex visual) => this[start + visual.index]);
32+
}
33+
}

engine/src/flutter/lib/web_ui/lib/src/engine/web_paragraph/code_unit_flags.dart

Lines changed: 50 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,74 @@
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:typed_data';
6+
57
import '../canvaskit/canvaskit_api.dart';
68
import '../text_fragmenter.dart';
7-
import 'paragraph.dart';
89

9-
class CodeUnitFlags {
10-
CodeUnitFlags(this._value);
10+
class AllCodeUnitFlags {
11+
AllCodeUnitFlags(this._text) : _allFlags = Uint8List(_text.length + 1) {
12+
_extract();
13+
}
14+
15+
final String _text;
16+
final Uint8List _allFlags;
17+
18+
int get length => _allFlags.length;
19+
20+
bool hasFlag(int index, CodeUnitFlag flag) {
21+
assert(index >= 0);
22+
assert(index < _allFlags.length);
1123

12-
static List<CodeUnitFlags> extractForParagraph(WebParagraph paragraph) {
13-
final List<CodeUnitInfo> ckFlags = canvasKit.CodeUnits.compute(paragraph.text);
14-
assert(ckFlags.length == (paragraph.text.length + 1));
24+
return (_allFlags[index] & flag._bitmask) != 0;
25+
}
26+
27+
void _extract() {
28+
// TODO(jlavrova): 1. This call to CanvasKit is not going to work with Skwasm.
29+
// 2. We are only using `whitespace` flags from CanvasKit. Can we hardcode them
30+
// here to avoid calling CanvasKit?
31+
// 3. Do we need other flags like `control` and `space`?
32+
final List<CodeUnitInfo> ckFlags = canvasKit.CodeUnits.compute(_text);
33+
assert(ckFlags.length == _allFlags.length);
1534

16-
final codeUnitFlags = ckFlags.map((info) => CodeUnitFlags(info.flags)).toList();
35+
for (int i = 0; i < _allFlags.length; i++) {
36+
_allFlags[i] = ckFlags[i].flags;
37+
}
1738

39+
// TODO(mdebbar): OPTIMIZATION: can we make `segmentText` update `codeUnitFlags` in-place?
1840
// Get text segmentation resuls using browser APIs.
19-
final SegmentationResult result = segmentText(paragraph.text);
41+
final SegmentationResult result = segmentText(_text);
2042

2143
// Fill out grapheme flags
22-
for (final grapheme in result.graphemes) {
23-
codeUnitFlags[grapheme].graphemeStart = true;
44+
for (final index in result.graphemes) {
45+
_allFlags[index] |= CodeUnitFlag.grapheme._bitmask;
2446
}
2547
// Fill out word flags
26-
for (final word in result.words) {
27-
codeUnitFlags[word].wordBreak = true;
48+
for (final index in result.words) {
49+
_allFlags[index] |= CodeUnitFlag.wordBreak._bitmask;
2850
}
2951
// Fill out line break flags
30-
for (int index = 0; index < result.breaks.length; index += 2) {
31-
final int lineBreak = result.breaks[index];
32-
if (result.breaks[index + 1] == kSoftLineBreak) {
33-
codeUnitFlags[lineBreak].softLineBreak = true;
52+
for (int i = 0; i < result.breaks.length; i += 2) {
53+
final int index = result.breaks[i];
54+
final int type = result.breaks[i + 1];
55+
56+
if (type == kSoftLineBreak) {
57+
_allFlags[index] |= CodeUnitFlag.softLineBreak._bitmask;
3458
} else {
35-
codeUnitFlags[lineBreak].hardLineBreak = true;
59+
_allFlags[index] |= CodeUnitFlag.hardLineBreak._bitmask;
3660
}
3761
}
38-
return codeUnitFlags;
39-
}
40-
41-
bool get isWhitespace => hasFlag(kWhitespaceFlag);
42-
set whitespace(bool enable) => _setFlag(kWhitespaceFlag, enable);
43-
44-
bool get isGraphemeStart => hasFlag(kGraphemeFlag);
45-
set graphemeStart(bool enable) => _setFlag(kGraphemeFlag, enable);
46-
47-
bool get isSoftLineBreak => hasFlag(kSoftLineBreakFlag);
48-
set softLineBreak(bool enable) => _setFlag(kSoftLineBreakFlag, enable);
49-
50-
bool get isHardLineBreak => hasFlag(kHardLineBreakFlag);
51-
set hardLineBreak(bool enable) => _setFlag(kHardLineBreakFlag, enable);
52-
53-
bool get isWordBreak => hasFlag(kWordBreakFlag);
54-
set wordBreak(bool enable) => _setFlag(kWordBreakFlag, enable);
55-
56-
bool hasFlag(int flag) {
57-
return (_value & flag) != 0;
58-
}
59-
60-
void _setFlag(int flag, bool enable) {
61-
_value = enable ? (_value | flag) : (_value & ~flag);
6262
}
63+
}
6364

64-
int _value;
65+
enum CodeUnitFlag {
66+
whitespace(0x01), // 1 << 0
67+
grapheme(0x02), // 1 << 1
68+
softLineBreak(0x04), // 1 << 2
69+
hardLineBreak(0x08), // 1 << 3
70+
wordBreak(0x10); // 1 << 4
6571

66-
@override
67-
String toString() {
68-
return [
69-
if (isWhitespace) 'whitespace',
70-
if (isGraphemeStart) 'grapheme',
71-
if (isSoftLineBreak) 'softBreak',
72-
if (isHardLineBreak) 'hardBreak',
73-
if (isWordBreak) 'word',
74-
].join(' ');
75-
}
72+
const CodeUnitFlag(this._bitmask);
7673

77-
static const int kWhitespaceFlag = 1 << 0;
78-
static const int kGraphemeFlag = 1 << 1;
79-
static const int kSoftLineBreakFlag = 1 << 2;
80-
static const int kHardLineBreakFlag = 1 << 3;
81-
static const int kWordBreakFlag = 1 << 4;
74+
final int _bitmask;
8275
}

0 commit comments

Comments
 (0)