From b785de0854838cd58c5252e2cdba81f8a7117b65 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 12 Aug 2021 11:41:41 -0700 Subject: [PATCH 1/3] [web] Clean up legacy Paragraph implementation --- lib/web_ui/lib/src/engine/dom_renderer.dart | 6 - .../lib/src/engine/text/canvas_paragraph.dart | 4 +- .../lib/src/engine/text/font_collection.dart | 2 - .../lib/src/engine/text/measurement.dart | 946 ------------- lib/web_ui/lib/src/engine/text/paragraph.dart | 1027 -------------- lib/web_ui/lib/src/engine/text/ruler.dart | 722 ---------- .../lib/src/engine/web_experiments.dart | 46 +- lib/web_ui/lib/src/ui/text.dart | 6 +- lib/web_ui/test/paragraph_test.dart | 1001 -------------- .../test/text/canvas_paragraph_test.dart | 59 +- lib/web_ui/test/text/font_loading_test.dart | 7 - lib/web_ui/test/text/measurement_test.dart | 1181 ----------------- 12 files changed, 58 insertions(+), 4949 deletions(-) delete mode 100644 lib/web_ui/test/paragraph_test.dart delete mode 100644 lib/web_ui/test/text/measurement_test.dart diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 694d4f89f8216..cc83ed4920853 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -16,7 +16,6 @@ import 'keyboard_binding.dart'; import 'platform_dispatcher.dart'; import 'pointer_binding.dart'; import 'semantics.dart'; -import 'text/measurement.dart'; import 'text_editing/text_editing.dart'; import 'util.dart'; import 'window.dart'; @@ -29,11 +28,6 @@ class DomRenderer { reset(); - TextMeasurementService.initialize( - rulerCacheCapacity: 10, - root: _glassPaneShadow!.node, - ); - assert(() { _setupHotRestart(); return true; diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart index 75286dc998c2c..48125a834f192 100644 --- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart @@ -15,6 +15,8 @@ import 'paint_service.dart'; import 'paragraph.dart'; import 'word_breaker.dart'; +const ui.Color _defaultTextColor = ui.Color(0xFFFF0000); + /// A paragraph made up of a flat list of text spans and placeholders. /// /// As opposed to [DomParagraph], a [CanvasParagraph] doesn't use a DOM element @@ -574,7 +576,7 @@ class RootStyleNode extends StyleNode { final EngineParagraphStyle paragraphStyle; @override - final ui.Color _color = defaultTextColor; + final ui.Color _color = _defaultTextColor; @override ui.TextDecoration? get _decoration => null; diff --git a/lib/web_ui/lib/src/engine/text/font_collection.dart b/lib/web_ui/lib/src/engine/text/font_collection.dart index 4538545ed3b1d..c56b77d5f057a 100644 --- a/lib/web_ui/lib/src/engine/text/font_collection.dart +++ b/lib/web_ui/lib/src/engine/text/font_collection.dart @@ -12,7 +12,6 @@ import '../assets.dart'; import '../browser_detection.dart'; import '../util.dart'; import 'layout_service.dart'; -import 'measurement.dart'; const String ahemFontFamily = 'Ahem'; const String ahemFontUrl = 'packages/ui/assets/ahem.ttf'; @@ -215,7 +214,6 @@ class FontManager { // There might be paragraph measurements for this new font before it is // loaded. They were measured using fallback font, so we should clear the // cache. - TextMeasurementService.clearCache(); Spanometer.clearRulersCache(); }, onError: (dynamic exception) { // Failures here will throw an html.DomException which confusingly diff --git a/lib/web_ui/lib/src/engine/text/measurement.dart b/lib/web_ui/lib/src/engine/text/measurement.dart index 4ba8203592680..4b1053350f649 100644 --- a/lib/web_ui/lib/src/engine/text/measurement.dart +++ b/lib/web_ui/lib/src/engine/text/measurement.dart @@ -2,22 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:html' as html; -import 'dart:math' as math; - -import 'package:meta/meta.dart'; -import 'package:ui/ui.dart' as ui; import '../../engine.dart' show registerHotRestartListener; import '../dom_renderer.dart'; -import '../util.dart'; -import '../web_experiments.dart'; -import '../window.dart'; -import 'line_break_properties.dart'; -import 'line_breaker.dart'; -import 'paragraph.dart'; -import 'ruler.dart'; // TODO(yjbanov): this is a hack we use to compute ideographic baseline; this // number is the ratio ideographic/alphabetic for font Ahem, @@ -27,16 +15,6 @@ import 'ruler.dart'; // anything as of this writing. const double baselineRatioHack = 1.1662499904632568; -/// Signature of a function that takes a character and returns true or false. -typedef CharPredicate = bool Function(int char); - -bool _newlinePredicate(int char) { - final LineCharProperty prop = lineLookup.findForChar(char); - return prop == LineCharProperty.BK || - prop == LineCharProperty.LF || - prop == LineCharProperty.CR; -} - /// Hosts ruler DOM elements in a hidden container under a `root` [html.Node]. /// /// The `root` [html.Node] is optional. Defaults to [domRenderer.glassPaneShadow]. @@ -76,606 +54,6 @@ class RulerHost { } } -/// Manages [ParagraphRuler] instances and caches them per unique -/// [ParagraphGeometricStyle]. -/// -/// All instances of [ParagraphRuler] should be created through this class. -/// -/// An optional `root` [html.Node] can be passed, under which the DOM required -/// to perform measurements will be hosted. -class RulerManager extends RulerHost { - RulerManager({ - required this.rulerCacheCapacity, - html.Node? root, - }) : super(root: root); - - final int rulerCacheCapacity; - - /// The cache of rulers used to measure text. - /// - /// Each ruler is keyed by paragraph style. This allows us to set up the - /// ruler's DOM structure once during the very first measurement of a given - /// paragraph style. Subsequent measurements could reuse the same ruler and - /// only swap the text contents. This minimizes the amount of work a browser - /// needs to do when measure many pieces of text with the same style. - /// - /// What makes this cache effective is the fact that a typical application - /// only uses a limited number of text styles. Using too many text styles on - /// the same screen is considered bad for user experience. - Map get rulers => _rulers; - Map _rulers = - {}; - - bool _rulerCacheCleanupScheduled = false; - - void _scheduleRulerCacheCleanup() { - if (!_rulerCacheCleanupScheduled) { - _rulerCacheCleanupScheduled = true; - scheduleMicrotask(() { - _rulerCacheCleanupScheduled = false; - cleanUpRulerCache(); - }); - } - } - - // Evicts all rulers from the cache. - void _evictAllRulers() { - _rulers.forEach((ParagraphGeometricStyle style, ParagraphRuler ruler) { - ruler.dispose(); - }); - _rulers = {}; - } - - /// If [window._isPhysicalSizeActuallyEmpty], evicts all rulers from the cache. - /// If ruler cache size exceeds [rulerCacheCapacity], evicts those rulers that - /// were used the least. - /// - /// Resets hit counts back to zero. - @visibleForTesting - void cleanUpRulerCache() { - // Measurements performed (and cached) inside a hidden iframe (with - // display:none) are wrong. - // Evict all rulers, so text gets re-measured when the iframe becomes - // visible. - // see: https://github.com/flutter/flutter/issues/36341 - if (window.physicalSize.isEmpty) { - _evictAllRulers(); - return; - } - if (_rulers.length > rulerCacheCapacity) { - final List sortedByUsage = _rulers.values.toList(); - sortedByUsage.sort((ParagraphRuler a, ParagraphRuler b) { - return b.hitCount - a.hitCount; - }); - _rulers = {}; - for (int i = 0; i < sortedByUsage.length; i++) { - final ParagraphRuler ruler = sortedByUsage[i]; - ruler.resetHitCount(); - if (i < rulerCacheCapacity) { - // Retain this ruler. - _rulers[ruler.style] = ruler; - } else { - // This ruler did not have enough usage this frame to be retained. - ruler.dispose(); - } - } - } - } - - /// Performs a cache lookup to find an existing [ParagraphRuler] for the given - /// [style] and if it can't find one in the cache, it would create one. - /// - /// The returned ruler is marked as hit so there's no need to do that - /// elsewhere. - @visibleForTesting - ParagraphRuler findOrCreateRuler(ParagraphGeometricStyle style) { - ParagraphRuler? ruler = _rulers[style]; - if (ruler == null) { - if (assertionsEnabled) { - domRenderer.debugRulerCacheMiss(); - } - ruler = _rulers[style] = ParagraphRuler(style, this); - _scheduleRulerCacheCleanup(); - } else { - if (assertionsEnabled) { - domRenderer.debugRulerCacheHit(); - } - } - ruler.hit(); - return ruler; - } -} - -/// Provides various text measurement APIs using either a dom-based approach -/// in [DomTextMeasurementService], or a canvas-based approach in -/// [CanvasTextMeasurementService]. -abstract class TextMeasurementService { - /// Whether this service uses a canvas to make the text measurements. - /// - /// If [isCanvas] is false, it indicates that this service uses DOM elements - /// to make the text measurements. - bool get isCanvas; - - /// Initializes the text measurement service with a specific - /// [rulerCacheCapacity] that gets passed to the [RulerManager]. - /// - /// An optional `root` [html.Node] can be passed, under which the DOM required - /// to perform measurements will be hosted. Defaults to [domRenderer.glassPaneShadow]. - static void initialize({required int rulerCacheCapacity, html.Node? root}) { - rulerManager?.dispose(); - rulerManager = null; - rulerManager = RulerManager( - rulerCacheCapacity: rulerCacheCapacity, - root: root, - ); - } - - @visibleForTesting - static RulerManager? rulerManager; - - /// The DOM-based text measurement service. - @visibleForTesting - static TextMeasurementService get domInstance => - DomTextMeasurementService.instance; - - /// The canvas-based text measurement service. - @visibleForTesting - static TextMeasurementService get canvasInstance => - CanvasTextMeasurementService.instance; - - /// Gets the appropriate [TextMeasurementService] instance for the given - /// [paragraph]. - static TextMeasurementService forParagraph(ui.Paragraph paragraph) { - // TODO(mdebbar): https://github.com/flutter/flutter/issues/33523 - // When the canvas-based implementation is complete and passes all the - // tests, get rid of [_experimentalEnableCanvasImplementation]. - // We need to check [window.physicalSize.isEmpty] because some canvas - // commands don't work as expected when they run inside a hidden iframe - // (with display:none) - // Skip using canvas measurements until the iframe becomes visible. - // see: https://github.com/flutter/flutter/issues/36341 - if (!window.physicalSize.isEmpty && - WebExperiments.instance!.useCanvasText && - _canUseCanvasMeasurement(paragraph as DomParagraph)) { - return canvasInstance; - } - return domInstance; - } - - /// Clears the cache of paragraph rulers that are used for measuring paragraph - /// metrics. - static void clearCache() { - rulerManager?._evictAllRulers(); - } - - static bool _canUseCanvasMeasurement(DomParagraph paragraph) { - // Currently, the canvas-based approach only works on plain text that - // doesn't have any of the following styles: - // - decoration - // - word spacing - final ParagraphGeometricStyle style = paragraph.geometricStyle; - return paragraph.plainText != null && - style.decoration == null && - style.wordSpacing == null; - } - - /// Measures the paragraph and returns a [MeasurementResult] object. - MeasurementResult? measure( - DomParagraph paragraph, - ui.ParagraphConstraints constraints, - ) { - assert(rulerManager != null); - final ParagraphGeometricStyle style = paragraph.geometricStyle; - final ParagraphRuler ruler = - TextMeasurementService.rulerManager!.findOrCreateRuler(style); - - if (assertionsEnabled) { - if (paragraph.plainText == null) { - domRenderer.debugRichTextLayout(); - } else { - domRenderer.debugPlainTextLayout(); - } - } - - MeasurementResult? result = ruler.cacheLookup(paragraph, constraints); - if (result != null) { - return result; - } - - result = _doMeasure(paragraph, constraints, ruler); - ruler.cacheMeasurement(paragraph, result); - return result; - } - - /// Measures the width of a substring of the given [paragraph] with no - /// constraints. - double measureSubstringWidth(DomParagraph paragraph, int start, int end); - - /// Returns text position given a paragraph, constraints and offset. - ui.TextPosition getTextPositionForOffset(DomParagraph paragraph, - ui.ParagraphConstraints? constraints, ui.Offset offset); - - /// Delegates to a [ParagraphRuler] to measure a list of text boxes that - /// enclose the given range of text. - List measureBoxesForRange( - DomParagraph paragraph, - ui.ParagraphConstraints constraints, { - required int start, - required int end, - required double alignOffset, - required ui.TextDirection textDirection, - }) { - final ParagraphGeometricStyle style = paragraph.geometricStyle; - final ParagraphRuler ruler = - TextMeasurementService.rulerManager!.findOrCreateRuler(style); - - return ruler.measureBoxesForRange( - paragraph.plainText!, - constraints, - start: start, - end: end, - alignOffset: alignOffset, - textDirection: textDirection, - ); - } - - /// Performs the actual measurement of the following values for the given - /// paragraph: - /// - /// * isSingleLine: whether the paragraph can be rendered in a single line. - /// * height: constrained measure of the entire paragraph's height. - /// * lineHeight: the height of a single line of the paragraph. - /// * alphabeticBaseline: single line measure. - /// * ideographicBaseline: based on [alphabeticBaseline]. - /// * maxIntrinsicWidth: the width of the paragraph with no line-wrapping. - /// * minIntrinsicWidth: the min width the paragraph fits in without overflowing. - /// - /// [MeasurementResult.width] is set to the same value of [constraints.width]. - /// - /// It also optionally computes [MeasurementResult.lines] in the given - /// paragraph. When that's available, it can be used by a canvas to render - /// the text line. - MeasurementResult _doMeasure( - DomParagraph paragraph, - ui.ParagraphConstraints constraints, - ParagraphRuler ruler, - ); -} - -/// A DOM-based text measurement implementation. -/// -/// This implementation is slower than [CanvasTextMeasurementService] but it's -/// needed for some cases that aren't yet supported in the canvas-based -/// implementation such as letter-spacing, word-spacing, etc. -class DomTextMeasurementService extends TextMeasurementService { - @override - final bool isCanvas = false; - - /// The text measurement service singleton. - static DomTextMeasurementService get instance => - _instance ??= DomTextMeasurementService(); - - static DomTextMeasurementService? _instance; - - @override - MeasurementResult _doMeasure( - DomParagraph paragraph, - ui.ParagraphConstraints constraints, - ParagraphRuler ruler, - ) { - ruler.willMeasure(paragraph); - final String? plainText = paragraph.plainText; - - ruler.measureAll(constraints); - - MeasurementResult result; - // When the text has a new line, we should always use multi-line mode. - final bool hasNewline = plainText?.contains('\n') ?? false; - if (!hasNewline && ruler.singleLineDimensions.width <= constraints.width) { - result = _measureSingleLineParagraph(ruler, paragraph, constraints); - } else { - // Assert: If text doesn't have new line for infinite constraints we - // should have called single line measure paragraph instead. - assert(hasNewline || constraints.width != double.infinity); - result = _measureMultiLineParagraph(ruler, paragraph, constraints); - } - ruler.didMeasure(); - return result; - } - - @override - double measureSubstringWidth(DomParagraph paragraph, int start, int end) { - assert(paragraph.plainText != null); - final ParagraphGeometricStyle style = paragraph.geometricStyle; - final ParagraphRuler ruler = - TextMeasurementService.rulerManager!.findOrCreateRuler(style); - - final String text = paragraph.plainText!.substring(start, end); - final ui.Paragraph substringParagraph = paragraph.cloneWithText(text); - - ruler.willMeasure(substringParagraph as DomParagraph); - ruler.measureAsSingleLine(); - final TextDimensions dimensions = ruler.singleLineDimensions; - ruler.didMeasure(); - return dimensions.width; - } - - @override - ui.TextPosition getTextPositionForOffset(DomParagraph paragraph, - ui.ParagraphConstraints? constraints, ui.Offset offset) { - assert( - paragraph.measurementResult!.lines == null, - 'should only be called when the faster lines-based approach is not possible', - ); - - final ParagraphGeometricStyle style = paragraph.geometricStyle; - final ParagraphRuler ruler = - TextMeasurementService.rulerManager!.findOrCreateRuler(style); - ruler.willMeasure(paragraph); - final int position = ruler.hitTest(constraints!, offset); - ruler.didMeasure(); - return ui.TextPosition(offset: position); - } - - /// Called when we have determined that the paragraph fits the [constraints] - /// without wrapping. - /// - /// This means that: - /// * `width == maxIntrinsicWidth` - we gave it more horizontal space than - /// it needs and so the paragraph won't expand beyond `maxIntrinsicWidth`. - /// * `height` is the height computed by `measureAsSingleLine`; giving the - /// paragraph the width constraint won't change its height as we already - /// determined that it fits within the constraint without wrapping. - /// * `alphabeticBaseline` is also final for the same reason as the `height` - /// value. - /// - /// This method still needs to measure `minIntrinsicWidth`. - MeasurementResult _measureSingleLineParagraph( - ParagraphRuler ruler, - DomParagraph paragraph, - ui.ParagraphConstraints constraints, - ) { - final double width = constraints.width; - final double minIntrinsicWidth = ruler.minIntrinsicDimensions.width; - double maxIntrinsicWidth = ruler.singleLineDimensions.width; - final double alphabeticBaseline = ruler.alphabeticBaseline; - final double height = ruler.singleLineDimensions.height; - - maxIntrinsicWidth = - _applySubPixelRoundingHack(minIntrinsicWidth, maxIntrinsicWidth); - final double ideographicBaseline = alphabeticBaseline * baselineRatioHack; - - final String? text = paragraph.plainText; - List? lines; - if (text != null) { - final double lineWidth = maxIntrinsicWidth; - final double alignOffset = _calculateAlignOffsetForLine( - paragraph: paragraph, - lineWidth: lineWidth, - maxWidth: width, - ); - lines = [ - EngineLineMetrics.withText( - text, - startIndex: 0, - endIndex: text.length, - endIndexWithoutNewlines: - _excludeTrailing(text, 0, text.length, _newlinePredicate), - hardBreak: true, - width: lineWidth, - widthWithTrailingSpaces: lineWidth, - left: alignOffset, - lineNumber: 0, - ), - ]; - } - - return MeasurementResult( - constraints.width, - isSingleLine: true, - width: width, - height: height, - naturalHeight: height, - lineHeight: height, - minIntrinsicWidth: minIntrinsicWidth, - maxIntrinsicWidth: maxIntrinsicWidth, - alphabeticBaseline: alphabeticBaseline, - ideographicBaseline: ideographicBaseline, - lines: lines, - placeholderBoxes: ruler.measurePlaceholderBoxes(), - textAlign: paragraph.textAlign, - textDirection: paragraph.textDirection, - ); - } - - /// Called when we have determined that the paragraph needs to wrap into - /// multiple lines to fit the [constraints], i.e. its `maxIntrinsicWidth` is - /// bigger than the available horizontal space. - /// - /// While `maxIntrinsicWidth` is still good from the call to - /// `measureAsSingleLine`, we need to re-measure with the width constraint - /// and get new values for width, height and alphabetic baseline. We also need - /// to measure `minIntrinsicWidth`. - MeasurementResult _measureMultiLineParagraph(ParagraphRuler ruler, - DomParagraph paragraph, ui.ParagraphConstraints constraints) { - // If constraint is infinite, we must use _measureSingleLineParagraph - final double width = constraints.width; - final double minIntrinsicWidth = ruler.minIntrinsicDimensions.width; - double maxIntrinsicWidth = ruler.singleLineDimensions.width; - final double alphabeticBaseline = ruler.alphabeticBaseline; - // Natural height is the full height of text ignoring height constraints. - final double naturalHeight = ruler.constrainedDimensions.height; - - double height; - double? lineHeight; - final int? maxLines = paragraph.geometricStyle.maxLines; - if (maxLines == null) { - height = naturalHeight; - } else { - // Lazily compute [lineHeight] when [maxLines] is not null. - lineHeight = ruler.lineHeight; - height = math.min(naturalHeight, maxLines * lineHeight); - } - - maxIntrinsicWidth = - _applySubPixelRoundingHack(minIntrinsicWidth, maxIntrinsicWidth); - assert(minIntrinsicWidth <= maxIntrinsicWidth); - final double ideographicBaseline = alphabeticBaseline * baselineRatioHack; - return MeasurementResult( - constraints.width, - isSingleLine: false, - width: width, - height: height, - lineHeight: lineHeight, - naturalHeight: naturalHeight, - minIntrinsicWidth: minIntrinsicWidth, - maxIntrinsicWidth: maxIntrinsicWidth, - alphabeticBaseline: alphabeticBaseline, - ideographicBaseline: ideographicBaseline, - lines: null, - placeholderBoxes: ruler.measurePlaceholderBoxes(), - textAlign: paragraph.textAlign, - textDirection: paragraph.textDirection, - ); - } - - /// This hack is needed because `offsetWidth` rounds the value to the nearest - /// whole number. On a very rare occasion the minimum intrinsic width reported - /// by the browser is slightly bigger than the reported maximum intrinsic - /// width. If the discrepancy overlaps 0.5 then the rounding happens in - /// opposite directions. - /// - /// For example, if minIntrinsicWidth == 99.5 and maxIntrinsicWidth == 99.48, - /// then minIntrinsicWidth is rounded up to 100, and maxIntrinsicWidth is - /// rounded down to 99. - // TODO(yjbanov): remove the need for this hack. - static double _applySubPixelRoundingHack( - double minIntrinsicWidth, double maxIntrinsicWidth) { - if (minIntrinsicWidth <= maxIntrinsicWidth) { - return maxIntrinsicWidth; - } - - if (minIntrinsicWidth - maxIntrinsicWidth < 2.0) { - return minIntrinsicWidth; - } - - throw Exception('minIntrinsicWidth ($minIntrinsicWidth) is greater than ' - 'maxIntrinsicWidth ($maxIntrinsicWidth).'); - } -} - -/// A canvas-based text measurement implementation. -/// -/// This is a faster implementation than [DomTextMeasurementService] and -/// provides line breaks information that can be useful for multi-line text. -class CanvasTextMeasurementService extends TextMeasurementService { - @override - final bool isCanvas = true; - - /// The text measurement service singleton. - static CanvasTextMeasurementService get instance => - _instance ??= CanvasTextMeasurementService(); - - static CanvasTextMeasurementService? _instance; - - final html.CanvasRenderingContext2D _canvasContext = - html.CanvasElement().context2D; - - @override - MeasurementResult _doMeasure( - DomParagraph paragraph, - ui.ParagraphConstraints constraints, - ParagraphRuler ruler, - ) { - final String text = paragraph.plainText!; - final ParagraphGeometricStyle style = paragraph.geometricStyle; - assert(text != null); // ignore: unnecessary_null_comparison - - // TODO(mdebbar): Check if the whole text can fit in a single-line. Then avoid all this ceremony. - _canvasContext.font = style.cssFontString; - final LinesCalculator linesCalculator = - LinesCalculator(_canvasContext, paragraph, constraints.width); - final MinIntrinsicCalculator minIntrinsicCalculator = - MinIntrinsicCalculator(_canvasContext, text, style); - final MaxIntrinsicCalculator maxIntrinsicCalculator = - MaxIntrinsicCalculator(_canvasContext, text, style); - - // Indicates whether we've reached the end of text or not. Even if the index - // [i] reaches the end of text, we don't want to stop looping until we hit - // [LineBreakType.endOfText] because there could be a "\n" at the end of the - // string and that would mess things up. - bool reachedEndOfText = false; - - // TODO(mdebbar): Chrome & Safari return more info from [canvasContext.measureText]. - int i = 0; - while (!reachedEndOfText) { - final LineBreakResult brk = nextLineBreak(text, i); - - linesCalculator.update(brk); - minIntrinsicCalculator.update(brk); - maxIntrinsicCalculator.update(brk); - - i = brk.index; - if (brk.type == LineBreakType.endOfText) { - reachedEndOfText = true; - } - } - - final double alphabeticBaseline = ruler.alphabeticBaseline; - final int lineCount = linesCalculator.lines.length; - final double lineHeight = ruler.lineHeight; - final double naturalHeight = lineCount * lineHeight; - final int? maxLines = style.maxLines; - final double height = maxLines == null - ? naturalHeight - : math.min(lineCount, maxLines) * lineHeight; - - final MeasurementResult result = MeasurementResult( - constraints.width, - isSingleLine: lineCount == 1, - alphabeticBaseline: alphabeticBaseline, - ideographicBaseline: alphabeticBaseline * baselineRatioHack, - height: height, - naturalHeight: naturalHeight, - lineHeight: lineHeight, - // `minIntrinsicWidth` is the greatest width of text that can't - // be broken down into multiple lines. - minIntrinsicWidth: minIntrinsicCalculator.value, - // `maxIntrinsicWidth` is the width of the widest piece of text - // that doesn't contain mandatory line breaks. - maxIntrinsicWidth: maxIntrinsicCalculator.value, - width: constraints.width, - lines: linesCalculator.lines, - placeholderBoxes: const [], - textAlign: paragraph.textAlign, - textDirection: paragraph.textDirection, - ); - return result; - } - - @override - double measureSubstringWidth(DomParagraph paragraph, int start, int end) { - assert(paragraph.plainText != null); - final String text = paragraph.plainText!; - final ParagraphGeometricStyle style = paragraph.geometricStyle; - _canvasContext.font = style.cssFontString; - return measureSubstring( - _canvasContext, - text, - start, - end, - letterSpacing: paragraph.geometricStyle.letterSpacing, - ); - } - - @override - ui.TextPosition getTextPositionForOffset(EngineParagraph paragraph, - ui.ParagraphConstraints? constraints, ui.Offset offset) { - // TODO(mdebbar): implement. - return const ui.TextPosition(offset: 0); - } -} - // These global variables are used to memoize calls to [measureSubstring]. They // are used to remember the last arguments passed to it, and the last return // value. @@ -746,327 +124,3 @@ double measureSubstring( double _roundWidth(double width) { return (width * 100).round() / 100; } - -/// From the substring defined by [text], [start] (inclusive) and [end] -/// (exclusive), exclude trailing characters that satisfy the given [predicate]. -/// -/// The return value is the new end of the substring after excluding the -/// trailing characters. -int _excludeTrailing(String text, int start, int end, CharPredicate predicate) { - assert(0 <= start); - assert(start <= end); - assert(end <= text.length); - - while (start < end && predicate(text.codeUnitAt(end - 1))) { - end--; - } - return end; -} - -/// During the text layout phase, this class splits the lines of text so that it -/// ends up fitting into the given width constraint. -/// -/// It implements the Flutter engine's behavior when it comes to handling -/// ellipsis and max lines. -class LinesCalculator { - LinesCalculator(this._canvasContext, this._paragraph, this._maxWidth); - - final html.CanvasRenderingContext2D _canvasContext; - final DomParagraph _paragraph; - final double _maxWidth; - - String? get _text => _paragraph.plainText; - ParagraphGeometricStyle get _style => _paragraph.geometricStyle; - - /// The lines that have been consumed so far. - List lines = []; - - /// The last line break regardless of whether it was optional or mandatory, or - /// whether we took it or not. - LineBreakResult _lastBreak = - const LineBreakResult.sameIndex(0, LineBreakType.mandatory); - - /// The last line break that actually caused a new line to exist. - LineBreakResult _lastTakenBreak = - const LineBreakResult.sameIndex(0, LineBreakType.mandatory); - - int get _lineStart => _lastTakenBreak.index; - int get _chunkStart => _lastBreak.index; - bool _reachedMaxLines = false; - - double? _cachedEllipsisWidth; - double get _ellipsisWidth => _cachedEllipsisWidth ??= - _roundWidth(_canvasContext.measureText(_style.ellipsis!).width! as double); - - bool get hasEllipsis => _style.ellipsis != null; - bool get unlimitedLines => _style.maxLines == null; - - /// Consumes the next line break opportunity in [_text]. - /// - /// This method should be called for every line break. As soon as it reaches - /// the maximum number of lines required - void update(LineBreakResult brk) { - final int chunkEnd = brk.index; - final int chunkEndWithoutNewlines = brk.indexWithoutTrailingNewlines; - final int chunkEndWithoutSpace = brk.indexWithoutTrailingSpaces; - - // A single chunk of text could be force-broken into multiple lines if it - // doesn't fit in a single line. That's why we need a loop. - while (!_reachedMaxLines) { - final double lineWidth = - measureSubstringWidth(_lineStart, chunkEndWithoutSpace); - - // The current chunk doesn't reach the maximum width, so we stop here and - // wait for the next line break. - if (lineWidth <= _maxWidth) { - break; - } - - // If the current chunk starts at the beginning of the line and exceeds - // [maxWidth], then we will need to force-break it. - final bool isChunkTooLong = _chunkStart == _lineStart; - - // When ellipsis is set, and maxLines is null, we stop at the first line - // that exceeds [maxWidth]. - final bool isLastLine = _reachedMaxLines = - (hasEllipsis && unlimitedLines) || - lines.length + 1 == _style.maxLines; - - if (isLastLine && hasEllipsis) { - // When there's an ellipsis, truncate text to leave enough space for - // the ellipsis. - final double availableWidth = _maxWidth - _ellipsisWidth; - final int breakingPoint = forceBreakSubstring( - maxWidth: availableWidth, - start: _lineStart, - end: chunkEndWithoutSpace, - ); - final double widthOfResultingLine = - measureSubstringWidth(_lineStart, breakingPoint) + _ellipsisWidth; - final double alignOffset = _calculateAlignOffsetForLine( - paragraph: _paragraph, - lineWidth: widthOfResultingLine, - maxWidth: _maxWidth, - ); - lines.add(EngineLineMetrics.withText( - _text!.substring(_lineStart, breakingPoint) + _style.ellipsis!, - startIndex: _lineStart, - endIndex: chunkEnd, - endIndexWithoutNewlines: chunkEndWithoutNewlines, - hardBreak: false, - width: widthOfResultingLine, - widthWithTrailingSpaces: widthOfResultingLine, - left: alignOffset, - lineNumber: lines.length, - )); - } else if (isChunkTooLong) { - final int breakingPoint = forceBreakSubstring( - maxWidth: _maxWidth, - start: _lineStart, - end: chunkEndWithoutSpace, - ); - if (breakingPoint == chunkEndWithoutSpace) { - // We couldn't force-break the chunk any further which means we reached - // the last character and there isn't enough space for it to fit in - // its own line. Since this is the last character in the chunk, we - // don't do anything here and we rely on the next iteration (or the - // [isHardBreak] check below) to break the line. - break; - } - _addLineBreak(LineBreakResult.sameIndex( - breakingPoint, - LineBreakType.opportunity, - )); - } else { - // The control case of current line exceeding [_maxWidth], we break the - // line. - _addLineBreak(_lastBreak); - } - } - - if (_reachedMaxLines) { - return; - } - - if (brk.isHard) { - _addLineBreak(brk); - } - _lastBreak = brk; - } - - void _addLineBreak(LineBreakResult brk) { - final int lineNumber = lines.length; - final double lineWidth = - measureSubstringWidth(_lineStart, brk.indexWithoutTrailingSpaces); - final double lineWidthWithTrailingSpaces = - measureSubstringWidth(_lineStart, brk.indexWithoutTrailingNewlines); - final double alignOffset = _calculateAlignOffsetForLine( - paragraph: _paragraph, - lineWidth: lineWidth, - maxWidth: _maxWidth, - ); - - final EngineLineMetrics metrics = EngineLineMetrics.withText( - _text!.substring(_lineStart, brk.indexWithoutTrailingNewlines), - startIndex: _lineStart, - endIndex: brk.index, - endIndexWithoutNewlines: brk.indexWithoutTrailingNewlines, - hardBreak: brk.isHard, - width: lineWidth, - widthWithTrailingSpaces: lineWidthWithTrailingSpaces, - left: alignOffset, - lineNumber: lineNumber, - ); - lines.add(metrics); - _lastTakenBreak = _lastBreak = brk; - if (lines.length == _style.maxLines) { - _reachedMaxLines = true; - } - } - - /// Measures the width of a substring of [_text] starting from the index - /// [start] (inclusive) to [end] (exclusive). - /// - /// This method uses [_text], [_style] and [_canvasContext] to perform the - /// measurement. - double measureSubstringWidth(int start, int end) { - return measureSubstring( - _canvasContext, - _text!, - start, - end, - letterSpacing: _style.letterSpacing, - ); - } - - /// In a continuous block of text, finds the point where text can be broken to - /// fit in the given constraint [maxWidth]. - /// - /// This always returns at least one character even if there isn't enough - /// space for it. - int forceBreakSubstring({ - required double maxWidth, - required int start, - required int end, - }) { - assert(0 <= start); - assert(start < end); - assert(end <= _text!.length); - - // When there's no ellipsis, the breaking point should be at least one - // character away from [start]. - int low = hasEllipsis ? start : start + 1; - int high = end; - do { - final int mid = (low + high) ~/ 2; - final double width = measureSubstringWidth(start, mid); - if (width < maxWidth) { - low = mid; - } else if (width > maxWidth) { - high = mid; - } else { - low = high = mid; - } - } while (high - low > 1); - - return low; - } -} - -/// During the text layout phase, this class takes care of calculating the -/// minimum intrinsic width of the given text. -class MinIntrinsicCalculator { - MinIntrinsicCalculator(this._canvasContext, this._text, this._style); - - final html.CanvasRenderingContext2D _canvasContext; - final String _text; - final ParagraphGeometricStyle _style; - - /// The value of minimum intrinsic width calculated so far. - double value = 0.0; - int _lastChunkEnd = 0; - - /// Consumes the next line break opportunity in [_text]. - /// - /// As this method gets called, it updates the [value] to the minimum - /// intrinsic width calculated so far. When the whole text is consumed, - /// [value] will contain the final minimum intrinsic width. - void update(LineBreakResult brk) { - final int chunkEnd = brk.index; - final double width = measureSubstring( - _canvasContext, - _text, - _lastChunkEnd, - brk.indexWithoutTrailingSpaces, - letterSpacing: _style.letterSpacing, - ); - if (width > value) { - value = width; - } - _lastChunkEnd = chunkEnd; - } -} - -/// During text layout, this class is responsible for calculating the maximum -/// intrinsic width of the given text. -class MaxIntrinsicCalculator { - MaxIntrinsicCalculator(this._canvasContext, this._text, this._style); - - final html.CanvasRenderingContext2D _canvasContext; - final String _text; - final ParagraphGeometricStyle _style; - - /// The value of maximum intrinsic width calculated so far. - double value = 0.0; - int _lastHardLineEnd = 0; - - /// Consumes the next line break opportunity in [_text]. - /// - /// As this method gets called, it updates the [value] to the maximum - /// intrinsic width calculated so far. When the whole text is consumed, - /// [value] will contain the final maximum intrinsic width. - void update(LineBreakResult brk) { - if (!brk.isHard) { - return; - } - - final double lineWidth = measureSubstring( - _canvasContext, - _text, - _lastHardLineEnd, - brk.indexWithoutTrailingNewlines, - letterSpacing: _style.letterSpacing, - ); - if (lineWidth > value) { - value = lineWidth; - } - _lastHardLineEnd = brk.index; - } -} - -/// Calculates the offset necessary for the given line to be correctly aligned. -double _calculateAlignOffsetForLine({ - required DomParagraph paragraph, - required double lineWidth, - required double maxWidth, -}) { - final double emptySpace = maxWidth - lineWidth; - // WARNING: the [paragraph] may not be laid out yet at this point. This - // function must not use layout metrics, such as [paragraph.height]. - switch (paragraph.textAlign) { - case ui.TextAlign.center: - return emptySpace / 2.0; - case ui.TextAlign.right: - return emptySpace; - case ui.TextAlign.start: - return paragraph.textDirection == ui.TextDirection.rtl - ? emptySpace - : 0.0; - case ui.TextAlign.end: - return paragraph.textDirection == ui.TextDirection.rtl - ? 0.0 - : emptySpace; - default: - return 0.0; - } -} diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 8a942ac0c60fc..11e747c030cd4 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'package:ui/ui.dart' as ui; @@ -11,18 +10,9 @@ import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; import '../dom_renderer.dart'; import '../html/bitmap_canvas.dart'; -import '../html/painting.dart'; -import '../profiler.dart'; import '../util.dart'; -import 'canvas_paragraph.dart'; import 'layout_service.dart'; -import 'measurement.dart'; import 'ruler.dart'; -import 'word_breaker.dart'; - -const ui.Color defaultTextColor = ui.Color(0xFFFF0000); - -const String placeholderClass = 'paragraph-placeholder'; class EngineLineMetrics implements ui.LineMetrics { EngineLineMetrics({ @@ -238,579 +228,6 @@ abstract class EngineParagraph implements ui.Paragraph { html.HtmlElement toDomElement(); } -/// Uses the DOM and hierarchical elements to represent the span of the -/// paragraph. -/// -/// This implementation will go away once the new [CanvasParagraph] is -/// complete and turned on by default. -class DomParagraph implements EngineParagraph { - /// This class is created by the engine, and should not be instantiated - /// or extended directly. - /// - /// To create a [DomParagraph] object, use a [DomParagraphBuilder]. - DomParagraph({ - required html.HtmlElement paragraphElement, - required ParagraphGeometricStyle geometricStyle, - required String? plainText, - required ui.Paint? paint, - required ui.TextAlign textAlign, - required ui.TextDirection textDirection, - required ui.Paint? background, - required this.placeholderCount, - }) : assert((plainText == null && paint == null) || - (plainText != null && paint != null)), - _paragraphElement = paragraphElement, - _geometricStyle = geometricStyle, - _plainText = plainText, - _textAlign = textAlign, - _textDirection = textDirection, - _paint = paint as SurfacePaint?, - _background = background as SurfacePaint?; - - final html.HtmlElement _paragraphElement; - final ParagraphGeometricStyle _geometricStyle; - final String? _plainText; - final SurfacePaint? _paint; - final ui.TextAlign _textAlign; - final ui.TextDirection _textDirection; - final SurfacePaint? _background; - - final int placeholderCount; - - String? get plainText => _plainText; - - html.HtmlElement get paragraphElement => _paragraphElement; - - ui.TextAlign get textAlign => _textAlign; - ui.TextDirection get textDirection => _textDirection; - - ParagraphGeometricStyle get geometricStyle => _geometricStyle; - - /// The instance of [TextMeasurementService] to be used to measure this - /// paragraph. - TextMeasurementService get _measurementService => - TextMeasurementService.forParagraph(this); - - /// The measurement result of the last layout operation. - MeasurementResult? get measurementResult => _measurementResult; - MeasurementResult? _measurementResult; - - bool get _hasLineMetrics => _measurementResult?.lines != null; - - // Defaulting to -1 for non-laid-out paragraphs like the native engine does. - @override - double get width => _measurementResult?.width ?? -1; - - @override - double get height => _measurementResult?.height ?? 0; - - /// {@template dart.ui.paragraph.naturalHeight} - /// The amount of vertical space the paragraph occupies while ignoring the - /// [ParagraphGeometricStyle.maxLines] constraint. - /// {@endtemplate} - /// - /// Valid only after [layout] has been called. - double get _naturalHeight => _measurementResult?.naturalHeight ?? 0; - - /// The amount of vertical space one line of this paragraph occupies. - /// - /// Valid only after [layout] has been called. - double get _lineHeight => _measurementResult?.lineHeight ?? 0; - - @override - double get longestLine { - if (_hasLineMetrics) { - double maxWidth = 0.0; - for (final ui.LineMetrics metrics in _measurementResult!.lines!) { - if (maxWidth < metrics.width) { - maxWidth = metrics.width; - } - } - return maxWidth; - } - - // If we don't have any line metrics information, there's no way to know the - // longest line in a multi-line paragraph. - return 0.0; - } - - @override - double get minIntrinsicWidth => _measurementResult?.minIntrinsicWidth ?? 0; - - @override - double get maxIntrinsicWidth => _measurementResult?.maxIntrinsicWidth ?? 0; - - @override - double get alphabeticBaseline => _measurementResult?.alphabeticBaseline ?? -1; - - @override - double get ideographicBaseline => - _measurementResult?.ideographicBaseline ?? -1; - - @override - bool get didExceedMaxLines => _didExceedMaxLines; - bool _didExceedMaxLines = false; - - ui.ParagraphConstraints? _lastUsedConstraints; - - /// Returns horizontal alignment offset for single line text when rendering - /// directly into a canvas without css text alignment styling. - double _alignOffset = 0.0; - - @override - void layout(ui.ParagraphConstraints constraints) { - // When constraint width has a decimal place, we floor it to avoid getting - // a layout width that's higher than the constraint width. - // - // For example, if constraint width is `30.8` and the text has a width of - // `30.5` then the TextPainter in the framework will ceil the `30.5` width - // which will result in a width of `40.0` that's higher than the constraint - // width. - constraints = ui.ParagraphConstraints( - width: constraints.width.floorToDouble(), - ); - - if (constraints == _lastUsedConstraints) { - return; - } - - late Stopwatch stopwatch; - if (Profiler.isBenchmarkMode) { - stopwatch = Stopwatch()..start(); - } - _measurementResult = _measurementService.measure(this, constraints); - if (Profiler.isBenchmarkMode) { - stopwatch.stop(); - Profiler.instance - .benchmark('text_layout', stopwatch.elapsedMicroseconds.toDouble()); - } - - _lastUsedConstraints = constraints; - - if (_geometricStyle.maxLines != null) { - _didExceedMaxLines = _naturalHeight > height; - } else { - _didExceedMaxLines = false; - } - - if (_measurementResult!.isSingleLine) { - switch (_textAlign) { - case ui.TextAlign.center: - _alignOffset = (constraints.width - maxIntrinsicWidth) / 2.0; - break; - case ui.TextAlign.right: - _alignOffset = constraints.width - maxIntrinsicWidth; - break; - case ui.TextAlign.start: - _alignOffset = _textDirection == ui.TextDirection.rtl - ? constraints.width - maxIntrinsicWidth - : 0.0; - break; - case ui.TextAlign.end: - _alignOffset = _textDirection == ui.TextDirection.ltr - ? constraints.width - maxIntrinsicWidth - : 0.0; - break; - default: - _alignOffset = 0.0; - break; - } - } - } - - @override - bool get hasArbitraryPaint => _geometricStyle.ellipsis != null; - - @override - void paint(BitmapCanvas canvas, ui.Offset offset) { - assert(drawOnCanvas); - assert(isLaidOut); - - // Paint the background first. - final SurfacePaint? background = _background; - if (background != null) { - final ui.Rect rect = - ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); - canvas.drawRect(rect, background.paintData); - } - - final List lines = _measurementResult!.lines!; - canvas.setCssFont(_geometricStyle.cssFontString); - - // Then paint the text. - canvas.setUpPaint(_paint!.paintData, null); - double y = offset.dy + alphabeticBaseline; - final int len = lines.length; - for (int i = 0; i < len; i++) { - _paintLine(canvas, lines[i], offset.dx, y); - y += _lineHeight; - } - canvas.tearDownPaint(); - } - - void _paintLine( - BitmapCanvas canvas, - EngineLineMetrics line, - double x, - double y, - ) { - x += line.left; - final double? letterSpacing = _geometricStyle.letterSpacing; - if (letterSpacing == null || letterSpacing == 0.0) { - canvas.fillText(line.displayText!, x, y); - } else { - // When letter-spacing is set, we go through a more expensive code path - // that renders each character separately with the correct spacing - // between them. - // - // We are drawing letter spacing like the web does it, by adding the - // spacing after each letter. This is different from Flutter which puts - // the spacing around each letter i.e. for a 10px letter spacing, Flutter - // would put 5px before each letter and 5px after it, but on the web, we - // put no spacing before the letter and 10px after it. This is how the DOM - // does it. - // - // TODO(mdebbar): Implement letter-spacing on canvas more efficiently: - // https://github.com/flutter/flutter/issues/51234 - final int len = line.displayText!.length; - for (int i = 0; i < len; i++) { - final String char = line.displayText![i]; - canvas.fillText(char, x, y); - x += letterSpacing + canvas.measureText(char).width!; - } - } - } - - @override - String toPlainText() { - return _plainText ?? - js_util.getProperty(_paragraphElement, 'textContent') as String; - } - - @override - html.HtmlElement toDomElement() { - assert(isLaidOut); - - final html.HtmlElement paragraphElement = - _paragraphElement.clone(true) as html.HtmlElement; - - final html.CssStyleDeclaration paragraphStyle = paragraphElement.style; - paragraphStyle - ..height = '${height}px' - ..width = '${width}px' - ..position = 'absolute' - ..whiteSpace = 'pre-wrap' - ..overflowWrap = 'break-word' - ..overflow = 'hidden'; - - final ParagraphGeometricStyle style = _geometricStyle; - - // TODO(mdebbar): https://github.com/flutter/flutter/issues/33223 - if (style.ellipsis != null && - (style.maxLines == null || style.maxLines == 1)) { - paragraphStyle - ..whiteSpace = 'pre' - ..textOverflow = 'ellipsis'; - } - return paragraphElement; - } - - @override - List getBoxesForPlaceholders() { - assert(isLaidOut); - return _measurementResult!.placeholderBoxes; - } - - /// Returns `true` if this paragraph can be directly painted to the canvas. - /// - /// - /// Examples of paragraphs that can't be drawn directly on the canvas: - /// - /// - Rich text where there are multiple pieces of text that have different - /// styles. - /// - Paragraphs that contain decorations. - /// - Paragraphs that have a non-null word-spacing. - /// - Paragraphs with a background. - @override - bool get drawOnCanvas { - if (!_hasLineMetrics) { - return false; - } - - bool canDrawTextOnCanvas; - if (_measurementService.isCanvas) { - canDrawTextOnCanvas = true; - } else { - canDrawTextOnCanvas = _geometricStyle.ellipsis == null; - } - - return canDrawTextOnCanvas && - _geometricStyle.decoration == null && - _geometricStyle.wordSpacing == null && - _geometricStyle.shadows == null; - } - - /// Whether this paragraph has been laid out. - @override - bool get isLaidOut => _measurementResult != null; - - /// Asserts that the properties used to measure paragraph layout are the same - /// as the properties of this paragraphs root style. - /// - /// Ignores properties that do not affect layout, such as - /// [ParagraphStyle.textAlign]. - bool debugHasSameRootStyle(ParagraphGeometricStyle style) { - assert(() { - if (style != _geometricStyle) { - throw Exception('Attempted to measure a paragraph whose style is ' - 'different from the style of the ruler used to measure it.'); - } - return true; - }()); - return true; - } - - @override - List getBoxesForRange( - int start, - int end, { - ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight, - ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight, - }) { - assert(boxHeightStyle != null); // ignore: unnecessary_null_comparison - assert(boxWidthStyle != null); // ignore: unnecessary_null_comparison - // Zero-length ranges and invalid ranges return an empty list. - if (start == end || start < 0 || end < 0) { - return []; - } - - // For rich text, we can't measure the boxes. So for now, we'll just return - // a placeholder box to stop exceptions from being thrown in the framework. - // https://github.com/flutter/flutter/issues/55587 - if (_plainText == null) { - return [ - ui.TextBox.fromLTRBD(0, 0, 0, _lineHeight, _textDirection), - ]; - } - - final int length = _plainText!.length; - // Ranges that are out of bounds should return an empty list. - if (start > length || end > length) { - return []; - } - - // Fallback to the old, DOM-based box measurements when there's no line - // metrics. - if (!_hasLineMetrics) { - return _measurementService.measureBoxesForRange( - this, - _lastUsedConstraints!, - start: start, - end: end, - alignOffset: _alignOffset, - textDirection: _textDirection, - ); - } - - final List lines = _measurementResult!.lines!; - if (start >= lines.last.endIndex) { - return []; - } - - final EngineLineMetrics startLine = _getLineForIndex(start); - EngineLineMetrics endLine = _getLineForIndex(end); - - // If the range end is exactly at the beginning of a line, we shouldn't - // include any boxes from that line. - if (end == endLine.startIndex) { - endLine = lines[endLine.lineNumber - 1]; - } - - final List boxes = []; - for (int i = startLine.lineNumber; i <= endLine.lineNumber; i++) { - boxes.add(_getBoxForLine(lines[i], start, end)); - } - return boxes; - } - - ui.TextBox _getBoxForLine(EngineLineMetrics line, int start, int end) { - final double widthBeforeBox = start <= line.startIndex - ? 0.0 - : _measurementService.measureSubstringWidth( - this, line.startIndex, start); - final double widthAfterBox = end >= line.endIndexWithoutNewlines - ? 0.0 - : _measurementService.measureSubstringWidth( - this, end, line.endIndexWithoutNewlines); - - final double top = line.lineNumber * _lineHeight; - - // |<------------------ line.width ------------------>| - // |-------------|------------------|-------------|-----------------| - // |<-line.left->|<-widthBeforeBox->|<-box width->|<-widthAfterBox->| - // |-------------|------------------|-------------|-----------------| - // - // ^^^^^^^^^^^^^ - // This is the box we want to return. - return ui.TextBox.fromLTRBD( - line.left + widthBeforeBox, - top, - line.left + line.widthWithTrailingSpaces - widthAfterBox, - top + _lineHeight, - _textDirection, - ); - } - - ui.Paragraph cloneWithText(String plainText) { - return DomParagraph( - plainText: plainText, - paragraphElement: _paragraphElement.clone(true) as html.HtmlElement, - geometricStyle: _geometricStyle, - paint: _paint, - textAlign: _textAlign, - textDirection: _textDirection, - background: _background, - placeholderCount: placeholderCount, - ); - } - - @override - ui.TextPosition getPositionForOffset(ui.Offset offset) { - final List? lines = _measurementResult!.lines; - if (!_hasLineMetrics) { - return getPositionForMultiSpanOffset(offset); - } - - // [offset] is above all the lines. - if (offset.dy < 0) { - return const ui.TextPosition( - offset: 0, - affinity: ui.TextAffinity.downstream, - ); - } - - final int lineNumber = offset.dy ~/ _measurementResult!.lineHeight!; - - // [offset] is below all the lines. - if (lineNumber >= lines!.length) { - return ui.TextPosition( - offset: _plainText!.length, - affinity: ui.TextAffinity.upstream, - ); - } - - final EngineLineMetrics lineMetrics = lines[lineNumber]; - final double lineLeft = lineMetrics.left; - final double lineRight = lineLeft + lineMetrics.width; - - // [offset] is to the left of the line. - if (offset.dx <= lineLeft) { - return ui.TextPosition( - offset: lineMetrics.startIndex, - affinity: ui.TextAffinity.downstream, - ); - } - - // [offset] is to the right of the line. - if (offset.dx >= lineRight) { - return ui.TextPosition( - offset: lineMetrics.endIndexWithoutNewlines, - affinity: ui.TextAffinity.upstream, - ); - } - - // If we reach here, it means the [offset] is somewhere within the line. The - // code below will do a binary search to find where exactly the [offset] - // falls within the line. - - final double dx = offset.dx - lineMetrics.left; - final TextMeasurementService instance = _measurementService; - - int low = lineMetrics.startIndex; - int high = lineMetrics.endIndexWithoutNewlines; - do { - final int current = (low + high) ~/ 2; - final double width = - instance.measureSubstringWidth(this, lineMetrics.startIndex, current); - if (width < dx) { - low = current; - } else if (width > dx) { - high = current; - } else { - low = high = current; - } - } while (high - low > 1); - - if (low == high) { - // The offset falls exactly in between the two letters. - return ui.TextPosition(offset: high, affinity: ui.TextAffinity.upstream); - } - - final double lowWidth = - instance.measureSubstringWidth(this, lineMetrics.startIndex, low); - final double highWidth = - instance.measureSubstringWidth(this, lineMetrics.startIndex, high); - - if (dx - lowWidth < highWidth - dx) { - // The offset is closer to the low index. - return ui.TextPosition(offset: low, affinity: ui.TextAffinity.downstream); - } else { - // The offset is closer to high index. - return ui.TextPosition(offset: high, affinity: ui.TextAffinity.upstream); - } - } - - ui.TextPosition getPositionForMultiSpanOffset(ui.Offset offset) { - assert(_lastUsedConstraints != null, - 'missing call to paragraph layout before reading text position'); - final TextMeasurementService instance = _measurementService; - return instance.getTextPositionForOffset( - this, _lastUsedConstraints, offset); - } - - @override - ui.TextRange getWordBoundary(ui.TextPosition position) { - final ui.TextPosition textPosition = position; - final String? text = _plainText; - if (text == null) { - return ui.TextRange(start: textPosition.offset, end: textPosition.offset); - } - - final int start = WordBreaker.prevBreakIndex(text, textPosition.offset + 1); - final int end = WordBreaker.nextBreakIndex(text, textPosition.offset); - return ui.TextRange(start: start, end: end); - } - - EngineLineMetrics _getLineForIndex(int index) { - assert(_hasLineMetrics); - final List lines = _measurementResult!.lines!; - assert(index >= 0); - - for (int i = 0; i < lines.length; i++) { - final EngineLineMetrics line = lines[i]; - if (index >= line.startIndex && index < line.endIndex) { - return line; - } - } - - return lines.last; - } - - @override - ui.TextRange getLineBoundary(ui.TextPosition position) { - if (_hasLineMetrics) { - final EngineLineMetrics line = _getLineForIndex(position.offset); - return ui.TextRange(start: line.startIndex, end: line.endIndex); - } - return ui.TextRange.empty; - } - - @override - List computeLineMetrics() { - return _measurementResult!.lines!; - } -} - /// The web implementation of [ui.ParagraphStyle]. class EngineParagraphStyle implements ui.ParagraphStyle { /// Creates a new instance of [EngineParagraphStyle]. @@ -848,21 +265,6 @@ class EngineParagraphStyle implements ui.ParagraphStyle { ui.TextAlign get effectiveTextAlign => textAlign ?? ui.TextAlign.start; ui.TextDirection get effectiveTextDirection => textDirection ?? ui.TextDirection.ltr; - String get _effectiveFontFamily { - if (assertionsEnabled) { - // In the flutter tester environment, we use a predictable-size font - // "Ahem". This makes widget tests predictable and less flaky. - if (ui.debugEmulateFlutterTesterEnvironment) { - return 'Ahem'; - } - } - final String? fontFamily = this.fontFamily; - if (fontFamily == null || fontFamily.isEmpty) { - return DomRenderer.defaultFontFamily; - } - return fontFamily; - } - double? get lineHeight { // TODO(mdebbar): Implement proper support for strut styles. // https://github.com/flutter/flutter/issues/32243 @@ -1224,387 +626,6 @@ class EngineStrutStyle implements ui.StrutStyle { ); } -/// The web implementation of [ui.ParagraphBuilder]. -class DomParagraphBuilder implements ui.ParagraphBuilder { - /// Marks a call to the [pop] method in the [_ops] list. - static final Object _paragraphBuilderPop = Object(); - - final html.HtmlElement _paragraphElement = - domRenderer.createElement('p') as html.HtmlElement; - final EngineParagraphStyle _paragraphStyle; - final List _ops = []; - - /// Creates a [DomParagraphBuilder] object, which is used to create a - /// [DomParagraph]. - DomParagraphBuilder(EngineParagraphStyle style) : _paragraphStyle = style { - // TODO(mdebbar): Implement support for strut font families, b/128317744 - List strutFontFamilies; - if (style._strutStyle != null) { - strutFontFamilies = []; - if (style._strutStyle!._fontFamily != null) { - strutFontFamilies.add(style._strutStyle!._fontFamily); - } - if (style._strutStyle!._fontFamilyFallback != null) { - strutFontFamilies.addAll(style._strutStyle!._fontFamilyFallback!); - } - } - _applyParagraphStyleToElement( - element: _paragraphElement, style: _paragraphStyle); - } - - /// Applies the given style to the added text until [pop] is called. - /// - /// See [pop] for details. - @override - void pushStyle(ui.TextStyle style) { - _ops.add(style); - } - - @override - int get placeholderCount => _placeholderCount; - int _placeholderCount = 0; - - @override - List get placeholderScales => _placeholderScales; - final List _placeholderScales = []; - - @override - void addPlaceholder( - double width, - double height, - ui.PlaceholderAlignment alignment, { - double scale = 1.0, - double? baselineOffset, - ui.TextBaseline? baseline, - }) { - // Require a baseline to be specified if using a baseline-based alignment. - assert(!(alignment == ui.PlaceholderAlignment.aboveBaseline || - alignment == ui.PlaceholderAlignment.belowBaseline || - alignment == ui.PlaceholderAlignment.baseline) || baseline != null); - - _placeholderCount++; - _placeholderScales.add(scale); - _ops.add(ParagraphPlaceholder( - width * scale, - height * scale, - alignment, - baselineOffset: (baselineOffset ?? height) * scale, - baseline: baseline ?? ui.TextBaseline.alphabetic, - )); - } - - // TODO(yjbanov): do we need to do this? -// static String _encodeLocale(Locale locale) => locale?.toString() ?? ''; - - /// Ends the effect of the most recent call to [pushStyle]. - /// - /// Internally, the paragraph builder maintains a stack of text styles. Text - /// added to the paragraph is affected by all the styles in the stack. Calling - /// [pop] removes the topmost style in the stack, leaving the remaining styles - /// in effect. - @override - void pop() { - _ops.add(_paragraphBuilderPop); - } - - /// Adds the given text to the paragraph. - /// - /// The text will be styled according to the current stack of text styles. - @override - void addText(String text) { - _ops.add(text); - } - - /// Applies the given paragraph style and returns a [Paragraph] containing the - /// added text and associated styling. - /// - /// After calling this function, the paragraph builder object is invalid and - /// cannot be used further. - @override - EngineParagraph build() { - return _tryBuildPlainText() ?? _buildRichText(); - } - - /// Attempts to build a [Paragraph] assuming it is plain text. - /// - /// A paragraph is considered plain if it is built using the following - /// sequence of ops: - /// - /// * Zero-or-more calls to [pushStyle]. - /// * One-or-more calls to [addText]. - /// * Zero-or-more calls to [pop]. - /// - /// Any other sequence will result in `null` and should be treated as rich - /// text. - /// - /// Plain text is not the same as not having style. The text may be styled - /// arbitrarily. However, it may not mix multiple styles in the same - /// paragraph. Plain text is more efficient to lay out and measure than rich - /// text. - EngineParagraph? _tryBuildPlainText() { - ui.Color? color; - ui.TextDecoration? decoration; - ui.Color? decorationColor; - ui.TextDecorationStyle? decorationStyle; - double? decorationThickness; - ui.FontWeight? fontWeight = _paragraphStyle.fontWeight; - ui.FontStyle? fontStyle = _paragraphStyle.fontStyle; - ui.TextBaseline? textBaseline; - String fontFamily = - _paragraphStyle.fontFamily ?? DomRenderer.defaultFontFamily; - List? fontFamilyFallback; - List? fontFeatures; - double fontSize = _paragraphStyle.fontSize ?? DomRenderer.defaultFontSize; - final ui.TextAlign textAlign = _paragraphStyle.effectiveTextAlign; - final ui.TextDirection textDirection = _paragraphStyle.effectiveTextDirection; - double? letterSpacing; - double? wordSpacing; - double? height; - ui.Locale? locale = _paragraphStyle.locale; - ui.Paint? background; - ui.Paint? foreground; - List? shadows; - - int i = 0; - - // This loop looks expensive. However, in reality most of plain text - // paragraphs will have no calls to [pushStyle], skipping this loop - // entirely. Occasionally there will be one [pushStyle], which causes this - // loop to run once then move on to aggregating text. - while (i < _ops.length && _ops[i] is EngineTextStyle) { - final EngineTextStyle style = _ops[i] as EngineTextStyle; - if (style.color != null) { - color = style.color!; - } - if (style.decoration != null) { - decoration = style.decoration; - } - if (style.decorationColor != null) { - decorationColor = style.decorationColor; - } - if (style.decorationStyle != null) { - decorationStyle = style.decorationStyle; - } - if (style.decorationThickness != null) { - decorationThickness = style.decorationThickness; - } - if (style.fontWeight != null) { - fontWeight = style.fontWeight; - } - if (style.fontStyle != null) { - fontStyle = style.fontStyle; - } - if (style.textBaseline != null) { - textBaseline = style.textBaseline; - } - fontFamily = style.fontFamily; - if (style.fontFamilyFallback != null) { - fontFamilyFallback = style.fontFamilyFallback; - } - if (style.fontFeatures != null) { - fontFeatures = style.fontFeatures; - } - if (style.fontSize != null) { - fontSize = style.fontSize!; - } - if (style.letterSpacing != null) { - letterSpacing = style.letterSpacing; - } - if (style.wordSpacing != null) { - wordSpacing = style.wordSpacing; - } - if (style.height != null) { - height = style.height; - } - if (style.locale != null) { - locale = style.locale; - } - if (style.background != null) { - background = style.background; - } - if (style.foreground != null) { - foreground = style.foreground; - } - if (style.shadows != null) { - shadows = style.shadows; - } - i++; - } - - if (color == null && foreground == null) { - color = defaultTextColor; - } - - final EngineTextStyle cumulativeStyle = EngineTextStyle( - color: color, - decoration: decoration, - decorationColor: decorationColor, - decorationStyle: decorationStyle, - decorationThickness: decorationThickness, - fontWeight: fontWeight, - fontStyle: fontStyle, - textBaseline: textBaseline, - fontFamily: fontFamily, - fontFamilyFallback: fontFamilyFallback, - fontFeatures: fontFeatures, - fontSize: fontSize, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - height: height, - locale: locale, - background: background, - foreground: foreground, - shadows: shadows, - ); - - ui.Paint paint; - if (foreground != null) { - paint = foreground; - } else { - paint = ui.Paint(); - paint.color = color!; - } - - if (i >= _ops.length) { - // Empty paragraph. - applyTextStyleToElement( - element: _paragraphElement, style: cumulativeStyle); - return DomParagraph( - paragraphElement: _paragraphElement, - geometricStyle: ParagraphGeometricStyle( - textDirection: _paragraphStyle.effectiveTextDirection, - textAlign: _paragraphStyle.effectiveTextAlign, - fontFamily: fontFamily, - fontWeight: fontWeight, - fontStyle: fontStyle, - fontSize: fontSize, - lineHeight: height, - maxLines: _paragraphStyle.maxLines, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - decoration: _textDecorationToCssString(decoration, decorationStyle), - ellipsis: _paragraphStyle.ellipsis, - shadows: shadows, - ), - plainText: '', - paint: paint, - textAlign: textAlign, - textDirection: textDirection, - background: cumulativeStyle.background, - placeholderCount: placeholderCount, - ); - } - - if (_ops[i] is! String) { - // After a series of [EngineTextStyle] ops there must be at least one text op. - // Otherwise, treat it as rich text. - return null; - } - - // Accumulate text into one contiguous string. - final StringBuffer plainTextBuffer = StringBuffer(); - while (i < _ops.length && _ops[i] is String) { - plainTextBuffer.write(_ops[i]); - i++; - } - - // After a series of [addText] ops there should only be a tail of [pop]s and - // nothing else. Otherwise it's rich text and we return null; - for (; i < _ops.length; i++) { - if (_ops[i] != _paragraphBuilderPop) { - return null; - } - } - - final String plainText = plainTextBuffer.toString(); - domRenderer.appendText(_paragraphElement, plainText); - applyTextStyleToElement( - element: _paragraphElement, style: cumulativeStyle); - // Since this is a plain paragraph apply background color to paragraph tag - // instead of individual spans. - if (cumulativeStyle.background != null) { - _applyTextBackgroundToElement( - element: _paragraphElement, style: cumulativeStyle); - } - return DomParagraph( - paragraphElement: _paragraphElement, - geometricStyle: ParagraphGeometricStyle( - textDirection: _paragraphStyle.effectiveTextDirection, - textAlign: _paragraphStyle.effectiveTextAlign, - fontFamily: fontFamily, - fontWeight: fontWeight, - fontStyle: fontStyle, - fontSize: fontSize, - lineHeight: height, - maxLines: _paragraphStyle.maxLines, - letterSpacing: letterSpacing, - wordSpacing: wordSpacing, - decoration: _textDecorationToCssString(decoration, decorationStyle), - ellipsis: _paragraphStyle.ellipsis, - shadows: shadows, - ), - plainText: plainText, - paint: paint, - textAlign: textAlign, - textDirection: textDirection, - background: cumulativeStyle.background, - placeholderCount: placeholderCount, - ); - } - - /// Builds a [Paragraph] as rich text. - EngineParagraph _buildRichText() { - final List elementStack = []; - html.Element currentElement() => - elementStack.isNotEmpty ? elementStack.last : _paragraphElement; - - for (int i = 0; i < _ops.length; i++) { - final dynamic op = _ops[i]; - if (op is EngineTextStyle) { - final html.SpanElement span = domRenderer.createElement('span') as html.SpanElement; - applyTextStyleToElement(element: span, style: op, isSpan: true); - if (op.background != null) { - _applyTextBackgroundToElement(element: span, style: op); - } - domRenderer.append(currentElement(), span); - elementStack.add(span); - } else if (op is String) { - domRenderer.appendText(currentElement(), op); - } else if (op is ParagraphPlaceholder) { - domRenderer.append( - currentElement(), - createPlaceholderElement(placeholder: op), - ); - } else if (identical(op, _paragraphBuilderPop)) { - elementStack.removeLast(); - } else { - throw UnsupportedError('Unsupported ParagraphBuilder operation: $op'); - } - } - - return DomParagraph( - paragraphElement: _paragraphElement, - geometricStyle: ParagraphGeometricStyle( - textDirection: _paragraphStyle.effectiveTextDirection, - textAlign: _paragraphStyle.effectiveTextAlign, - fontFamily: _paragraphStyle.fontFamily, - fontWeight: _paragraphStyle.fontWeight, - fontStyle: _paragraphStyle.fontStyle, - fontSize: _paragraphStyle.fontSize, - lineHeight: _paragraphStyle.height, - maxLines: _paragraphStyle.maxLines, - ellipsis: _paragraphStyle.ellipsis, - ), - plainText: null, - paint: null, - textAlign: _paragraphStyle.effectiveTextAlign, - textDirection: _paragraphStyle.effectiveTextDirection, - background: null, - placeholderCount: placeholderCount, - ); - } -} - /// Holds information for a placeholder in a paragraph. /// /// [width], [height] and [baselineOffset] are expected to be already scaled. @@ -1675,40 +696,6 @@ String fontWeightIndexToCss({int fontWeightIndex = 3}) { return ''; } -/// Applies a paragraph [style] to an [element], translating the properties to -/// their corresponding CSS equivalents. -void _applyParagraphStyleToElement({ - required html.HtmlElement element, - required EngineParagraphStyle style, -}) { - assert(element != null); // ignore: unnecessary_null_comparison - assert(style != null); // ignore: unnecessary_null_comparison - // TODO(yjbanov): What do we do about ParagraphStyle._locale and ellipsis? - final html.CssStyleDeclaration cssStyle = element.style; - - if (style.textAlign != null) { - cssStyle.textAlign = textAlignToCssValue( - style.textAlign, style.textDirection ?? ui.TextDirection.ltr); - } - if (style.lineHeight != null) { - cssStyle.lineHeight = '${style.lineHeight}'; - } - if (style.textDirection != null) { - cssStyle.direction = textDirectionToCss(style.textDirection); - } - if (style.fontSize != null) { - cssStyle.fontSize = '${style.fontSize!.floor()}px'; - } - if (style.fontWeight != null) { - cssStyle.fontWeight = fontWeightToCss(style.fontWeight); - } - if (style.fontStyle != null) { - cssStyle.fontStyle = - style.fontStyle == ui.FontStyle.normal ? 'normal' : 'italic'; - } - cssStyle.fontFamily = canonicalizeFontFamily(style._effectiveFontFamily); -} - /// Applies a text [style] to an [element], translating the properties to their /// corresponding CSS equivalents. /// @@ -1794,7 +781,6 @@ html.Element createPlaceholderElement({ required ParagraphPlaceholder placeholder, }) { final html.Element element = domRenderer.createElement('span'); - element.className = placeholderClass; final html.CssStyleDeclaration style = element.style; style ..display = 'inline-block' @@ -1875,19 +861,6 @@ String _fontFeatureListToCss(List fontFeatures) { return sb.toString(); } -/// Applies background color properties in text style to paragraph or span -/// elements. -void _applyTextBackgroundToElement({ - required html.HtmlElement element, - required EngineTextStyle style, -}) { - final ui.Paint? newBackground = style.background; - if (newBackground != null) { - DomRenderer.setElementStyle( - element, 'background-color', colorToCssString(newBackground.color)); - } -} - /// Converts text decoration style to CSS text-decoration-style value. String? _textDecorationToCssString( ui.TextDecoration? decoration, ui.TextDecorationStyle? decorationStyle) { diff --git a/lib/web_ui/lib/src/engine/text/ruler.dart b/lib/web_ui/lib/src/engine/text/ruler.dart index dc12dbb66063c..79d1d2d20445c 100644 --- a/lib/web_ui/lib/src/engine/text/ruler.dart +++ b/lib/web_ui/lib/src/engine/text/ruler.dart @@ -4,7 +4,6 @@ import 'dart:html' as html; -import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; @@ -247,73 +246,6 @@ class TextDimensions { final html.HtmlElement _element; html.Rectangle? _cachedBoundingClientRect; - /// Attempts to efficiently copy text from [from]. - /// - /// The primary efficiency gain is from rare occurrence of rich text in - /// typical apps. - void updateText(DomParagraph from, ParagraphGeometricStyle style) { - assert(from != null); // ignore: unnecessary_null_comparison - assert(_element != null); // ignore: unnecessary_null_comparison - assert(from.debugHasSameRootStyle(style)); - assert(() { - final bool wasEmptyOrPlainText = _element.childNodes.isEmpty || - (_element.childNodes.length == 1 && - _element.childNodes.first is html.Text); - if (!wasEmptyOrPlainText) { - throw Exception( - 'Failed to copy text into the paragraph measuring element. The ' - 'element already contains rich text "${_element.innerHtml}". It is ' - 'likely that a previous measurement did not clean up after ' - 'itself.'); - } - return true; - }()); - - _invalidateBoundsCache(); - final String? plainText = from.plainText; - if (plainText != null) { - // Plain text: just set the string. The paragraph's style is assumed to - // match the style set on the `element`. Setting text as plain string is - // faster because it doesn't change the DOM structure or CSS attributes, - // and therefore doesn't trigger style recalculations in the browser. - if (plainText.endsWith('\n')) { - // On the web the last newline is ignored. To be consistent with - // native engine implementation we add extra newline to get correct - // height measurement. - _element.text = '$plainText\n'; - } else { - _element.text = plainText; - } - } else { - // Rich text: deeply copy contents. This is the slow case that should be - // avoided if fast layout performance is desired. - final html.Element copy = from.paragraphElement.clone(true) as html.Element; - _element.nodes.addAll(copy.nodes); - } - } - - /// Updated element style width. - void updateConstraintWidth(double width, String? ellipsis) { - _invalidateBoundsCache(); - - if (width.isInfinite) { - _element.style - ..width = null - ..whiteSpace = 'pre'; - } else if (ellipsis != null) { - // Width is finite, but we don't want to let the text soft-wrap when - // ellipsis overflow is enabled. - _element.style - ..width = '${width}px' - ..whiteSpace = 'pre'; - } else { - // Width is finite and there's no ellipsis overflow. - _element.style - ..width = '${width}px' - ..whiteSpace = 'pre-wrap'; - } - } - void _invalidateBoundsCache() { _cachedBoundingClientRect = null; } @@ -324,36 +256,6 @@ class TextDimensions { _element.text = ' '; } - /// Applies geometric style properties to the [element]. - void applyStyle(ParagraphGeometricStyle style) { - final html.CssStyleDeclaration elementStyle = _element.style; - elementStyle - ..direction = textDirectionToCss(style.textDirection) - ..textAlign = textAlignToCssValue(style.textAlign, style.textDirection) - ..fontSize = style.fontSize != null ? '${style.fontSize!.floor()}px' : null - ..fontFamily = canonicalizeFontFamily(style.effectiveFontFamily) - ..fontWeight = - style.fontWeight != null ? fontWeightToCss(style.fontWeight) : null - ..fontStyle = style.fontStyle != null - ? style.fontStyle == ui.FontStyle.normal ? 'normal' : 'italic' - : null - ..letterSpacing = - style.letterSpacing != null ? '${style.letterSpacing}px' : null - ..wordSpacing = - style.wordSpacing != null ? '${style.wordSpacing}px' : null; - final String? decoration = style.decoration; - if (browserEngine == BrowserEngine.webkit) { - DomRenderer.setElementStyle( - _element, '-webkit-text-decoration', decoration); - } else { - elementStyle.textDecoration = decoration; - } - if (style.lineHeight != null) { - elementStyle.lineHeight = style.lineHeight!.toString(); - } - _invalidateBoundsCache(); - } - void applyHeightStyle(TextHeightStyle textHeightStyle) { final String fontFamily = textHeightStyle.fontFamily; final double fontSize = textHeightStyle.fontSize; @@ -379,9 +281,6 @@ class TextDimensions { html.Rectangle _readAndCacheMetrics() => _cachedBoundingClientRect ??= _element.getBoundingClientRect(); - /// The width of the paragraph being measured. - double get width => _readAndCacheMetrics().width as double; - /// The height of the paragraph being measured. double get height { double cachedHeight = _readAndCacheMetrics().height as double; @@ -464,624 +363,3 @@ class TextHeightRuler { return probe; } } - -/// Performs 4 types of measurements: -/// -/// 1. Single line: can be prepared by calling [measureAsSingleLine]. -/// Measurement values will be available at [singleLineDimensions]. -/// -/// 2. Minimum intrinsic width: can be prepared by calling -/// [measureMinIntrinsicWidth]. Measurement values will be available at -/// [minIntrinsicDimensions]. -/// -/// 3. Constrained: can be prepared by calling [measureWithConstraints] and -/// passing the constraints. Measurement values will be available at -/// [constrainedDimensions]. -/// -/// 4. Boxes: within a paragraph, it measures a list of text boxes that enclose -/// a given range of text. -/// -/// For performance reasons, it's advised to use [measureAll] and then reading -/// whatever measurements are needed. This causes the browser to only reflow -/// once instead of many times. -/// -/// The [measureAll] method performs the first 3 stateful measurements but not -/// the 4th one. -/// -/// This class is both reusable and stateful. Use it carefully. The correct -/// usage is as follows: -/// -/// * First, call [willMeasure] passing it the paragraph to be measured. -/// * Call any of the [measureAsSingleLine], [measureMinIntrinsicWidth], -/// [measureWithConstraints], or [measureAll], to prepare the respective -/// measurement. These methods can be called any number of times. -/// * Call [didMeasure] to indicate that you are done with the paragraph passed -/// to the [willMeasure] method. -/// -/// It is safe to reuse this object as long as paragraphs passed to the -/// [measure] method have the same style. -/// -/// The only stateless method provided by this class is [measureBoxesForRange] -/// that doesn't rely on [willMeasure] and [didMeasure] lifecycle methods. -/// -/// This class optimizes for plain text paragraphs, which should constitute the -/// majority of paragraphs in typical apps. -class ParagraphRuler { - /// The only style that this [ParagraphRuler] measures text. - final ParagraphGeometricStyle style; - - /// A [RulerManager] owns the host DOM element that this ruler can add - /// elements to. - /// - /// The [rulerManager] keeps a cache of multiple [ParagraphRuler] instances, - /// but a [ParagraphRuler] can only belong to one [RulerManager]. - final RulerManager rulerManager; - - ParagraphRuler(this.style, this.rulerManager) { - _configureSingleLineHostElements(); - _configureMinIntrinsicHostElements(); - _configureConstrainedHostElements(); - } - - /// The alphabetic baseline of the paragraph being measured. - double get alphabeticBaseline => _textHeightRuler.alphabeticBaseline; - - // Elements used to measure single-line metrics. - final html.DivElement _singleLineHost = html.DivElement(); - final TextDimensions singleLineDimensions = - TextDimensions(html.ParagraphElement()); - - // Elements used to measure minIntrinsicWidth. - final html.DivElement _minIntrinsicHost = html.DivElement(); - TextDimensions minIntrinsicDimensions = - TextDimensions(html.ParagraphElement()); - - // Elements used to measure metrics under a width constraint. - final html.DivElement _constrainedHost = html.DivElement(); - TextDimensions constrainedDimensions = - TextDimensions(html.ParagraphElement()); - - // Elements used to measure the line-height metric. - late final TextHeightRuler _textHeightRuler = - TextHeightRuler(style.textHeightStyle, rulerManager); - double get lineHeight { - return _textHeightRuler.height; - } - - /// The number of times this ruler was used this frame. - /// - /// This value is used to determine which rulers are rarely used and should be - /// evicted from the ruler cache. - int get hitCount => _hitCount; - int _hitCount = 0; - - /// This method should be called whenever this ruler is being used to perform - /// measurements. - /// - /// It increases the hit count of this ruler which is used when clearing the - /// [rulerManager]'s cache to find the least used rulers. - void hit() { - _hitCount++; - } - - /// Resets the hit count back to zero. - void resetHitCount() { - _hitCount = 0; - } - - /// Makes sure this ruler is not used again after it has been disposed of, - /// which would indicate a bug. - @visibleForTesting - bool get debugIsDisposed => _debugIsDisposed; - bool _debugIsDisposed = false; - - void _configureSingleLineHostElements() { - _singleLineHost.style - ..visibility = 'hidden' - ..position = 'absolute' - ..top = '0' // this is important as baseline == probe.bottom - ..left = '0' - ..display = 'flex' - ..flexDirection = 'row' - ..alignItems = 'baseline' - ..margin = '0' - ..border = '0' - ..padding = '0'; - - if (assertionsEnabled) { - _singleLineHost.setAttribute('data-ruler', 'single-line'); - } - - singleLineDimensions.applyStyle(style); - - // Force single-line (even if wider than screen) and preserve whitespaces. - singleLineDimensions._element.style.whiteSpace = 'pre'; - - singleLineDimensions.appendToHost(_singleLineHost); - rulerManager.addElement(_singleLineHost); - } - - void _configureMinIntrinsicHostElements() { - // Configure min intrinsic host elements. - _minIntrinsicHost.style - ..visibility = 'hidden' - ..position = 'absolute' - ..top = '0' // this is important as baseline == probe.bottom - ..left = '0' - ..display = 'flex' - ..flexDirection = 'row' - ..margin = '0' - ..border = '0' - ..padding = '0'; - - if (assertionsEnabled) { - _minIntrinsicHost.setAttribute('data-ruler', 'min-intrinsic'); - } - - minIntrinsicDimensions.applyStyle(style); - - // "flex: 0" causes the paragraph element to shrink horizontally, exposing - // its minimum intrinsic width. - minIntrinsicDimensions._element.style - ..flex = '0' - ..display = 'inline' - // Preserve newlines, wrap text, remove end of line spaces. - // Not using pre-wrap here since end of line space hang measurement - // changed in Chrome 77 Beta. - ..whiteSpace = 'pre-line'; - - _minIntrinsicHost.append(minIntrinsicDimensions._element); - rulerManager.addElement(_minIntrinsicHost); - } - - void _configureConstrainedHostElements() { - _constrainedHost.style - ..visibility = 'hidden' - ..position = 'absolute' - ..top = '0' // this is important as baseline == probe.bottom - ..left = '0' - ..display = 'flex' - ..flexDirection = 'row' - ..alignItems = 'baseline' - ..margin = '0' - ..border = '0' - ..padding = '0'; - - if (assertionsEnabled) { - _constrainedHost.setAttribute('data-ruler', 'constrained'); - } - - constrainedDimensions.applyStyle(style); - final html.CssStyleDeclaration elementStyle = - constrainedDimensions._element.style; - elementStyle - ..display = 'block' - ..overflowWrap = 'break-word'; - - if (style.ellipsis != null) { - elementStyle - ..overflow = 'hidden' - ..textOverflow = 'ellipsis'; - } - - constrainedDimensions.appendToHost(_constrainedHost); - rulerManager.addElement(_constrainedHost); - } - - /// The paragraph being measured. - DomParagraph? _paragraph; - - /// Prepares this ruler for measuring the given [paragraph]. - /// - /// This method must be called before calling any of the `measure*` methods. - void willMeasure(DomParagraph paragraph) { - assert(paragraph != null); // ignore: unnecessary_null_comparison - assert(() { - if (_paragraph != null) { - throw Exception( - 'Attempted to reuse a $ParagraphRuler but it is currently ' - 'measuring another paragraph ($_paragraph). It is possible that '); - } - return true; - }()); - assert(paragraph.debugHasSameRootStyle(style)); - _paragraph = paragraph; - } - - /// Prepares all 3 measurements: - /// 1. single line. - /// 2. minimum intrinsic width. - /// 3. constrained. - void measureAll(ui.ParagraphConstraints constraints) { - measureAsSingleLine(); - measureMinIntrinsicWidth(); - measureWithConstraints(constraints); - } - - /// Lays out the paragraph in a single line, giving it infinite amount of - /// horizontal space. - /// - /// Measures [width], [height], and [alphabeticBaseline]. - void measureAsSingleLine() { - assert(!_debugIsDisposed); - assert(_paragraph != null); - - // HACK(mdebbar): TextField uses an empty string to measure the line height, - // which doesn't work. So we need to replace it with a whitespace. The - // correct fix would be to do line height and baseline measurements and - // cache them separately. - if (_paragraph!.plainText == '') { - singleLineDimensions.updateTextToSpace(); - } else { - singleLineDimensions.updateText(_paragraph!, style); - } - } - - /// Lays out the paragraph inside a flex row and sets "flex: 0", which - /// squeezes the paragraph, forcing it to occupy minimum intrinsic width. - /// - /// Measures [width] and [height]. - void measureMinIntrinsicWidth() { - assert(!_debugIsDisposed); - assert(_paragraph != null); - - minIntrinsicDimensions.updateText(_paragraph!, style); - } - - /// Lays out the paragraph giving it a width constraint. - /// - /// Measures [width], [height], and [alphabeticBaseline]. - void measureWithConstraints(ui.ParagraphConstraints constraints) { - assert(!_debugIsDisposed); - assert(_paragraph != null); - - constrainedDimensions.updateText(_paragraph!, style); - - // The extra 0.5 is because sometimes the browser needs slightly more space - // than the size it reports back. When that happens the text may be wrap - // when we thought it didn't. - constrainedDimensions.updateConstraintWidth( - constraints.width + 0.5, - style.ellipsis, - ); - } - - List measurePlaceholderBoxes() { - assert(!_debugIsDisposed); - - final DomParagraph? paragraph = _paragraph; - assert(paragraph != null); - - if (paragraph!.placeholderCount == 0) { - return const []; - } - - final List placeholderElements = - constrainedDimensions._element.querySelectorAll('.$placeholderClass'); - final List boxes = []; - - for (final html.Element element in placeholderElements) { - final html.Rectangle rect = element.getBoundingClientRect(); - boxes.add(ui.TextBox.fromLTRBD( - rect.left as double, - rect.top as double, - rect.right as double, - rect.bottom as double, - paragraph.textDirection, - )); - } - return boxes; - } - - /// Returns text position in a paragraph that contains multiple - /// nested spans given an offset. - int hitTest(ui.ParagraphConstraints constraints, ui.Offset offset) { - measureWithConstraints(constraints); - // Get paragraph element root used to measure constrainedDimensions. - final html.HtmlElement el = constrainedDimensions._element; - final List textNodes = []; - // Collect all text nodes (breadth first traversal). - // Since there is no api to get bounds of text nodes directly we work - // upwards and measure span elements and finally the paragraph. - _collectTextNodes(el.childNodes, textNodes); - // Hit test spans starting from leaf nodes up (backwards). - for (int i = textNodes.length - 1; i >= 0; i--) { - final html.Node node = textNodes[i]; - // Check if offset is within client rect bounds of text node's - // parent element. - final html.Element parent = node.parentNode! as html.Element; - final html.Rectangle bounds = parent.getBoundingClientRect(); - final double dx = offset.dx; - final double dy = offset.dy; - if (dx >= bounds.left && - dx < bounds.right && - dy >= bounds.top && - dy < bounds.bottom) { - // We found the element bounds that contains offset. - // Calculate text position for this node. - return _countTextPosition(el.childNodes, textNodes[i]); - } - } - return 0; - } - - void _collectTextNodes(Iterable nodes, List textNodes) { - if (nodes.isEmpty) { - return; - } - final List childNodes = []; - for (final html.Node node in nodes) { - if (node.nodeType == html.Node.TEXT_NODE) { - textNodes.add(node); - } - childNodes.addAll(node.childNodes); - } - _collectTextNodes(childNodes, textNodes); - } - - int _countTextPosition(List nodes, html.Node endNode) { - int position = 0; - final List stack = nodes.reversed.toList(); - while (true) { - final html.Node node = stack.removeLast(); - stack.addAll(node.childNodes.reversed); - if (node == endNode) { - break; - } - if (node.nodeType == html.Node.TEXT_NODE) { - position += node.text!.length; - } - } - return position; - } - - /// Performs clean-up after a measurement is done, preparing this ruler for - /// a future reuse. - /// - /// Call this method immediately after calling `measure*` methods for a - /// particular [paragraph]. This ruler is not reusable until [didMeasure] is - /// called. - void didMeasure() { - assert(_paragraph != null); - // Remove any rich text we set during layout for the following reasons: - // - there won't be any text for the browser to lay out when we commit the - // current frame. - // - this keeps the cost of removing content together with the measurement - // in the profile. Otherwise, the cost of removing will be paid by a - // random next paragraph measured in the future, and make the performance - // profile hard to understand. - // - // We do not do this for plain text, because replacing plain text is more - // expensive than paying the cost of the DOM mutation to clean it. - if (_paragraph!.plainText == null) { - domRenderer - ..clearDom(singleLineDimensions._element) - ..clearDom(minIntrinsicDimensions._element) - ..clearDom(constrainedDimensions._element); - } - _paragraph = null; - } - - /// Performs stateless measurement of text boxes for a given range of text. - /// - /// This method doesn't depend on [willMeasure] and [didMeasure] lifecycle - /// methods. - List measureBoxesForRange( - String plainText, - ui.ParagraphConstraints constraints, { - required int start, - required int end, - required double alignOffset, - required ui.TextDirection textDirection, - }) { - assert(!_debugIsDisposed); - assert(start >= 0 && start <= plainText.length); - assert(end >= 0 && end <= plainText.length); - assert(start <= end); - - final String before = plainText.substring(0, start); - final String rangeText = plainText.substring(start, end); - final String after = plainText.substring(end); - - final html.SpanElement rangeSpan = html.SpanElement()..text = rangeText; - - // Setup the [ruler.constrainedDimensions] element to be used for measurement. - domRenderer.clearDom(constrainedDimensions._element); - constrainedDimensions._element - ..appendText(before) - ..append(rangeSpan) - ..appendText(after); - constrainedDimensions.updateConstraintWidth(constraints.width, null); - - // Measure the rects of [rangeSpan]. - final List> clientRects = rangeSpan.getClientRects(); - final List boxes = []; - - final double maxLinesLimit = style.maxLines == null - ? double.infinity - : style.maxLines! * lineHeight; - - html.Rectangle? previousRect; - for (final html.Rectangle rect in clientRects) { - // If [rect] is an empty box on the same line as the previous box, don't - // include it in the result. - if (rect.top == previousRect?.top && rect.left == rect.right) { - continue; - } - // As soon as we go beyond [maxLines], stop adding boxes. - if (rect.top >= maxLinesLimit) { - break; - } - - boxes.add(ui.TextBox.fromLTRBD( - rect.left.toDouble() + alignOffset, - rect.top as double, - rect.right.toDouble() + alignOffset, - rect.bottom as double, - textDirection, - )); - previousRect = rect; - } - - // Cleanup after measuring the boxes. - domRenderer.clearDom(constrainedDimensions._element); - return boxes; - } - - /// Detaches this ruler from the DOM and makes it unusable for future - /// measurements. - /// - /// Disposed rulers should be garbage collected after calling this method. - void dispose() { - assert(() { - if (_paragraph != null) { - throw Exception('Attempted to dispose of a ruler in the middle of ' - 'measurement. This is likely a bug in the framework.'); - } - return true; - }()); - _singleLineHost.remove(); - _minIntrinsicHost.remove(); - _constrainedHost.remove(); - _textHeightRuler.dispose(); - assert(() { - _debugIsDisposed = true; - return true; - }()); - } - - // Bounded cache for text measurement for a particular width constraint. - Map> _measurementCache = - >{}; - // Mru list for cache. - final List _mruList = []; - static const int _cacheLimit = 2400; - // Number of items to evict when cache limit is reached. - static const int _cacheBlockFactor = 100; - // Number of constraint results per unique text item. - // This limit prevents growth during animation where the size of a container - // is changing. - static const int _constraintCacheSize = 8; - - void cacheMeasurement(DomParagraph paragraph, MeasurementResult? item) { - final String? plainText = paragraph.plainText; - final List constraintCache = - _measurementCache[plainText] ??= []; - constraintCache.add(item); - if (constraintCache.length > _constraintCacheSize) { - constraintCache.removeAt(0); - } - _mruList.add(plainText); - if (_mruList.length > _cacheLimit) { - // Evict a range. - for (int i = 0; i < _cacheBlockFactor; i++) { - _measurementCache.remove(_mruList[i]); - } - _mruList.removeRange(0, _cacheBlockFactor); - } - } - - MeasurementResult? cacheLookup( - DomParagraph paragraph, ui.ParagraphConstraints constraints) { - final String? plainText = paragraph.plainText; - if (plainText == null) { - // Multi span paragraph, do not use cache item. - return null; - } - final List? constraintCache = - _measurementCache[plainText]; - if (constraintCache == null) { - return null; - } - final int len = constraintCache.length; - for (int i = 0; i < len; i++) { - final MeasurementResult item = constraintCache[i]!; - if (item.constraintWidth == constraints.width && - item.textAlign == paragraph.textAlign && - item.textDirection == paragraph.textDirection) { - return item; - } - } - return null; - } -} - -/// The result that contains all measurements of a paragraph at the given -/// constraint width. -@immutable -class MeasurementResult { - /// The width that was given as a constraint when the paragraph was laid out. - final double constraintWidth; - - /// Whether the paragraph can fit in a single line given [constraintWidth]. - final bool isSingleLine; - - /// The amount of horizontal space the paragraph occupies. - final double width; - - /// The amount of vertical space the paragraph occupies. - final double height; - - /// {@macro dart.ui.paragraph.naturalHeight} - /// - /// When [ParagraphGeometricStyle.maxLines] is null, [naturalHeight] and - /// [height] should be equal. - final double naturalHeight; - - /// The amount of vertical space each line of the paragraph occupies. - /// - /// In some cases, measuring [lineHeight] is unnecessary, so it's nullable. If - /// present, it should be equal to [height] when [isSingleLine] is true. - final double? lineHeight; - - /// {@macro dart.ui.paragraph.minIntrinsicWidth} - final double minIntrinsicWidth; - - /// {@macro dart.ui.paragraph.maxIntrinsicWidth} - final double maxIntrinsicWidth; - - /// {@macro dart.ui.paragraph.alphabeticBaseline} - final double alphabeticBaseline; - - /// {@macro dart.ui.paragraph.ideographicBaseline} - final double ideographicBaseline; - - /// The full list of [EngineLineMetrics] that describe in detail the various metrics - /// of each laid out line. - final List? lines; - - final List placeholderBoxes; - - /// The text align value of the paragraph. - final ui.TextAlign textAlign; - - /// The text direction of the paragraph. - final ui.TextDirection textDirection; - - const MeasurementResult( - this.constraintWidth, { - required this.isSingleLine, - required this.width, - required this.height, - required this.naturalHeight, - required this.lineHeight, - required this.minIntrinsicWidth, - required this.maxIntrinsicWidth, - required this.alphabeticBaseline, - required this.ideographicBaseline, - required this.lines, - required this.placeholderBoxes, - required ui.TextAlign? textAlign, - required ui.TextDirection? textDirection, - }) : assert(constraintWidth != null), // ignore: unnecessary_null_comparison - assert(isSingleLine != null), // ignore: unnecessary_null_comparison - assert(width != null), // ignore: unnecessary_null_comparison - assert(height != null), // ignore: unnecessary_null_comparison - assert(naturalHeight != null), // ignore: unnecessary_null_comparison - assert(minIntrinsicWidth != null), // ignore: unnecessary_null_comparison - assert(maxIntrinsicWidth != null), // ignore: unnecessary_null_comparison - assert(alphabeticBaseline != null), // ignore: unnecessary_null_comparison - assert(ideographicBaseline != null), // ignore: unnecessary_null_comparison - this.textAlign = textAlign ?? ui.TextAlign.start,// ignore: unnecessary_this - this.textDirection = textDirection ?? ui.TextDirection.ltr;// ignore: unnecessary_this -} diff --git a/lib/web_ui/lib/src/engine/web_experiments.dart b/lib/web_ui/lib/src/engine/web_experiments.dart index 2d84c7ed040df..7dea7f8e98622 100644 --- a/lib/web_ui/lib/src/engine/web_experiments.dart +++ b/lib/web_ui/lib/src/engine/web_experiments.dart @@ -19,53 +19,15 @@ class WebExperiments { } static WebExperiments ensureInitialized() { - return WebExperiments.instance ?? (WebExperiments.instance = WebExperiments._()); + return WebExperiments.instance ?? + (WebExperiments.instance = WebExperiments._()); } static WebExperiments? instance; - /// Experiment flag for using canvas-based text measurement. - bool get useCanvasText => _useCanvasText; - set useCanvasText(bool? enabled) { - _useCanvasText = enabled ?? _defaultUseCanvasText; - } - - static const bool _defaultUseCanvasText = bool.fromEnvironment( - 'FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT', - defaultValue: true, - ); - - bool _useCanvasText = _defaultUseCanvasText; - - // TODO(mdebbar): Clean up https://github.com/flutter/flutter/issues/71952 - /// Experiment flag for using canvas-based measurement for rich text. - bool get useCanvasRichText => _useCanvasRichText; - set useCanvasRichText(bool? enabled) { - _useCanvasRichText = enabled ?? _defaultUseCanvasRichText; - } - - static const bool _defaultUseCanvasRichText = bool.fromEnvironment( - 'FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_RICH_TEXT', - defaultValue: true, - ); - - bool _useCanvasRichText = _defaultUseCanvasRichText; - /// Reset all experimental flags to their default values. - void reset() { - _useCanvasText = _defaultUseCanvasText; - _useCanvasRichText = _defaultUseCanvasRichText; - } + void reset() {} /// Used to enable/disable experimental flags in the web engine. - void updateExperiment(String name, bool? enabled) { - switch (name) { - case 'useCanvasText': - useCanvasText = enabled; - break; - case 'useCanvasRichText': - useCanvasRichText = enabled; - break; - } - } + void updateExperiment(String name, bool? enabled) {} } diff --git a/lib/web_ui/lib/src/ui/text.dart b/lib/web_ui/lib/src/ui/text.dart index bfcf7e251d8e5..a076c6bcd8fe4 100644 --- a/lib/web_ui/lib/src/ui/text.dart +++ b/lib/web_ui/lib/src/ui/text.dart @@ -706,12 +706,8 @@ abstract class ParagraphBuilder { factory ParagraphBuilder(ParagraphStyle style) { if (engine.useCanvasKit) { return engine.CkParagraphBuilder(style); - } else if (engine.WebExperiments.instance!.useCanvasRichText) { - return engine.CanvasParagraphBuilder( - style as engine.EngineParagraphStyle); - } else { - return engine.DomParagraphBuilder(style as engine.EngineParagraphStyle); } + return engine.CanvasParagraphBuilder(style as engine.EngineParagraphStyle); } void pushStyle(TextStyle style); void pop(); diff --git a/lib/web_ui/test/paragraph_test.dart b/lib/web_ui/test/paragraph_test.dart deleted file mode 100644 index a84a28357efeb..0000000000000 --- a/lib/web_ui/test/paragraph_test.dart +++ /dev/null @@ -1,1001 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' hide window; - - -void testEachMeasurement(String description, VoidCallback body, {bool? skip}) { - test('$description (dom measurement)', () async { - try { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - WebExperiments.instance!.useCanvasText = false; - WebExperiments.instance!.useCanvasRichText = false; - return body(); - } finally { - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - TextMeasurementService.clearCache(); - } - }, skip: skip); - test('$description (canvas measurement)', () async { - try { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - WebExperiments.instance!.useCanvasText = true; - WebExperiments.instance!.useCanvasRichText = false; - return body(); - } finally { - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - TextMeasurementService.clearCache(); - } - }, skip: skip); -} - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -Future testMain() async { - await webOnlyInitializeTestDomRenderer(); - - // Ahem font uses a constant ideographic/alphabetic baseline ratio. - const double kAhemBaselineRatio = 1.25; - - testEachMeasurement('predictably lays out a single-line paragraph', () { - for (final double fontSize in [10.0, 20.0, 30.0, 40.0]) { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: fontSize, - )); - builder.addText('Test'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 400.0)); - - expect(paragraph.height, closeTo(fontSize, 0.001)); - expect(paragraph.width, closeTo(400.0, 0.001)); - expect(paragraph.minIntrinsicWidth, closeTo(fontSize * 4.0, 0.001)); - expect(paragraph.maxIntrinsicWidth, closeTo(fontSize * 4.0, 0.001)); - expect(paragraph.alphabeticBaseline, closeTo(fontSize * .8, 0.001)); - expect( - paragraph.ideographicBaseline, - closeTo(paragraph.alphabeticBaseline * kAhemBaselineRatio, 3.0), - ); - } - }); - - testEachMeasurement('predictably lays out a multi-line paragraph', () { - for (final double fontSize in [10.0, 20.0, 30.0, 40.0]) { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: fontSize, - )); - builder.addText('Test Ahem'); - final Paragraph paragraph = builder.build(); - paragraph.layout(ParagraphConstraints(width: fontSize * 5.0)); - - expect( - paragraph.height, closeTo(fontSize * 2.0, 0.001)); // because it wraps - expect(paragraph.width, closeTo(fontSize * 5.0, 0.001)); - expect(paragraph.minIntrinsicWidth, closeTo(fontSize * 4.0, 0.001)); - - // TODO(yjbanov): see https://github.com/flutter/flutter/issues/21965 - expect(paragraph.maxIntrinsicWidth, closeTo(fontSize * 9.0, 0.001)); - expect(paragraph.alphabeticBaseline, closeTo(fontSize * .8, 0.001)); - expect( - paragraph.ideographicBaseline, - closeTo(paragraph.alphabeticBaseline * kAhemBaselineRatio, 3.0), - ); - } - }); - - testEachMeasurement('predictably lays out a single-line rich paragraph', () { - for (final double fontSize in [10.0, 20.0, 30.0, 40.0]) { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: fontSize, - )); - builder.addText('span1'); - builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); - builder.addText('span2'); - final Paragraph paragraph = builder.build(); - paragraph.layout(ParagraphConstraints(width: fontSize * 10.0)); - - expect(paragraph.height, fontSize); - expect(paragraph.width, fontSize * 10.0); - expect(paragraph.minIntrinsicWidth, fontSize * 10.0); - expect(paragraph.maxIntrinsicWidth, fontSize * 10.0); - } - }, - // TODO(mdebbar): https://github.com/flutter/flutter/issues/50771 - // TODO(mdebbar): https://github.com/flutter/flutter/issues/46638 - // TODO(mdebbar): https://github.com/flutter/flutter/issues/50590 - skip: browserEngine != BrowserEngine.blink); - - testEachMeasurement('predictably lays out a multi-line rich paragraph', () { - for (final double fontSize in [10.0, 20.0, 30.0, 40.0]) { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: fontSize, - )); - builder.addText('12345 '); - builder.addText('67890 '); - builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); - builder.addText('bold'); - final Paragraph paragraph = builder.build(); - paragraph.layout(ParagraphConstraints(width: fontSize * 5.0)); - - expect(paragraph.height, fontSize * 3.0); // because it wraps - expect(paragraph.width, fontSize * 5.0); - expect(paragraph.minIntrinsicWidth, fontSize * 5.0); - expect(paragraph.maxIntrinsicWidth, fontSize * 16.0); - } - }, - // TODO(mdebbar): https://github.com/flutter/flutter/issues/46638 - // TODO(mdebbar): https://github.com/flutter/flutter/issues/50590 - // TODO(mdebbar): https://github.com/flutter/flutter/issues/50771 - skip: browserEngine != BrowserEngine.blink); - - testEachMeasurement('getPositionForOffset single-line', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.ltr, - )); - builder.addText('abcd efg'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 1000)); - - // At the beginning of the line. - expect( - paragraph.getPositionForOffset(const Offset(0, 5)), - const TextPosition(offset: 0, affinity: TextAffinity.downstream), - ); - // Below the line. - expect( - paragraph.getPositionForOffset(const Offset(0, 12)), - const TextPosition(offset: 8, affinity: TextAffinity.upstream), - ); - // Above the line. - expect( - paragraph.getPositionForOffset(const Offset(0, -5)), - const TextPosition(offset: 0, affinity: TextAffinity.downstream), - ); - // At the end of the line. - expect( - paragraph.getPositionForOffset(const Offset(80, 5)), - const TextPosition(offset: 8, affinity: TextAffinity.upstream), - ); - // On the left side of "b". - expect( - paragraph.getPositionForOffset(const Offset(14, 5)), - const TextPosition(offset: 1, affinity: TextAffinity.downstream), - ); - // On the right side of "b". - expect( - paragraph.getPositionForOffset(const Offset(16, 5)), - const TextPosition(offset: 2, affinity: TextAffinity.upstream), - ); - }); - - test('getPositionForOffset multi-line', () { - // [Paragraph.getPositionForOffset] for multi-line text doesn't work well - // with dom-based measurement. - WebExperiments.instance!.useCanvasText = true; - WebExperiments.instance!.useCanvasRichText = false; - TextMeasurementService.initialize(rulerCacheCapacity: 2); - - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.ltr, - )); - builder.addText('abcd\n'); - builder.addText('abcdefg\n'); - builder.addText('ab'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 100)); - - // First line: "abcd\n" - - // At the beginning of the first line. - expect( - paragraph.getPositionForOffset(const Offset(0, 5)), - const TextPosition(offset: 0, affinity: TextAffinity.downstream), - ); - // Above the first line. - expect( - paragraph.getPositionForOffset(const Offset(0, -15)), - const TextPosition(offset: 0, affinity: TextAffinity.downstream), - ); - // At the end of the first line. - expect( - paragraph.getPositionForOffset(const Offset(50, 5)), - const TextPosition(offset: 4, affinity: TextAffinity.upstream), - ); - // On the left side of "b" in the first line. - expect( - paragraph.getPositionForOffset(const Offset(14, 5)), - const TextPosition(offset: 1, affinity: TextAffinity.downstream), - ); - // On the right side of "b" in the first line. - expect( - paragraph.getPositionForOffset(const Offset(16, 5)), - const TextPosition(offset: 2, affinity: TextAffinity.upstream), - ); - - // Second line: "abcdefg\n" - - // At the beginning of the second line. - expect( - paragraph.getPositionForOffset(const Offset(0, 15)), - const TextPosition(offset: 5, affinity: TextAffinity.downstream), - ); - // At the end of the second line. - expect( - paragraph.getPositionForOffset(const Offset(100, 15)), - const TextPosition(offset: 12, affinity: TextAffinity.upstream), - ); - // On the left side of "e" in the second line. - expect( - paragraph.getPositionForOffset(const Offset(44, 15)), - const TextPosition(offset: 9, affinity: TextAffinity.downstream), - ); - // On the right side of "e" in the second line. - expect( - paragraph.getPositionForOffset(const Offset(46, 15)), - const TextPosition(offset: 10, affinity: TextAffinity.upstream), - ); - - // Last (third) line: "ab" - - // At the beginning of the last line. - expect( - paragraph.getPositionForOffset(const Offset(0, 25)), - const TextPosition(offset: 13, affinity: TextAffinity.downstream), - ); - // At the end of the last line. - expect( - paragraph.getPositionForOffset(const Offset(100, 25)), - const TextPosition(offset: 15, affinity: TextAffinity.upstream), - ); - // Below the last line. - expect( - paragraph.getPositionForOffset(const Offset(0, 32)), - const TextPosition(offset: 15, affinity: TextAffinity.upstream), - ); - // On the left side of "b" in the last line. - expect( - paragraph.getPositionForOffset(const Offset(12, 25)), - const TextPosition(offset: 14, affinity: TextAffinity.downstream), - ); - // On the right side of "a" in the last line. - expect( - paragraph.getPositionForOffset(const Offset(9, 25)), - const TextPosition(offset: 14, affinity: TextAffinity.upstream), - ); - - TextMeasurementService.clearCache(); - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - }); - - test('getPositionForOffset multi-line centered', () { - WebExperiments.instance!.useCanvasText = true; - WebExperiments.instance!.useCanvasRichText = false; - TextMeasurementService.initialize(rulerCacheCapacity: 2); - - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.ltr, - textAlign: TextAlign.center, - )); - builder.addText('abcd\n'); - builder.addText('abcdefg\n'); - builder.addText('ab'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 100)); - - // First line: "abcd\n" - - // At the beginning of the first line. - expect( - paragraph.getPositionForOffset(const Offset(0, 5)), - const TextPosition(offset: 0, affinity: TextAffinity.downstream), - ); - // Above the first line. - expect( - paragraph.getPositionForOffset(const Offset(0, -15)), - const TextPosition(offset: 0, affinity: TextAffinity.downstream), - ); - // At the end of the first line. - expect( - paragraph.getPositionForOffset(const Offset(100, 5)), - const TextPosition(offset: 4, affinity: TextAffinity.upstream), - ); - // On the left side of "b" in the first line. - expect( - // The line is centered so it's shifted to the right by "30.0px". - paragraph.getPositionForOffset(const Offset(30.0 + 14, 5)), - const TextPosition(offset: 1, affinity: TextAffinity.downstream), - ); - // On the right side of "b" in the first line. - expect( - // The line is centered so it's shifted to the right by "30.0px". - paragraph.getPositionForOffset(const Offset(30.0 + 16, 5)), - const TextPosition(offset: 2, affinity: TextAffinity.upstream), - ); - - // Second line: "abcdefg\n" - - // At the beginning of the second line. - expect( - paragraph.getPositionForOffset(const Offset(0, 15)), - const TextPosition(offset: 5, affinity: TextAffinity.downstream), - ); - // At the end of the second line. - expect( - paragraph.getPositionForOffset(const Offset(100, 15)), - const TextPosition(offset: 12, affinity: TextAffinity.upstream), - ); - // On the left side of "e" in the second line. - expect( - // The line is centered so it's shifted to the right by "15.0px". - paragraph.getPositionForOffset(const Offset(15.0 + 44, 15)), - const TextPosition(offset: 9, affinity: TextAffinity.downstream), - ); - // On the right side of "e" in the second line. - expect( - // The line is centered so it's shifted to the right by "15.0px". - paragraph.getPositionForOffset(const Offset(15.0 + 46, 15)), - const TextPosition(offset: 10, affinity: TextAffinity.upstream), - ); - - // Last (third) line: "ab" - - // At the beginning of the last line. - expect( - paragraph.getPositionForOffset(const Offset(0, 25)), - const TextPosition(offset: 13, affinity: TextAffinity.downstream), - ); - // At the end of the last line. - expect( - paragraph.getPositionForOffset(const Offset(100, 25)), - const TextPosition(offset: 15, affinity: TextAffinity.upstream), - ); - // Below the last line. - expect( - paragraph.getPositionForOffset(const Offset(0, 32)), - const TextPosition(offset: 15, affinity: TextAffinity.upstream), - ); - // On the left side of "b" in the last line. - expect( - // The line is centered so it's shifted to the right by "40.0px". - paragraph.getPositionForOffset(const Offset(40.0 + 12, 25)), - const TextPosition(offset: 14, affinity: TextAffinity.downstream), - ); - // On the right side of "a" in the last line. - expect( - // The line is centered so it's shifted to the right by "40.0px". - paragraph.getPositionForOffset(const Offset(40.0 + 9, 25)), - const TextPosition(offset: 14, affinity: TextAffinity.upstream), - ); - - TextMeasurementService.clearCache(); - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - }); - - test('getWordBoundary', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()) - ..addText('Lorem ipsum dolor'); - final Paragraph paragraph = builder.build(); - - const TextRange loremRange = TextRange(start: 0, end: 5); - expect(paragraph.getWordBoundary(const TextPosition(offset: 0)), loremRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 1)), loremRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 2)), loremRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 3)), loremRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 4)), loremRange); - - const TextRange firstSpace = TextRange(start: 5, end: 6); - expect(paragraph.getWordBoundary(const TextPosition(offset: 5)), firstSpace); - - const TextRange ipsumRange = TextRange(start: 6, end: 11); - expect(paragraph.getWordBoundary(const TextPosition(offset: 6)), ipsumRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 7)), ipsumRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 8)), ipsumRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 9)), ipsumRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 10)), ipsumRange); - - const TextRange secondSpace = TextRange(start: 11, end: 12); - expect(paragraph.getWordBoundary(const TextPosition(offset: 11)), secondSpace); - - const TextRange dolorRange = TextRange(start: 12, end: 17); - expect(paragraph.getWordBoundary(const TextPosition(offset: 12)), dolorRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 13)), dolorRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 14)), dolorRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 15)), dolorRange); - expect(paragraph.getWordBoundary(const TextPosition(offset: 16)), dolorRange); - - const TextRange endRange = TextRange(start: 17, end: 17); - expect(paragraph.getWordBoundary(const TextPosition(offset: 17)), endRange); - }); - - testEachMeasurement('getBoxesForRange returns a box', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.rtl, - )); - builder.addText('abcd'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 1000)); - expect( - paragraph.getBoxesForRange(1, 2).single, - const TextBox.fromLTRBD( - 970, - 0, - 980, - 10, - TextDirection.rtl, - ), - ); - }); - - testEachMeasurement('getBoxesForRange returns a box for rich text', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.ltr, - )); - builder.addText('abcd'); - builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); - builder.addText('xyz'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 1000)); - expect( - paragraph.getBoxesForRange(1, 2).single, - const TextBox.fromLTRBD(0, 0, 0, 10, TextDirection.ltr), - ); - }); - - testEachMeasurement( - 'getBoxesForRange return empty list for zero-length range', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - )); - builder.addText('abcd'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 1000)); - expect(paragraph.getBoxesForRange(0, 0), isEmpty); - }); - - testEachMeasurement('getBoxesForRange multi-line', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.ltr, - )); - builder.addText('abcd\n'); - builder.addText('abcdefg\n'); - builder.addText('ab'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 100)); - - // First line: "abcd\n" - - // At the beginning of the first line. - expect( - paragraph.getBoxesForRange(0, 0), - [], - ); - // At the end of the first line. - expect( - paragraph.getBoxesForRange(4, 4), - [], - ); - // Between "b" and "c" in the first line. - expect( - paragraph.getBoxesForRange(2, 2), - [], - ); - // The range "ab" in the first line. - expect( - paragraph.getBoxesForRange(0, 2), - const [ - TextBox.fromLTRBD(0.0, 0.0, 20.0, 10.0, TextDirection.ltr), - ], - ); - // The range "bc" in the first line. - expect( - paragraph.getBoxesForRange(1, 3), - const [ - TextBox.fromLTRBD(10.0, 0.0, 30.0, 10.0, TextDirection.ltr), - ], - ); - // The range "d" in the first line. - expect( - paragraph.getBoxesForRange(3, 4), - const [ - TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr), - ], - ); - // The range "\n" in the first line. - expect( - paragraph.getBoxesForRange(4, 5), - const [ - TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr), - ], - ); - // The range "cd\n" in the first line. - expect( - paragraph.getBoxesForRange(2, 5), - const [ - TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr), - ], - ); - - // Second line: "abcdefg\n" - - // At the beginning of the second line. - expect( - paragraph.getBoxesForRange(5, 5), - [], - ); - // At the end of the second line. - expect( - paragraph.getBoxesForRange(12, 12), - [], - ); - // The range "efg" in the second line. - expect( - paragraph.getBoxesForRange(9, 12), - const [ - TextBox.fromLTRBD(40.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - // The range "bcde" in the second line. - expect( - paragraph.getBoxesForRange(6, 10), - const [ - TextBox.fromLTRBD(10.0, 10.0, 50.0, 20.0, TextDirection.ltr), - ], - ); - // The range "fg\n" in the second line. - expect( - paragraph.getBoxesForRange(10, 13), - const [ - TextBox.fromLTRBD(50.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - - // Last (third) line: "ab" - - // At the beginning of the last line. - expect( - paragraph.getBoxesForRange(13, 13), - [], - ); - // At the end of the last line. - expect( - paragraph.getBoxesForRange(15, 15), - [], - ); - // The range "a" in the last line. - expect( - paragraph.getBoxesForRange(14, 15), - const [ - TextBox.fromLTRBD(10.0, 20.0, 20.0, 30.0, TextDirection.ltr), - ], - ); - // The range "ab" in the last line. - expect( - paragraph.getBoxesForRange(13, 15), - const [ - TextBox.fromLTRBD(0.0, 20.0, 20.0, 30.0, TextDirection.ltr), - ], - ); - - - // Combine multiple lines - - // The range "cd\nabc". - expect( - paragraph.getBoxesForRange(2, 8), - const [ - TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 30.0, 20.0, TextDirection.ltr), - ], - ); - - // The range "\nabcd". - expect( - paragraph.getBoxesForRange(4, 9), - const [ - TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 40.0, 20.0, TextDirection.ltr), - ], - ); - - // The range "d\nabcdefg\na". - expect( - paragraph.getBoxesForRange(3, 14), - const [ - TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 20.0, 10.0, 30.0, TextDirection.ltr), - ], - ); - - // The range "abcd\nabcdefg\n". - expect( - paragraph.getBoxesForRange(0, 13), - const [ - TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - - // The range "abcd\nabcdefg\nab". - expect( - paragraph.getBoxesForRange(0, 15), - const [ - TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 20.0, 20.0, 30.0, TextDirection.ltr), - ], - ); - }); - - testEachMeasurement('getBoxesForRange with maxLines', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.ltr, - maxLines: 2, - )); - builder.addText('abcd\n'); - builder.addText('abcdefg\n'); - builder.addText('ab'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 100)); - - // First line: "abcd\n" - - // At the beginning of the first line. - expect( - paragraph.getBoxesForRange(0, 0), - [], - ); - // At the end of the first line. - expect( - paragraph.getBoxesForRange(4, 4), - [], - ); - // Between "b" and "c" in the first line. - expect( - paragraph.getBoxesForRange(2, 2), - [], - ); - // The range "ab" in the first line. - expect( - paragraph.getBoxesForRange(0, 2), - const [ - TextBox.fromLTRBD(0.0, 0.0, 20.0, 10.0, TextDirection.ltr), - ], - ); - // The range "bc" in the first line. - expect( - paragraph.getBoxesForRange(1, 3), - const [ - TextBox.fromLTRBD(10.0, 0.0, 30.0, 10.0, TextDirection.ltr), - ], - ); - // The range "d" in the first line. - expect( - paragraph.getBoxesForRange(3, 4), - const [ - TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr), - ], - ); - // The range "\n" in the first line. - expect( - paragraph.getBoxesForRange(4, 5), - const [ - TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr), - ], - ); - // The range "cd\n" in the first line. - expect( - paragraph.getBoxesForRange(2, 5), - const [ - TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr), - ], - ); - - // Second line: "abcdefg\n" - - // At the beginning of the second line. - expect( - paragraph.getBoxesForRange(5, 5), - [], - ); - // At the end of the second line. - expect( - paragraph.getBoxesForRange(12, 12), - [], - ); - // The range "efg" in the second line. - expect( - paragraph.getBoxesForRange(9, 12), - const [ - TextBox.fromLTRBD(40.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - // The range "bcde" in the second line. - expect( - paragraph.getBoxesForRange(6, 10), - const [ - TextBox.fromLTRBD(10.0, 10.0, 50.0, 20.0, TextDirection.ltr), - ], - ); - // The range "fg\n" in the second line. - expect( - paragraph.getBoxesForRange(10, 13), - const [ - TextBox.fromLTRBD(50.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - - // Last (third) line: "ab" - - // At the beginning of the last line. - expect( - paragraph.getBoxesForRange(13, 13), - [], - ); - // At the end of the last line. - expect( - paragraph.getBoxesForRange(15, 15), - [], - ); - // The range "a" in the last line. - expect( - paragraph.getBoxesForRange(14, 15), - [], - ); - // The range "ab" in the last line. - expect( - paragraph.getBoxesForRange(13, 15), - [], - ); - - - // Combine multiple lines - - // The range "cd\nabc". - expect( - paragraph.getBoxesForRange(2, 8), - const [ - TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 30.0, 20.0, TextDirection.ltr), - ], - ); - - // The range "\nabcd". - expect( - paragraph.getBoxesForRange(4, 9), - const [ - TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 40.0, 20.0, TextDirection.ltr), - ], - ); - - // The range "d\nabcdefg\na". - expect( - paragraph.getBoxesForRange(3, 14), - const [ - TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - - // The range "abcd\nabcdefg\n". - expect( - paragraph.getBoxesForRange(0, 13), - const [ - TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - - // The range "abcd\nabcdefg\nab". - expect( - paragraph.getBoxesForRange(0, 15), - const [ - TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), - ], - ); - }); - - testEachMeasurement('getBoxesForRange includes trailing spaces', () { - const String text = 'abcd abcde '; - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - )); - builder.addText(text); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: double.infinity)); - expect( - paragraph.getBoxesForRange(0, text.length), - const [ - TextBox.fromLTRBD(0.0, 0.0, 120.0, 10.0, TextDirection.ltr), - ], - ); - }); - - testEachMeasurement('getBoxesForRange multi-line includes trailing spaces', () { - const String text = 'abcd\nabcde \nabc'; - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - )); - builder.addText(text); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: double.infinity)); - expect( - paragraph.getBoxesForRange(0, text.length), - const [ - TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr), - TextBox.fromLTRBD(0.0, 20.0, 30.0, 30.0, TextDirection.ltr), - ], - ); - }); - - test('longestLine', () { - // [Paragraph.longestLine] is only supported by canvas-based measurement. - WebExperiments.instance!.useCanvasText = true; - WebExperiments.instance!.useCanvasRichText = false; - TextMeasurementService.initialize(rulerCacheCapacity: 2); - - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - )); - builder.addText('abcd\nabcde abc'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 80.0)); - expect(paragraph.longestLine, 50.0); - - TextMeasurementService.clearCache(); - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - }); - - testEachMeasurement('getLineBoundary (single-line)', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - )); - builder.addText('One single line'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 400.0)); - - // "One single line".length == 15 - for (int i = 0; i < 15; i++) { - expect( - paragraph.getLineBoundary(TextPosition(offset: i)), - const TextRange(start: 0, end: 15), - reason: 'failed at offset $i', - ); - } - }); - - test('getLineBoundary (multi-line)', () { - // [Paragraph.getLineBoundary] for multi-line paragraphs is only supported - // by canvas-based measurement. - WebExperiments.instance!.useCanvasText = true; - WebExperiments.instance!.useCanvasRichText = false; - TextMeasurementService.initialize(rulerCacheCapacity: 2); - - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - )); - builder.addText('First line\n'); - builder.addText('Second line\n'); - builder.addText('Third line'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 400.0)); - - // "First line\n".length == 11 - for (int i = 0; i < 11; i++) { - expect( - paragraph.getLineBoundary(TextPosition(offset: i)), - const TextRange(start: 0, end: 11), - reason: 'failed at offset $i', - ); - } - - // "Second line\n".length == 12 - for (int i = 11; i < 23; i++) { - expect( - paragraph.getLineBoundary(TextPosition(offset: i)), - const TextRange(start: 11, end: 23), - reason: 'failed at offset $i', - ); - } - - // "Third line".length == 10 - for (int i = 23; i < 33; i++) { - expect( - paragraph.getLineBoundary(TextPosition(offset: i)), - const TextRange(start: 23, end: 33), - reason: 'failed at offset $i', - ); - } - - TextMeasurementService.clearCache(); - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - }); - - testEachMeasurement('width should be a whole integer', () { - final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( - fontFamily: 'Ahem', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 10, - textDirection: TextDirection.ltr, - )); - builder.addText('abc'); - final Paragraph paragraph = builder.build(); - paragraph.layout(const ParagraphConstraints(width: 30.8)); - - expect(paragraph.width, 30); - expect(paragraph.height, 10); - }); -} diff --git a/lib/web_ui/test/text/canvas_paragraph_test.dart b/lib/web_ui/test/text/canvas_paragraph_test.dart index d838635fcc506..6d287261782d3 100644 --- a/lib/web_ui/test/text/canvas_paragraph_test.dart +++ b/lib/web_ui/test/text/canvas_paragraph_test.dart @@ -7,6 +7,8 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import '../html/paragraph/helper.dart'; + const ui.Color white = ui.Color(0xFFFFFFFF); const ui.Color black = ui.Color(0xFF000000); const ui.Color red = ui.Color(0xFFFF0000); @@ -22,15 +24,6 @@ ui.ParagraphConstraints constrain(double width) { return ui.ParagraphConstraints(width: width); } -CanvasParagraph rich( - EngineParagraphStyle style, - void Function(CanvasParagraphBuilder) callback, -) { - final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); - callback(builder); - return builder.build(); -} - void main() { internalBootstrapBrowserTest(() => testMain); } @@ -686,6 +679,54 @@ Future testMain() async { } }); }); + + test('$CanvasParagraph.getWordBoundary', () { + final ui.Paragraph paragraph = plain(ahemStyle, 'Lorem ipsum dolor'); + + const ui.TextRange loremRange = ui.TextRange(start: 0, end: 5); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 0)), loremRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 1)), loremRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 2)), loremRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 3)), loremRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 4)), loremRange); + + const ui.TextRange firstSpace = ui.TextRange(start: 5, end: 6); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 5)), firstSpace); + + const ui.TextRange ipsumRange = ui.TextRange(start: 6, end: 11); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 6)), ipsumRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 7)), ipsumRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 8)), ipsumRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 9)), ipsumRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 10)), ipsumRange); + + const ui.TextRange secondSpace = ui.TextRange(start: 11, end: 12); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 11)), secondSpace); + + const ui.TextRange dolorRange = ui.TextRange(start: 12, end: 17); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 12)), dolorRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 13)), dolorRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 14)), dolorRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 15)), dolorRange); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 16)), dolorRange); + + const ui.TextRange endRange = ui.TextRange(start: 17, end: 17); + expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 17)), endRange); + }); + + test('$CanvasParagraph.longestLine', () { + final ui.Paragraph paragraph = plain(ahemStyle, 'abcd\nabcde abc'); + paragraph.layout(const ui.ParagraphConstraints(width: 80.0)); + expect(paragraph.longestLine, 50.0); + }); + + test('$CanvasParagraph.width should be a whole integer', () { + final ui.Paragraph paragraph = plain(ahemStyle, 'abc'); + paragraph.layout(const ui.ParagraphConstraints(width: 30.8)); + + expect(paragraph.width, 30); + expect(paragraph.height, 10); + }); } /// Shortcut to create a [ui.TextBox] with an optional [ui.TextDirection]. diff --git a/lib/web_ui/test/text/font_loading_test.dart b/lib/web_ui/test/text/font_loading_test.dart index 81d925c18757a..22e8d65271174 100644 --- a/lib/web_ui/test/text/font_loading_test.dart +++ b/lib/web_ui/test/text/font_loading_test.dart @@ -56,12 +56,6 @@ Future testMain() async { const ui.ParagraphConstraints constraints = ui.ParagraphConstraints(width: 30.0); - final DomParagraphBuilder domBuilder = DomParagraphBuilder(style); - domBuilder.addText('test'); - // Triggers the measuring and verifies the result has been cached. - domBuilder.build().layout(constraints); - expect(TextMeasurementService.rulerManager!.rulers.length, 1); - final CanvasParagraphBuilder canvasBuilder = CanvasParagraphBuilder(style); canvasBuilder.addText('test'); // Triggers the measuring and verifies the ruler cache has been populated. @@ -78,7 +72,6 @@ Future testMain() async { // Verifies the font is loaded, and the cache is cleaned. expect(_containsFontFamily('Blehm'), isTrue); - expect(TextMeasurementService.rulerManager!.rulers.length, 0); expect(Spanometer.rulers.length, 0); }, // TODO(hterkelsen): https://github.com/flutter/flutter/issues/56702 diff --git a/lib/web_ui/test/text/measurement_test.dart b/lib/web_ui/test/text/measurement_test.dart deleted file mode 100644 index 18567ec87c848..0000000000000 --- a/lib/web_ui/test/text/measurement_test.dart +++ /dev/null @@ -1,1181 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@TestOn('chrome || firefox') - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -final ui.ParagraphStyle ahemStyle = ui.ParagraphStyle( - fontFamily: 'ahem', - fontSize: 10, -); -const ui.ParagraphConstraints constraints = ui.ParagraphConstraints(width: 50); -const ui.ParagraphConstraints infiniteConstraints = - ui.ParagraphConstraints(width: double.infinity); - -DomParagraph build(ui.ParagraphStyle style, String text, - {ui.TextStyle? textStyle}) { - final DomParagraphBuilder builder = DomParagraphBuilder(style as EngineParagraphStyle); - if (textStyle != null) { - builder.pushStyle(textStyle); - } - builder.addText(text); - return builder.build() as DomParagraph; -} - -typedef MeasurementTestBody = void Function(TextMeasurementService instance); - -/// Runs the same test twice - once with dom measurement and once with canvas -/// measurement. -void testMeasurements(String description, MeasurementTestBody body, { - bool? skipDom, - bool? skipCanvas, -}) { - test( - '$description (dom)', - () { - try { - WebExperiments.instance!.useCanvasRichText = false; - return body(TextMeasurementService.domInstance); - } finally { - WebExperiments.instance!.useCanvasRichText = null; - } - }, - skip: skipDom, - ); - test( - '$description (canvas)', - () { - try { - WebExperiments.instance!.useCanvasRichText = false; - return body(TextMeasurementService.canvasInstance); - } finally { - WebExperiments.instance!.useCanvasRichText = null; - } - }, - skip: skipCanvas, - ); -} -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -Future testMain() async { - await ui.webOnlyInitializeTestDomRenderer(); - - group('$RulerManager', () { - final ui.ParagraphStyle s1 = ui.ParagraphStyle(fontFamily: 'sans-serif'); - final ui.ParagraphStyle s2 = ui.ParagraphStyle( - fontWeight: ui.FontWeight.bold, - ); - final ui.ParagraphStyle s3 = ui.ParagraphStyle(fontSize: 22.0); - - late ParagraphGeometricStyle style1, style2, style3; - late DomParagraph style1Text1, style1Text2; // two paragraphs sharing style - late DomParagraph style2Text1, style3Text3; - - setUp(() { - style1Text1 = build(s1, '1'); - style1Text2 = build(s1, '2'); - style2Text1 = build(s2, '1'); - style3Text3 = build(s3, '3'); - - style1 = style1Text1.geometricStyle; - style2 = style2Text1.geometricStyle; - style3 = style3Text3.geometricStyle; - - final ParagraphGeometricStyle style1_2 = style1Text2.geometricStyle; - expect(style1_2, style1); // styles must be equal despite different text - }); - - test('caches rulers', () { - final RulerManager rulerManager = RulerManager(rulerCacheCapacity: 2); - ParagraphRuler ruler1, ruler2, ruler3; - - expect(rulerManager.rulerCacheCapacity, 2); - expect(rulerManager.rulers.length, 0); - - // First ruler cached - ruler1 = rulerManager.findOrCreateRuler(style1); - expect(rulerManager.rulers.length, 1); - expect(ruler1.hitCount, 1); - - // Increase hit count for style 1 - ruler1 = rulerManager.findOrCreateRuler(style1); - expect(rulerManager.rulers.length, 1); - expect(ruler1.hitCount, 2); - - // Previous ruler reused - rulerManager.findOrCreateRuler(style1); - expect(rulerManager.rulers.length, 1); - expect(ruler1.hitCount, 3); - - // Second ruler created and cached - ruler2 = rulerManager.findOrCreateRuler(style2); - expect(rulerManager.rulers.length, 2); - expect(ruler1.hitCount, 3); - expect(ruler2.hitCount, 1); - - // Increase hit count for style 2 - rulerManager.findOrCreateRuler(style2); - rulerManager.findOrCreateRuler(style2); - rulerManager.findOrCreateRuler(style2); - expect(rulerManager.rulers.length, 2); - expect(ruler2.hitCount, 4); - - // Third ruler cached: it is ok to store more rulers that the cache - // capacity because the cache is cleaned-up at the next microtask. - ruler3 = rulerManager.findOrCreateRuler(style3); - - // Final ruler states - expect(rulerManager.rulers.length, 3); - expect(ruler1.hitCount, 3); - expect(ruler2.hitCount, 4); - expect(ruler3.hitCount, 1); - // The least hit ruler isn't disposed yet. - expect(ruler3.debugIsDisposed, isFalse); - - // Cleaning up the cache should bring its size down to capacity limit. - rulerManager.cleanUpRulerCache(); - expect(rulerManager.rulers.length, 2); - expect(rulerManager.rulers, containsValue(ruler1)); // retained - expect(rulerManager.rulers, containsValue(ruler2)); // retained - expect(rulerManager.rulers, isNot(containsValue(ruler3))); // evicted - expect(ruler1.debugIsDisposed, isFalse); - expect(ruler2.debugIsDisposed, isFalse); - expect(ruler3.debugIsDisposed, isTrue); - - ruler1 = rulerManager.rulers[style1]!; - expect(ruler1.style, style1); - expect(ruler1.hitCount, 0); // hit counts are reset - - ruler2 = rulerManager.rulers[style2]!; - expect(ruler2.style, style2); - expect(ruler2.hitCount, 0); // hit counts are reset - }); - }); - - group('$TextMeasurementService', () { - setUp(() { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - }); - tearDown(() { - TextMeasurementService.clearCache(); - }); - - testMeasurements( - 'preserves whitespace when measuring', - (TextMeasurementService instance) { - DomParagraph text; - MeasurementResult result; - - // leading whitespaces - text = build(ahemStyle, ' abc'); - result = instance.measure(text, infiniteConstraints)!; - expect(result.maxIntrinsicWidth, 60); - expect(result.minIntrinsicWidth, 30); - expect(result.height, 10); - expect(result.lines, [ - line(' abc', 0, 6, hardBreak: true, width: 60.0, lineNumber: 0, left: 0.0), - ]); - - // trailing whitespaces - text = build(ahemStyle, 'abc '); - result = instance.measure(text, infiniteConstraints)!; - expect(result.maxIntrinsicWidth, 60); - expect(result.minIntrinsicWidth, 30); - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line('abc ', 0, 6, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement always includes trailing whitespace in the - // width, while Flutter and Canvas-based measurement don't. - expect(result.lines, [ - line('abc ', 0, 6, hardBreak: true, width: 60.0, lineNumber: 0, left: 0.0), - ]); - } - - // mixed whitespaces - text = build(ahemStyle, ' ab c '); - result = instance.measure(text, infiniteConstraints)!; - expect(result.maxIntrinsicWidth, 100); - expect(result.minIntrinsicWidth, 20); - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line(' ab c ', 0, 10, hardBreak: true, width: 80.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement always includes trailing whitespace in the - // width, while Flutter and Canvas-based measurement don't. - expect(result.lines, [ - line(' ab c ', 0, 10, hardBreak: true, width: 100.0, lineNumber: 0, left: 0.0), - ]); - } - - // single whitespace - text = build(ahemStyle, ' '); - result = instance.measure(text, infiniteConstraints)!; - expect(result.maxIntrinsicWidth, 10); - expect(result.minIntrinsicWidth, 0); - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line(' ', 0, 1, hardBreak: true, width: 0.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement always includes trailing whitespace in the - // width, while Flutter and Canvas-based measurement don't. - expect(result.lines, [ - line(' ', 0, 1, hardBreak: true, width: 10.0, lineNumber: 0, left: 0.0), - ]); - } - - // whitespace only - text = build(ahemStyle, ' '); - result = instance.measure(text, infiniteConstraints)!; - expect(result.maxIntrinsicWidth, 50); - expect(result.minIntrinsicWidth, 0); - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line(' ', 0, 5, hardBreak: true, width: 0.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement always includes trailing whitespace in the - // width, while Flutter and Canvas-based measurement don't. - expect(result.lines, [ - line(' ', 0, 5, hardBreak: true, width: 50.0, lineNumber: 0, left: 0.0), - ]); - } - }, - ); - - testMeasurements( - 'uses single-line when text can fit without wrapping', - (TextMeasurementService instance) { - final MeasurementResult result = - instance.measure(build(ahemStyle, '12345'), constraints)!; - - // Should fit on a single line. - expect(result.isSingleLine, isTrue); - expect(result.alphabeticBaseline, 8); - expect(result.maxIntrinsicWidth, 50); - expect(result.minIntrinsicWidth, 50); - expect(result.width, 50); - expect(result.height, 10); - expect(result.lines, [ - line('12345', 0, 5, hardBreak: true, width: 50.0, lineNumber: 0, left: 0.0), - ]); - }, - ); - - testMeasurements( - 'simple multi-line text', - (TextMeasurementService instance) { - const ui.ParagraphConstraints constraints = - ui.ParagraphConstraints(width: 70); - MeasurementResult result; - - // The long text doesn't fit in 70px of width, so it needs to wrap. - result = instance.measure(build(ahemStyle, 'foo bar baz'), constraints)!; - expect(result.isSingleLine, isFalse); - expect(result.alphabeticBaseline, 8); - expect(result.maxIntrinsicWidth, 110); - expect(result.minIntrinsicWidth, 30); - expect(result.width, 70); - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('foo bar ', 0, 8, hardBreak: false, width: 70.0, lineNumber: 0, left: 0.0), - line('baz', 8, 11, hardBreak: true, width: 30.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }, - ); - - testMeasurements( - 'uses multi-line for long text', - (TextMeasurementService instance) { - MeasurementResult result; - - // The long text doesn't fit in 50px of width, so it needs to wrap. - result = instance.measure(build(ahemStyle, '1234567890'), constraints)!; - expect(result.isSingleLine, isFalse); - expect(result.alphabeticBaseline, 8); - expect(result.maxIntrinsicWidth, 100); - expect(result.minIntrinsicWidth, 100); - expect(result.width, 50); - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('12345', 0, 5, hardBreak: false, width: 50.0, lineNumber: 0, left: 0.0), - line('67890', 5, 10, hardBreak: true, width: 50.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // The first word is force-broken twice. - result = - instance.measure(build(ahemStyle, 'abcdefghijk lm'), constraints)!; - expect(result.isSingleLine, isFalse); - expect(result.alphabeticBaseline, 8); - expect(result.maxIntrinsicWidth, 140); - expect(result.minIntrinsicWidth, 110); - expect(result.width, 50); - expect(result.height, 30); - if (instance.isCanvas) { - expect(result.lines, [ - line('abcde', 0, 5, hardBreak: false, width: 50.0, lineNumber: 0, left: 0.0), - line('fghij', 5, 10, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('k lm', 10, 14, hardBreak: true, width: 40.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Constraints aren't enough even for a single character. In this case, - // we show a minimum of one character per line. - const ui.ParagraphConstraints narrowConstraints = - ui.ParagraphConstraints(width: 8); - result = instance.measure(build(ahemStyle, 'AA'), narrowConstraints)!; - expect(result.isSingleLine, isFalse); - expect(result.alphabeticBaseline, 8); - expect(result.maxIntrinsicWidth, 20); - expect(result.minIntrinsicWidth, 20); - expect(result.width, 8); - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('A', 0, 1, hardBreak: false, width: 10.0, lineNumber: 0, left: 0.0), - line('A', 1, 2, hardBreak: true, width: 10.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Extremely narrow constraints with new line in the middle. - result = instance.measure(build(ahemStyle, 'AA\nA'), narrowConstraints)!; - expect(result.isSingleLine, isFalse); - expect(result.alphabeticBaseline, 8); - expect(result.maxIntrinsicWidth, 20); - expect(result.minIntrinsicWidth, 20); - expect(result.width, 8); - expect(result.height, 30); - if (instance.isCanvas) { - expect(result.lines, [ - line('A', 0, 1, hardBreak: false, width: 10.0, lineNumber: 0, left: 0.0), - line('A', 1, 3, hardBreak: true, width: 10.0, lineNumber: 1, left: 0.0), - line('A', 3, 4, hardBreak: true, width: 10.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Extremely narrow constraints with new line in the end. - result = instance.measure(build(ahemStyle, 'AAA\n'), narrowConstraints)!; - expect(result.isSingleLine, isFalse); - expect(result.alphabeticBaseline, 8); - expect(result.maxIntrinsicWidth, 30); - expect(result.minIntrinsicWidth, 30); - expect(result.width, 8); - expect(result.height, 40); - if (instance.isCanvas) { - expect(result.lines, [ - line('A', 0, 1, hardBreak: false, width: 10.0, lineNumber: 0, left: 0.0), - line('A', 1, 2, hardBreak: false, width: 10.0, lineNumber: 1, left: 0.0), - line('A', 2, 4, hardBreak: true, width: 10.0, lineNumber: 2, left: 0.0), - line('', 4, 4, hardBreak: true, width: 0.0, lineNumber: 3, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }, - skipDom: browserEngine == BrowserEngine.webkit, - ); - - testMeasurements( - 'uses multi-line for text that contains new-line', - (TextMeasurementService instance) { - final MeasurementResult result = - instance.measure(build(ahemStyle, '12\n34'), constraints)!; - - // Text containing newlines should always be drawn in multi-line mode. - expect(result.isSingleLine, isFalse); - expect(result.maxIntrinsicWidth, 20); - expect(result.minIntrinsicWidth, 20); - expect(result.width, 50); - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('12', 0, 3, hardBreak: true, width: 20.0, lineNumber: 0, left: 0.0), - line('34', 3, 5, hardBreak: true, width: 20.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }, - ); - - testMeasurements('empty lines', (TextMeasurementService instance) { - MeasurementResult result; - - // Empty lines in the beginning. - result = instance.measure(build(ahemStyle, '\n\n1234'), constraints)!; - expect(result.maxIntrinsicWidth, 40); - expect(result.minIntrinsicWidth, 40); - expect(result.height, 30); - if (instance.isCanvas) { - expect(result.lines, [ - line('', 0, 1, hardBreak: true, width: 0.0, lineNumber: 0, left: 0.0), - line('', 1, 2, hardBreak: true, width: 0.0, lineNumber: 1, left: 0.0), - line('1234', 2, 6, hardBreak: true, width: 40.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Empty lines in the middle. - result = instance.measure(build(ahemStyle, '12\n\n345'), constraints)!; - expect(result.maxIntrinsicWidth, 30); - expect(result.minIntrinsicWidth, 30); - expect(result.height, 30); - if (instance.isCanvas) { - expect(result.lines, [ - line('12', 0, 3, hardBreak: true, width: 20.0, lineNumber: 0, left: 0.0), - line('', 3, 4, hardBreak: true, width: 0.0, lineNumber: 1, left: 0.0), - line('345', 4, 7, hardBreak: true, width: 30.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Empty lines in the end. - result = instance.measure(build(ahemStyle, '1234\n\n'), constraints)!; - expect(result.maxIntrinsicWidth, 40); - expect(result.minIntrinsicWidth, 40); - if (instance.isCanvas) { - // This can only be done correctly in the canvas-based implementation. - expect(result.height, 30); - expect(result.lines, [ - line('1234', 0, 5, hardBreak: true, width: 40.0, lineNumber: 0, left: 0.0), - line('', 5, 6, hardBreak: true, width: 0.0, lineNumber: 1, left: 0.0), - line('', 6, 6, hardBreak: true, width: 0.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }); - - testMeasurements( - 'wraps multi-line text correctly when constraint width is infinite', - (TextMeasurementService instance) { - final DomParagraph paragraph = build(ahemStyle, '123\n456 789'); - final MeasurementResult result = - instance.measure(paragraph, infiniteConstraints)!; - - expect(result.isSingleLine, isFalse); - expect(result.maxIntrinsicWidth, 70); - expect(result.minIntrinsicWidth, 30); - expect(result.width, double.infinity); - expect(result.height, 20); - - if (instance.isCanvas) { - expect(result.lines, [ - line('123', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('456 789', 4, 11, hardBreak: true, width: 70.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }, - ); - - testMeasurements( - 'takes letter spacing into account', - (TextMeasurementService instance) { - const ui.ParagraphConstraints constraints = - ui.ParagraphConstraints(width: 100); - final ui.TextStyle spacedTextStyle = ui.TextStyle(letterSpacing: 3); - final DomParagraph spacedText = - build(ahemStyle, 'abc', textStyle: spacedTextStyle); - - final MeasurementResult spacedResult = - instance.measure(spacedText, constraints)!; - - expect(spacedResult.minIntrinsicWidth, 39); - expect(spacedResult.maxIntrinsicWidth, 39); - }, - ); - - test('takes word spacing into account', () { - const ui.ParagraphConstraints constraints = - ui.ParagraphConstraints(width: 100); - - final DomParagraph normalText = build(ahemStyle, 'a b c'); - final DomParagraph spacedText = - build(ahemStyle, 'a b c', textStyle: ui.TextStyle(wordSpacing: 1.5)); - - // Word spacing is only supported via DOM measurement. - final TextMeasurementService instance = - TextMeasurementService.forParagraph(spacedText); - expect(instance, const TypeMatcher()); - - final MeasurementResult normalResult = - instance.measure(normalText, constraints)!; - final MeasurementResult spacedResult = - instance.measure(spacedText, constraints)!; - - expect( - normalResult.maxIntrinsicWidth < spacedResult.maxIntrinsicWidth, - isTrue, - ); - }); - - testMeasurements('minIntrinsicWidth', (TextMeasurementService instance) { - MeasurementResult result; - - // Simple case. - result = instance.measure(build(ahemStyle, 'abc de fghi'), constraints)!; - expect(result.minIntrinsicWidth, 40); - if (instance.isCanvas) { - expect(result.lines, [ - line('abc ', 0, 4, hardBreak: false, width: 30.0, lineNumber: 0, left: 0.0), - line('de ', 4, 7, hardBreak: false, width: 20.0, lineNumber: 1, left: 0.0), - line('fghi', 7, 11, hardBreak: true, width: 40.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // With new lines. - result = instance.measure(build(ahemStyle, 'abcd\nef\nghi'), constraints)!; - expect(result.minIntrinsicWidth, 40); - if (instance.isCanvas) { - expect(result.lines, [ - line('abcd', 0, 5, hardBreak: true, width: 40.0, lineNumber: 0, left: 0.0), - line('ef', 5, 8, hardBreak: true, width: 20.0, lineNumber: 1, left: 0.0), - line('ghi', 8, 11, hardBreak: true, width: 30.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // With trailing whitespace. - result = instance.measure(build(ahemStyle, 'abcd efg'), constraints)!; - expect(result.minIntrinsicWidth, 40); - if (instance.isCanvas) { - expect(result.lines, [ - line('abcd ', 0, 10, hardBreak: false, width: 40.0, lineNumber: 0, left: 0.0), - line('efg', 10, 13, hardBreak: true, width: 30.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // With trailing whitespace and new lines. - result = instance.measure(build(ahemStyle, 'abc \ndefg'), constraints)!; - expect(result.minIntrinsicWidth, 40); - if (instance.isCanvas) { - expect(result.lines, [ - line('abc ', 0, 8, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('defg', 8, 12, hardBreak: true, width: 40.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Very long text. - result = instance.measure(build(ahemStyle, 'AAAAAAAAAAAA'), constraints)!; - expect(result.minIntrinsicWidth, 120); - if (instance.isCanvas) { - expect(result.lines, [ - line('AAAAA', 0, 5, hardBreak: false, width: 50.0, lineNumber: 0, left: 0.0), - line('AAAAA', 5, 10, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('AA', 10, 12, hardBreak: true, width: 20.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }); - - testMeasurements('maxIntrinsicWidth', (TextMeasurementService instance) { - MeasurementResult result; - - // Simple case. - result = instance.measure(build(ahemStyle, 'abc de fghi'), constraints)!; - expect(result.maxIntrinsicWidth, 110); - if (instance.isCanvas) { - expect(result.lines, [ - line('abc ', 0, 4, hardBreak: false, width: 30.0, lineNumber: 0, left: 0.0), - line('de ', 4, 7, hardBreak: false, width: 20.0, lineNumber: 1, left: 0.0), - line('fghi', 7, 11, hardBreak: true, width: 40.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // With new lines. - result = instance.measure(build(ahemStyle, 'abcd\nef\nghi'), constraints)!; - expect(result.maxIntrinsicWidth, 40); - if (instance.isCanvas) { - expect(result.lines, [ - line('abcd', 0, 5, hardBreak: true, width: 40.0, lineNumber: 0, left: 0.0), - line('ef', 5, 8, hardBreak: true, width: 20.0, lineNumber: 1, left: 0.0), - line('ghi', 8, 11, hardBreak: true, width: 30.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // With long whitespace. - result = instance.measure(build(ahemStyle, 'abcd efg'), constraints)!; - expect(result.maxIntrinsicWidth, 100); - if (instance.isCanvas) { - expect(result.lines, [ - line('abcd ', 0, 7, hardBreak: false, width: 40.0, lineNumber: 0, left: 0.0), - line('efg', 7, 10, hardBreak: true, width: 30.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // With trailing whitespace. - result = instance.measure(build(ahemStyle, 'abc def '), constraints)!; - expect(result.maxIntrinsicWidth, 100); - if (instance.isCanvas) { - expect(result.lines, [ - line('abc ', 0, 4, hardBreak: false, width: 30.0, lineNumber: 0, left: 0.0), - line('def ', 4, 10, hardBreak: true, width: 30.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // With trailing whitespace and new lines. - result = instance.measure(build(ahemStyle, 'abc \ndef '), constraints)!; - expect(result.maxIntrinsicWidth, 60); - if (instance.isCanvas) { - expect(result.lines, [ - line('abc ', 0, 5, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('def ', 5, 11, hardBreak: true, width: 30.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Very long text. - result = instance.measure(build(ahemStyle, 'AAAAAAAAAAAA'), constraints)!; - expect(result.maxIntrinsicWidth, 120); - if (instance.isCanvas) { - expect(result.lines, [ - line('AAAAA', 0, 5, hardBreak: false, width: 50.0, lineNumber: 0, left: 0.0), - line('AAAAA', 5, 10, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('AA', 10, 12, hardBreak: true, width: 20.0, lineNumber: 2, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }); - - testMeasurements( - 'respects text overflow', - (TextMeasurementService instance) { - final ui.ParagraphStyle overflowStyle = ui.ParagraphStyle( - fontFamily: 'ahem', - fontSize: 10, - ellipsis: '...', - ); - - MeasurementResult result; - - // The text shouldn't be broken into multiple lines, so the height should - // be equal to a height of a single line. - final DomParagraph longText = build( - overflowStyle, - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - ); - result = instance.measure(longText, constraints)!; - expect(result.minIntrinsicWidth, 480); - expect(result.maxIntrinsicWidth, 480); - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line('AA...', 0, 48, hardBreak: false, width: 50.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement can't handle the ellipsis case very well. The - // text wraps into multiple lines instead. - expect(result.lines, isNull); - } - - // The short prefix should make the text break into two lines, but the - // second line should remain unbroken. - final DomParagraph longTextShortPrefix = build( - overflowStyle, - 'AAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - ); - result = instance.measure(longTextShortPrefix, constraints)!; - expect(result.minIntrinsicWidth, 450); - expect(result.maxIntrinsicWidth, 450); - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('AAA', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('AA...', 4, 49, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't handle the ellipsis case very well. The - // text wraps into multiple lines instead. - expect(result.lines, isNull); - } - - // Tiny constraints. - const ui.ParagraphConstraints tinyConstraints = - ui.ParagraphConstraints(width: 30); - final DomParagraph text = build(overflowStyle, 'AAAA'); - result = instance.measure(text, tinyConstraints)!; - expect(result.minIntrinsicWidth, 40); - expect(result.maxIntrinsicWidth, 40); - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line('...', 0, 4, hardBreak: false, width: 30.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement can't handle the ellipsis case very well. The - // text wraps into multiple lines instead. - expect(result.lines, isNull); - } - - // Tinier constraints (not enough for the ellipsis). - const ui.ParagraphConstraints tinierConstraints = - ui.ParagraphConstraints(width: 10); - result = instance.measure(text, tinierConstraints)!; - expect(result.minIntrinsicWidth, 40); - expect(result.maxIntrinsicWidth, 40); - expect(result.height, 10); - if (instance.isCanvas) { - // TODO(mdebbar): https://github.com/flutter/flutter/issues/34346 - // expect(result.lines, [ - // line('.', 0, 4, hardBreak: false, width: 10.0, lineNumber: 0, left: 0.0), - // ]); - expect(result.lines, [ - line('...', 0, 4, hardBreak: false, width: 30.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement can't handle the ellipsis case very well. The - // text wraps into multiple lines instead. - expect(result.lines, isNull); - } - }, - skipDom: browserEngine == BrowserEngine.webkit, - ); - - testMeasurements('respects max lines', (TextMeasurementService instance) { - final ui.ParagraphStyle maxlinesStyle = ui.ParagraphStyle( - fontFamily: 'ahem', - fontSize: 10, - maxLines: 2, - ); - - MeasurementResult result; - - // The height should be that of a single line. - final DomParagraph oneline = build(maxlinesStyle, 'One line'); - result = instance.measure(oneline, infiniteConstraints)!; - expect(result.height, 10); - expect(result.lines, [ - line('One line', 0, 8, hardBreak: true, width: 80.0, lineNumber: 0, left: 0.0), - ]); - - // The height should respect max lines and be limited to two lines here. - final DomParagraph threelines = - build(maxlinesStyle, 'First\nSecond\nThird'); - result = instance.measure(threelines, infiniteConstraints)!; - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('First', 0, 6, hardBreak: true, width: 50.0, lineNumber: 0, left: 0.0), - line('Second', 6, 13, hardBreak: true, width: 60.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // The height should respect max lines and be limited to two lines here. - final DomParagraph veryLong = build( - maxlinesStyle, - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - ); - result = instance.measure(veryLong, constraints)!; - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('Lorem ', 0, 6, hardBreak: false, width: 50.0, lineNumber: 0, left: 0.0), - line('ipsum ', 6, 12, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Case when last line is a long unbreakable word. - final DomParagraph veryLongLastLine = build( - maxlinesStyle, - 'AAA AAAAAAAAAAAAAAAAAAA', - ); - result = instance.measure(veryLongLastLine, constraints)!; - expect(result.height, 20); - if (instance.isCanvas) { - expect(result.lines, [ - line('AAA ', 0, 4, hardBreak: false, width: 30.0, lineNumber: 0, left: 0.0), - line('AAAAA', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }); - - testMeasurements( - 'respects text overflow and max lines combined', - (TextMeasurementService instance) { - const ui.ParagraphConstraints constraints = - ui.ParagraphConstraints(width: 60); - final ui.ParagraphStyle onelineStyle = ui.ParagraphStyle( - fontFamily: 'ahem', - fontSize: 10, - maxLines: 1, - ellipsis: '...', - ); - final ui.ParagraphStyle multilineStyle = ui.ParagraphStyle( - fontFamily: 'ahem', - fontSize: 10, - maxLines: 2, - ellipsis: '...', - ); - - DomParagraph p; - MeasurementResult result; - - // Simple no overflow case. - p = build(onelineStyle, 'abcdef'); - result = instance.measure(p, constraints)!; - expect(result.height, 10); - expect(result.lines, [ - line('abcdef', 0, 6, hardBreak: true, width: 60.0, lineNumber: 0, left: 0.0), - ]); - - // Simple overflow case. - p = build(onelineStyle, 'abcd efg'); - result = instance.measure(p, constraints)!; - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line('abc...', 0, 8, hardBreak: false, width: 60.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement can't handle the ellipsis case very well. The - // text wraps into multiple lines instead. - expect(result.lines, isNull); - } - - // Another simple overflow case. - p = build(onelineStyle, 'a bcde fgh'); - result = instance.measure(p, constraints)!; - expect(result.height, 10); - if (instance.isCanvas) { - expect(result.lines, [ - line('a b...', 0, 10, hardBreak: false, width: 60.0, lineNumber: 0, left: 0.0), - ]); - } else { - // DOM-based measurement can't handle the ellipsis case very well. The - // text wraps into multiple lines instead. - expect(result.lines, isNull); - } - - // The ellipsis is supposed to go on the second line, but because the - // 2nd line doesn't overflow, no ellipsis is shown. - p = build(multilineStyle, 'abcdef ghijkl'); - result = instance.measure(p, constraints)!; - // This can only be done correctly in the canvas-based implementation. - if (instance.isCanvas) { - expect(result.height, 20); - - expect(result.lines, [ - line('abcdef ', 0, 7, hardBreak: false, width: 60.0, lineNumber: 0, left: 0.0), - line('ghijkl', 7, 13, hardBreak: true, width: 60.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // But when the 2nd line is long enough, the ellipsis is shown. - p = build(multilineStyle, 'abcd efghijkl'); - result = instance.measure(p, constraints)!; - // This can only be done correctly in the canvas-based implementation. - if (instance.isCanvas) { - expect(result.height, 20); - - expect(result.lines, [ - line('abcd ', 0, 5, hardBreak: false, width: 40.0, lineNumber: 0, left: 0.0), - line('efg...', 5, 13, hardBreak: false, width: 60.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Even if the second line can be broken, we don't break it, we just - // insert the ellipsis. - p = build(multilineStyle, 'abcde f gh ijk'); - result = instance.measure(p, constraints)!; - // This can only be done correctly in the canvas-based implementation. - if (instance.isCanvas) { - expect(result.height, 20); - - expect(result.lines, [ - line('abcde ', 0, 6, hardBreak: false, width: 50.0, lineNumber: 0, left: 0.0), - line('f g...', 6, 14, hardBreak: false, width: 60.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // First line overflows but second line doesn't. - p = build(multilineStyle, 'abcdefg hijk'); - result = instance.measure(p, constraints)!; - // This can only be done correctly in the canvas-based implementation. - if (instance.isCanvas) { - expect(result.height, 20); - - expect(result.lines, [ - line('abcdef', 0, 6, hardBreak: false, width: 60.0, lineNumber: 0, left: 0.0), - line('g hijk', 6, 12, hardBreak: true, width: 60.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - - // Both first and second lines overflow. - p = build(multilineStyle, 'abcdefg hijklmnop'); - result = instance.measure(p, constraints)!; - // This can only be done correctly in the canvas-based implementation. - if (instance.isCanvas) { - expect(result.height, 20); - - expect(result.lines, [ - line('abcdef', 0, 6, hardBreak: false, width: 60.0, lineNumber: 0, left: 0.0), - line('g h...', 6, 17, hardBreak: false, width: 60.0, lineNumber: 1, left: 0.0), - ]); - } else { - // DOM-based measurement can't produce line metrics for multi-line - // paragraphs. - expect(result.lines, isNull); - } - }, - ); - - test('handles textAlign', () { - final TextMeasurementService instance = TextMeasurementService.canvasInstance; - DomParagraph p; - MeasurementResult result; - - ui.ParagraphStyle createStyle(ui.TextAlign textAlign) { - return ui.ParagraphStyle( - fontFamily: 'ahem', - fontSize: 10, - textAlign: textAlign, - textDirection: ui.TextDirection.ltr, - ); - } - - p = build(createStyle(ui.TextAlign.start), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 0.0), - ]); - - p = build(createStyle(ui.TextAlign.end), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 20.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 40.0), - ]); - - p = build(createStyle(ui.TextAlign.center), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 10.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 20.0), - ]); - - p = build(createStyle(ui.TextAlign.left), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 0.0), - ]); - - p = build(createStyle(ui.TextAlign.right), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 20.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 40.0), - ]); - }); - - testMeasurements( - 'handles rtl with textAlign', - (TextMeasurementService instance) { - final TextMeasurementService instance = TextMeasurementService.canvasInstance; - DomParagraph p; - MeasurementResult result; - - ui.ParagraphStyle createStyle(ui.TextAlign textAlign) { - return ui.ParagraphStyle( - fontFamily: 'ahem', - fontSize: 10, - textAlign: textAlign, - textDirection: ui.TextDirection.rtl, - ); - } - - p = build(createStyle(ui.TextAlign.start), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 20.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 40.0), - ]); - - p = build(createStyle(ui.TextAlign.end), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 0.0), - ]); - - p = build(createStyle(ui.TextAlign.center), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 10.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 20.0), - ]); - - p = build(createStyle(ui.TextAlign.left), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 0.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 0.0), - ]); - - p = build(createStyle(ui.TextAlign.right), 'abc\ndefghi'); - result = instance.measure(p, constraints)!; - expect(result.lines, [ - line('abc', 0, 4, hardBreak: true, width: 30.0, lineNumber: 0, left: 20.0), - line('defgh', 4, 9, hardBreak: false, width: 50.0, lineNumber: 1, left: 0.0), - line('i', 9, 10, hardBreak: true, width: 10.0, lineNumber: 2, left: 40.0), - ]); - }, - ); - }); -} - -/// Shortcut to avoid many line wraps in the tests above. -EngineLineMetrics line( - String displayText, - int startIndex, - int endIndex, { - required double width, - required int lineNumber, - required bool hardBreak, - required double left, -}) { - return EngineLineMetrics.withText( - displayText, - startIndex: startIndex, - endIndex: endIndex, - hardBreak: hardBreak, - width: width, - lineNumber: lineNumber, - left: left, - endIndexWithoutNewlines: -1, - widthWithTrailingSpaces: width, - ); -} From 33323c0df8dfc21d9056de00ac3b88bdd2a26da5 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Fri, 13 Aug 2021 14:18:29 -0700 Subject: [PATCH 2/3] fix tests using the old API --- .../lib/src/engine/text/canvas_paragraph.dart | 6 +- .../engine/surface/scene_builder_test.dart | 5 +- .../test/engine/web_experiments_test.dart | 67 ------ .../compositing/compositing_golden_test.dart | 12 +- .../paragraph/text_overflow_golden_test.dart | 72 ------ .../test/html/paragraph/text_scuba.dart | 36 +-- lib/web_ui/test/text_test.dart | 217 +++--------------- 7 files changed, 46 insertions(+), 369 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart index 48125a834f192..45c8023e9ffd3 100644 --- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart @@ -19,9 +19,9 @@ const ui.Color _defaultTextColor = ui.Color(0xFFFF0000); /// A paragraph made up of a flat list of text spans and placeholders. /// -/// As opposed to [DomParagraph], a [CanvasParagraph] doesn't use a DOM element -/// to represent the structure of its spans and styles. Instead it uses a flat -/// list of [ParagraphSpan] objects. +/// [CanvasParagraph] doesn't use a DOM element to represent the structure of +/// its spans and styles. Instead it uses a flat list of [ParagraphSpan] +/// objects. class CanvasParagraph implements EngineParagraph { /// This class is created by the engine, and should not be instantiated /// or extended directly. diff --git a/lib/web_ui/test/engine/surface/scene_builder_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart index 53635774a8946..e97999f05c679 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -468,7 +468,10 @@ void testMain() { final bool useOffset = int.tryParse(char) == null; final EnginePictureRecorder recorder = EnginePictureRecorder(); final RecordingCanvas canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 400, 400)); - final DomParagraph paragraph = (DomParagraphBuilder(EngineParagraphStyle())..addText(char)).build() as DomParagraph; + final ui.Paragraph paragraph = (ui.ParagraphBuilder(ui.ParagraphStyle()) + ..pushStyle(ui.TextStyle(decoration: ui.TextDecoration.lineThrough)) + ..addText(char)) + .build(); paragraph.layout(const ui.ParagraphConstraints(width: 1000)); canvas.drawParagraph(paragraph, ui.Offset.zero); final ui.EngineLayer newLayer = useOffset diff --git a/lib/web_ui/test/engine/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart index dba7f54948bb1..fd31fc9b97ed1 100644 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -9,9 +9,6 @@ import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine/web_experiments.dart'; -const bool _defaultUseCanvasText = true; -const bool _defaultUseCanvasRichText = true; - void main() { internalBootstrapBrowserTest(() => testMain); } @@ -25,70 +22,6 @@ void testMain() { WebExperiments.instance!.reset(); }); - test('default web experiment values', () { - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - }); - - test('can turn on/off web experiments', () { - WebExperiments.instance!.updateExperiment('useCanvasText', true); - WebExperiments.instance!.updateExperiment('useCanvasRichText', true); - expect(WebExperiments.instance!.useCanvasText, isTrue); - expect(WebExperiments.instance!.useCanvasRichText, isTrue); - - WebExperiments.instance!.updateExperiment('useCanvasText', false); - WebExperiments.instance!.updateExperiment('useCanvasRichText', false); - expect(WebExperiments.instance!.useCanvasText, isFalse); - expect(WebExperiments.instance!.useCanvasRichText, isFalse); - - WebExperiments.instance!.updateExperiment('useCanvasText', null); - WebExperiments.instance!.updateExperiment('useCanvasRichText', null); - // Goes back to default value. - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - }); - - test('ignores unknown experiments', () { - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - WebExperiments.instance!.updateExperiment('foobarbazqux', true); - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - WebExperiments.instance!.updateExperiment('foobarbazqux', false); - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - }); - - test('can reset web experiments', () { - WebExperiments.instance!.updateExperiment('useCanvasText', false); - WebExperiments.instance!.updateExperiment('useCanvasRichText', false); - WebExperiments.instance!.reset(); - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - - WebExperiments.instance!.updateExperiment('useCanvasText', false); - WebExperiments.instance!.updateExperiment('useCanvasRichText', false); - WebExperiments.instance!.updateExperiment('foobarbazqux', true); - WebExperiments.instance!.reset(); - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - }); - - test('js interop also works', () { - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - - expect(() => jsUpdateExperiment('useCanvasText', true), returnsNormally); - expect(() => jsUpdateExperiment('useCanvasRichText', true), returnsNormally); - expect(WebExperiments.instance!.useCanvasText, isTrue); - expect(WebExperiments.instance!.useCanvasRichText, isTrue); - - expect(() => jsUpdateExperiment('useCanvasText', null), returnsNormally); - expect(() => jsUpdateExperiment('useCanvasRichText', null), returnsNormally); - expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText); - expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText); - }); - test('js interop throws on wrong type', () { expect(() => jsUpdateExperiment(123, true), throwsA(anything)); expect(() => jsUpdateExperiment('foo', 123), throwsA(anything)); diff --git a/lib/web_ui/test/html/compositing/compositing_golden_test.dart b/lib/web_ui/test/html/compositing/compositing_golden_test.dart index 4c2e11ec40cca..4cfb4f4c2be13 100644 --- a/lib/web_ui/test/html/compositing/compositing_golden_test.dart +++ b/lib/web_ui/test/html/compositing/compositing_golden_test.dart @@ -818,10 +818,12 @@ void _testCullRectComputation() { 'renders clipped text with high quality', () async { // To reproduce blurriness we need real clipping. - final DomParagraph paragraph = - (DomParagraphBuilder(EngineParagraphStyle(fontFamily: 'Roboto')) + final CanvasParagraph paragraph = + (ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: 'Roboto')) + // Use a decoration to force rendering in DOM mode. + ..pushStyle(ui.TextStyle(decoration: ui.TextDecoration.lineThrough, decorationColor: const ui.Color(0x00000000))) ..addText('Am I blurry?')) - .build() as DomParagraph; + .build() as CanvasParagraph; paragraph.layout(const ui.ParagraphConstraints(width: 1000)); final ui.Rect canvasSize = ui.Rect.fromLTRB( @@ -843,7 +845,7 @@ void _testCullRectComputation() { final RecordingCanvas canvas = recorder.beginRecording(outerClip); canvas.drawParagraph(paragraph, const ui.Offset(8.5, 8.5)); final ui.Picture picture = recorder.endRecording(); - expect(canvas.renderStrategy.hasArbitraryPaint, isFalse); + expect(paragraph.drawOnCanvas, isFalse); builder.addPicture( ui.Offset.zero, @@ -857,7 +859,7 @@ void _testCullRectComputation() { final RecordingCanvas canvas = recorder.beginRecording(innerClip); canvas.drawParagraph(paragraph, ui.Offset(8.5, 8.5 + innerClip.top)); final ui.Picture picture = recorder.endRecording(); - expect(canvas.renderStrategy.hasArbitraryPaint, isFalse); + expect(paragraph.drawOnCanvas, isFalse); builder.addPicture( ui.Offset.zero, diff --git a/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart b/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart index 4bd1638a989aa..f8b954e07644b 100644 --- a/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart +++ b/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart @@ -13,10 +13,6 @@ import 'text_scuba.dart'; typedef CanvasTest = FutureOr Function(EngineCanvas canvas); const String threeLines = 'First\nSecond\nThird'; -const String veryLongWithShortPrefix = - 'Lorem ipsum dolor\nsit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; -const String veryLongWithShortSuffix = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.'; const String veryLong = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; const String longUnbreakable = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; @@ -30,18 +26,8 @@ Future testMain() async { viewportSize: const Size(800, 800), ); - final TextStyle warningStyle = TextStyle( - color: const Color(0xFFFF0000), - fontFamily: 'Roboto', - fontSize: 10, - ); - setUpStableTestFonts(); - EngineParagraph warning(String text) { - return paragraph(text, textStyle: warningStyle); - } - testEachCanvas('maxLines clipping', (EngineCanvas canvas) { Offset offset = Offset.zero; EngineParagraph p; @@ -70,64 +56,6 @@ Future testMain() async { return scuba.diffCanvasScreenshot(canvas, 'text_max_lines'); }); - testEachCanvas('maxLines with overflow', (EngineCanvas canvas) { - Offset offset = Offset.zero; - EngineParagraph p; - - // Only the first line is rendered with no ellipsis because the first line - // doesn't overflow. - p = paragraph( - threeLines, - paragraphStyle: ParagraphStyle(ellipsis: '...'), - ); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - // The first two lines are rendered with an ellipsis on the 2nd line. - p = paragraph( - veryLongWithShortPrefix, - paragraphStyle: ParagraphStyle(ellipsis: '...'), - maxWidth: 200, - ); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - // Only the first line is rendered with an ellipsis. - if (!WebExperiments.instance!.useCanvasText) { - // This is now correct with the canvas-based measurement, so we shouldn't - // print the "(wrong)" warning. - p = warning('(wrong)'); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height); - } - p = paragraph( - veryLongWithShortSuffix, - paragraphStyle: ParagraphStyle(ellipsis: '...'), - maxWidth: 200, - ); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - // Only the first two lines are rendered and the ellipsis appears on the 2nd - // line. - if (!WebExperiments.instance!.useCanvasText) { - // This is now correct with the canvas-based measurement, so we shouldn't - // print the "(wrong)" warning. - p = warning('(wrong)'); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height); - } - p = paragraph( - veryLong, - paragraphStyle: ParagraphStyle(maxLines: 2, ellipsis: '...'), - maxWidth: 200, - ); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - return scuba.diffCanvasScreenshot(canvas, 'text_max_lines_with_ellipsis'); - }); - testEachCanvas('long unbreakable text', (EngineCanvas canvas) { Offset offset = Offset.zero; EngineParagraph p; diff --git a/lib/web_ui/test/html/paragraph/text_scuba.dart b/lib/web_ui/test/html/paragraph/text_scuba.dart index 4b245a946aa82..e63718242bf74 100644 --- a/lib/web_ui/test/html/paragraph/text_scuba.dart +++ b/lib/web_ui/test/html/paragraph/text_scuba.dart @@ -71,7 +71,7 @@ class EngineScubaTester { sceneElement.append(canvas.rootElement); html.document.body!.append(sceneElement); String screenshotName = '${fileName}_${canvas.runtimeType}'; - if (WebExperiments.instance!.useCanvasText) { + if (canvas is BitmapCanvas) { screenshotName += '+canvas_measurement'; } await diffScreenshot( @@ -94,41 +94,11 @@ typedef CanvasTest = FutureOr Function(EngineCanvas canvas); void testEachCanvas(String description, CanvasTest body, {double? maxDiffRate}) { const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800); - test('$description (bitmap)', () { - try { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - WebExperiments.instance!.useCanvasText = false; - WebExperiments.instance!.useCanvasRichText = false; - return body(BitmapCanvas(bounds, RenderStrategy())); - } finally { - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - TextMeasurementService.clearCache(); - } - }); test('$description (bitmap + canvas measurement)', () async { - try { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - WebExperiments.instance!.useCanvasText = true; - WebExperiments.instance!.useCanvasRichText = false; - await body(BitmapCanvas(bounds, RenderStrategy())); - } finally { - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - TextMeasurementService.clearCache(); - } + return body(BitmapCanvas(bounds, RenderStrategy())); }); test('$description (dom)', () { - try { - TextMeasurementService.initialize(rulerCacheCapacity: 2); - WebExperiments.instance!.useCanvasText = false; - WebExperiments.instance!.useCanvasRichText = false; - return body(DomCanvas(domRenderer.createElement('flt-picture'))); - } finally { - WebExperiments.instance!.useCanvasText = null; - WebExperiments.instance!.useCanvasRichText = null; - TextMeasurementService.clearCache(); - } + return body(DomCanvas(domRenderer.createElement('flt-picture'))); }); } diff --git a/lib/web_ui/test/text_test.dart b/lib/web_ui/test/text_test.dart index 87acff462b013..d51fb80fa6197 100644 --- a/lib/web_ui/test/text_test.dart +++ b/lib/web_ui/test/text_test.dart @@ -10,6 +10,7 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; +import 'html/paragraph/helper.dart'; import 'matchers.dart'; void main() { @@ -88,16 +89,13 @@ Future testMain() async { }); test('lay out unattached paragraph', () { - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + final CanvasParagraph paragraph = plain(EngineParagraphStyle( fontFamily: 'sans-serif', fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, fontSize: 14.0, - )); - builder.addText('How do you do this fine morning?'); - final DomParagraph paragraph = builder.build() as DomParagraph; + ), 'How do you do this fine morning?'); - expect(paragraph.paragraphElement.parent, isNull); expect(paragraph.height, 0.0); expect(paragraph.width, -1.0); expect(paragraph.minIntrinsicWidth, 0.0); @@ -107,7 +105,6 @@ Future testMain() async { paragraph.layout(const ParagraphConstraints(width: 60.0)); - expect(paragraph.paragraphElement.parent, isNull); expect(paragraph.height, greaterThan(0.0)); expect(paragraph.width, greaterThan(0.0)); expect(paragraph.minIntrinsicWidth, greaterThan(0.0)); @@ -154,18 +151,18 @@ Future testMain() async { }); test('$ParagraphBuilder detects plain text', () { - DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle( fontFamily: 'sans-serif', fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, fontSize: 15.0, )); builder.addText('hi'); - DomParagraph paragraph = builder.build() as DomParagraph; + CanvasParagraph paragraph = builder.build() as CanvasParagraph; expect(paragraph.plainText, isNotNull); - expect(paragraph.geometricStyle.fontWeight, FontWeight.normal); + expect(paragraph.paragraphStyle.fontWeight, FontWeight.normal); - builder = DomParagraphBuilder(EngineParagraphStyle( + builder = ParagraphBuilder(EngineParagraphStyle( fontFamily: 'sans-serif', fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, @@ -173,13 +170,12 @@ Future testMain() async { )); builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); builder.addText('hi'); - paragraph = builder.build() as DomParagraph; + paragraph = builder.build() as CanvasParagraph; expect(paragraph.plainText, isNotNull); - expect(paragraph.geometricStyle.fontWeight, FontWeight.bold); }); test('$ParagraphBuilder detects rich text', () { - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + final ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle( fontFamily: 'sans-serif', fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, @@ -188,176 +184,27 @@ Future testMain() async { builder.addText('h'); builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); builder.addText('i'); - final DomParagraph paragraph = builder.build() as DomParagraph; - expect(paragraph.plainText, isNull); - expect(paragraph.geometricStyle.fontWeight, FontWeight.normal); + final CanvasParagraph paragraph = builder.build() as CanvasParagraph; + expect(paragraph.plainText, 'hi'); }); test('$ParagraphBuilder treats empty text as plain', () { - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + final ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle( fontFamily: 'sans-serif', fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, fontSize: 15.0, )); builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); - final DomParagraph paragraph = builder.build() as DomParagraph; + final CanvasParagraph paragraph = builder.build() as CanvasParagraph; expect(paragraph.plainText, ''); - expect(paragraph.geometricStyle.fontWeight, FontWeight.bold); - }); - - // Regression test for https://github.com/flutter/flutter/issues/34931. - test('hit test on styled text returns correct span offset', () { - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( - fontFamily: 'sans-serif', - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - fontSize: 15.0, - )); - builder.pushStyle(TextStyle(fontWeight: FontWeight.bold)); - const String firstSpanText = 'XYZ'; - builder.addText(firstSpanText); - builder.pushStyle(TextStyle(fontWeight: FontWeight.normal)); - const String secondSpanText = '1234'; - builder.addText(secondSpanText); - builder.pushStyle(TextStyle(fontStyle: FontStyle.italic)); - builder.addText('followed by a link'); - final DomParagraph paragraph = builder.build() as DomParagraph; - paragraph.layout(const ParagraphConstraints(width: 800.0)); - expect(paragraph.plainText, isNull); - const int secondSpanStartPosition = firstSpanText.length; - const int thirdSpanStartPosition = - firstSpanText.length + secondSpanText.length; - expect(paragraph.getPositionForOffset(const Offset(50, 0)).offset, - secondSpanStartPosition); - expect(paragraph.getPositionForOffset(const Offset(150, 0)).offset, - thirdSpanStartPosition); - }); - - test('hit test on the nested text span and returns correct span offset', () { - const String fontFamily = 'sans-serif'; - const double fontSize = 20.0; - final TextStyle style = TextStyle(fontFamily: fontFamily, fontSize: fontSize); - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( - fontFamily: fontFamily, - fontSize: fontSize, - )); - - const String text00 = 'test test test test test te00 '; - const String text010 = 'test010 '; - const String text02 = 'test test test test te02 '; - const String text030 = 'test030 '; - const String text04 = 'test test test test test test test test test test te04 '; - const String text050 = 'test050 '; - - /* Logical arrangement: Tree - - Root TextSpan: 0 - */ - builder.pushStyle(style); - { - // 1st child TextSpan of Root: 0.0 - builder.pushStyle(style); - builder.addText(text00); - builder.pop(); - - // 2nd child TextSpan of Root: 0.1 - builder.pushStyle(style); - { - // 1st child TextSpan of 0.1: 0.1.0 - builder.pushStyle(style); - builder.addText(text010); - builder.pop(); - } - builder.pop(); - - // 3rd child TextSpan of Root: 0.2 - builder.pushStyle(style); - builder.addText(text02); - builder.pop(); - - // 4th child TextSpan of Root: 0.3 - builder.pushStyle(style); - { - // 1st child TextSpan of 0.3: 0.3.0 - builder.pushStyle(style); - builder.addText(text030); - builder.pop(); - } - builder.pop(); - - // 5th child TextSpan of Root: 0.4 - builder.pushStyle(style); - builder.addText(text04); - builder.pop(); - - // 6th child TextSpan of Root: 0.5 - builder.pushStyle(style); - { - // 1st child TextSpan of 0.5: 0.5.0 - builder.pushStyle(style); - builder.addText(text050); - builder.pop(); - } - builder.pop(); - } - builder.pop(); - - /* Display arrangement: Visible texts - - Because `const fontSize = 20.0`, the width of each character is 20 and the - height is 20. `Display arrangement` squashes `Logical arrangement` to the - (x, y) plane. That means `Display arrangement` only shows the visible texts. - The order of texts is text00 --> text010 --> text02 --> text030 --> text04 - --> text050. - - The output is like that. - - |------------ 600 ------------| Begin of test010 - |--------------- 760 ----------------| End of test010 - |---------- 500 ---------| Begin of test030 - |------------- 660 -------------| End of test030 - |-- 180 --| Begin of test050 - |------ 360 -----| End of test050 - 'test test test test test te00 test010 ' - 'test test test test te02 test030 test ' - 'test test test test test test test test ' - 'test te04 test050 ' - */ - - final DomParagraph paragraph = builder.build() as DomParagraph; - paragraph.layout(const ParagraphConstraints(width: 800)); - - // Reference the offsets with the output of `Display arrangement`. - const int offset010 = text00.length; - const int offset030 = offset010 + text010.length + text02.length; - const int offset04 = offset030 + text030.length; - const int offset050 = offset04 + text04.length; - // Tap text010. - expect(paragraph.getPositionForOffset(const Offset(700, 10)).offset, offset010); - // Tap text030 - expect(paragraph.getPositionForOffset(const Offset(600, 30)).offset, offset030); - // Tap text050 - expect(paragraph.getPositionForOffset(const Offset(220, 70)).offset, offset050); - // Tap the left neighbor of text050 - expect(paragraph.getPositionForOffset(const Offset(199, 70)).offset, offset04); - // Tap the right neighbor of text050. No matter who the right neighbor of - // text0505 is, it must not be text050 itself. - expect(paragraph.getPositionForOffset(const Offset(360, 70)).offset, - isNot(offset050)); - // Tap the neighbor above text050 - expect(paragraph.getPositionForOffset(const Offset(220, 59)).offset, offset04); - // Tap the neighbor below text050. No matter who the neighbor above text050, - // it must not be text050 itself. - expect(paragraph.getPositionForOffset(const Offset(220, 80)).offset, - isNot(offset050)); }); // Regression test for https://github.com/flutter/flutter/issues/38972 test( 'should not set fontFamily to effectiveFontFamily for spans in rich text', () { - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + final ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle( fontFamily: 'Roboto', fontStyle: FontStyle.normal, fontWeight: FontWeight.normal, @@ -370,11 +217,11 @@ Future testMain() async { builder.pushStyle(TextStyle(fontSize: 30.0, fontWeight: FontWeight.normal)); const String secondSpanText = 'def'; builder.addText(secondSpanText); - final DomParagraph paragraph = builder.build() as DomParagraph; + final CanvasParagraph paragraph = builder.build() as CanvasParagraph; paragraph.layout(const ParagraphConstraints(width: 800.0)); - expect(paragraph.plainText, isNull); + expect(paragraph.plainText, 'abcdef'); final List spans = - paragraph.paragraphElement.querySelectorAll('span'); + paragraph.toDomElement().querySelectorAll('span'); expect(spans[0].style.fontFamily, 'Ahem, $fallback, sans-serif'); // The nested span here should not set it's family to default sans-serif. expect(spans[1].style.fontFamily, 'Ahem, $fallback, sans-serif'); @@ -388,15 +235,13 @@ Future testMain() async { // Set this to false so it doesn't default to 'Ahem' font. debugEmulateFlutterTesterEnvironment = false; - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + final CanvasParagraph paragraph = plain(EngineParagraphStyle( fontFamily: 'SomeFont', fontSize: 12.0, - )); - - builder.addText('Hello'); + ), 'Hello'); - final DomParagraph paragraph = builder.build() as DomParagraph; - expect(paragraph.paragraphElement.style.fontFamily, + paragraph.layout(constrain(double.infinity)); + expect(paragraph.toDomElement().style.fontFamily, 'SomeFont, $fallback, sans-serif'); debugEmulateFlutterTesterEnvironment = true; @@ -410,15 +255,13 @@ Future testMain() async { // Set this to false so it doesn't default to 'Ahem' font. debugEmulateFlutterTesterEnvironment = false; - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + final CanvasParagraph paragraph = plain(EngineParagraphStyle( fontFamily: 'serif', fontSize: 12.0, - )); + ), 'Hello'); - builder.addText('Hello'); - - final DomParagraph paragraph = builder.build() as DomParagraph; - expect(paragraph.paragraphElement.style.fontFamily, 'serif'); + paragraph.layout(constrain(double.infinity)); + expect(paragraph.toDomElement().style.fontFamily, 'serif'); debugEmulateFlutterTesterEnvironment = true; }); @@ -427,15 +270,13 @@ Future testMain() async { // Set this to false so it doesn't default to 'Ahem' font. debugEmulateFlutterTesterEnvironment = false; - final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle( + final CanvasParagraph paragraph = plain(EngineParagraphStyle( fontFamily: 'MyFont 2000', fontSize: 12.0, - )); - - builder.addText('Hello'); + ), 'Hello'); - final DomParagraph paragraph = builder.build() as DomParagraph; - expect(paragraph.paragraphElement.style.fontFamily, + paragraph.layout(constrain(double.infinity)); + expect(paragraph.toDomElement().style.fontFamily, '"MyFont 2000", $fallback, sans-serif'); debugEmulateFlutterTesterEnvironment = true; From ed1547583b10d7c8a5e1b95011c072507dcf0d5f Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Fri, 13 Aug 2021 15:18:37 -0700 Subject: [PATCH 3/3] remove tests that rely on old behavior --- .../paragraph/text_overflow_golden_test.dart | 31 --- .../paragraph/text_style_golden_test.dart | 248 ------------------ 2 files changed, 279 deletions(-) delete mode 100644 lib/web_ui/test/html/paragraph/text_style_golden_test.dart diff --git a/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart b/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart index f8b954e07644b..234df6027d4c0 100644 --- a/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart +++ b/lib/web_ui/test/html/paragraph/text_overflow_golden_test.dart @@ -55,35 +55,4 @@ Future testMain() async { return scuba.diffCanvasScreenshot(canvas, 'text_max_lines'); }); - - testEachCanvas('long unbreakable text', (EngineCanvas canvas) { - Offset offset = Offset.zero; - EngineParagraph p; - - // The whole line is rendered unbroken when there are no constraints. - p = paragraph(longUnbreakable); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - // The whole line is rendered with an ellipsis. - p = paragraph( - longUnbreakable, - paragraphStyle: ParagraphStyle(ellipsis: '...'), - maxWidth: 200, - ); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - // The text is broken into multiple lines. - p = paragraph(longUnbreakable, maxWidth: 200); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - // Very narrow constraint (less than one character's width). - p = paragraph('AA', maxWidth: 7); - canvas.drawParagraph(p, offset); - offset = offset.translate(0, p.height + 10); - - return scuba.diffCanvasScreenshot(canvas, 'text_long_unbreakable'); - }); } diff --git a/lib/web_ui/test/html/paragraph/text_style_golden_test.dart b/lib/web_ui/test/html/paragraph/text_style_golden_test.dart deleted file mode 100644 index 017ca9216d39e..0000000000000 --- a/lib/web_ui/test/html/paragraph/text_style_golden_test.dart +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:test/bootstrap/browser.dart'; -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart'; - -import 'text_scuba.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -Future testMain() async { - final EngineScubaTester scuba = await EngineScubaTester.initialize( - viewportSize: const Size(800, 800), - ); - - setUpStableTestFonts(); - - void drawLetterAndWordSpacing(EngineCanvas canvas) { - Offset offset = Offset.zero; - - for (double spacing = 0; spacing < 15; spacing += 5) { - canvas.drawParagraph( - paragraph('HelloWorld', - textStyle: TextStyle( - color: const Color(0xFF000000), - decoration: TextDecoration.none, - fontFamily: 'Roboto', - fontSize: 30, - letterSpacing: spacing)), - offset, - ); - offset = offset.translate(0, 40); - } - for (double spacing = 0; spacing < 30; spacing += 10) { - final TextStyle textStyle = TextStyle( - color: const Color(0xFF00FF00), - decoration: TextDecoration.none, - fontFamily: 'Roboto', - fontSize: 30, - wordSpacing: spacing); - canvas.drawParagraph( - paragraph('Hello World', textStyle: textStyle, maxWidth: 600), - offset, - ); - offset = offset.translate(0, 40); - } - } - - testEachCanvas('draws text with letter/word spacing', (EngineCanvas canvas) { - drawLetterAndWordSpacing(canvas); - return scuba.diffCanvasScreenshot( - canvas, 'paint_bounds_for_text_style_letter_spacing'); - }); - - void drawTextDecorationStyle(EngineCanvas canvas) { - final List decorationStyles = [ - TextDecorationStyle.solid, - TextDecorationStyle.double, - TextDecorationStyle.dotted, - TextDecorationStyle.dashed, - TextDecorationStyle.wavy, - ]; - - Offset offset = Offset.zero; - - for (final TextDecorationStyle decorationStyle in decorationStyles) { - final TextStyle textStyle = TextStyle( - color: const Color.fromRGBO(50, 50, 255, 1.0), - decoration: TextDecoration.underline, - decorationStyle: decorationStyle, - decorationColor: const Color.fromRGBO(50, 50, 50, 1.0), - fontFamily: 'Roboto', - fontSize: 30, - ); - canvas.drawParagraph( - paragraph('Hello World', textStyle: textStyle, maxWidth: 600), - offset, - ); - offset = offset.translate(0, 40); - } - } - - testEachCanvas('draws text decoration style', (EngineCanvas canvas) { - drawTextDecorationStyle(canvas); - return scuba.diffCanvasScreenshot( - canvas, 'paint_bounds_for_text_decorationStyle'); - }); - - void drawTextDecoration(EngineCanvas canvas) { - final List decorations = [ - TextDecoration.overline, - TextDecoration.underline, - TextDecoration.combine([ - TextDecoration.underline, - TextDecoration.lineThrough - ]), - TextDecoration.combine( - [TextDecoration.underline, TextDecoration.overline]), - TextDecoration.combine( - [TextDecoration.overline, TextDecoration.lineThrough]) - ]; - - Offset offset = Offset.zero; - - for (final TextDecoration decoration in decorations) { - final TextStyle textStyle = TextStyle( - color: const Color.fromRGBO(50, 50, 255, 1.0), - decoration: decoration, - decorationStyle: TextDecorationStyle.solid, - decorationColor: const Color.fromRGBO(255, 160, 0, 1.0), - fontFamily: 'Roboto', - fontSize: 20, - ); - canvas.drawParagraph( - paragraph( - 'Hello World $decoration', - textStyle: textStyle, - maxWidth: 600, - ), - offset, - ); - offset = offset.translate(0, 40); - } - } - - testEachCanvas('draws text decoration', (EngineCanvas canvas) { - drawTextDecoration(canvas); - return scuba.diffCanvasScreenshot( - canvas, 'paint_bounds_for_text_decoration'); - }); - - void drawTextWithBackground(EngineCanvas canvas) { - // Single-line text. - canvas.drawParagraph( - paragraph( - 'Hello World', - maxWidth: 600, - textStyle: TextStyle( - color: const Color.fromRGBO(0, 0, 0, 1.0), - background: Paint()..color = const Color.fromRGBO(255, 50, 50, 1.0), - fontFamily: 'Roboto', - fontSize: 30, - ), - ), - Offset.zero, - ); - - // Multi-line text. - canvas.drawParagraph( - paragraph( - 'Multi line Hello World paragraph', - maxWidth: 200, - textStyle: TextStyle( - color: const Color.fromRGBO(0, 0, 0, 1.0), - background: Paint()..color = const Color.fromRGBO(50, 50, 255, 1.0), - fontFamily: 'Roboto', - fontSize: 30, - ), - ), - const Offset(0, 40), - ); - } - - void drawTextWithShadow(EngineCanvas canvas) { - // Single-line text. - canvas.drawParagraph( - paragraph( - 'Hello World', - maxWidth: 600, - textStyle: TextStyle( - color: const Color.fromRGBO(0, 0, 0, 1.0), - background: Paint()..color = const Color.fromRGBO(255, 50, 50, 1.0), - fontFamily: 'Roboto', - fontSize: 30, - shadows: [ - const Shadow( - blurRadius: 0, - color: Color.fromRGBO(255, 0, 255, 1.0), - offset: Offset(10, 5), - ), - ], - ), - ), - Offset.zero, - ); - - // Multi-line text. - canvas.drawParagraph( - paragraph( - 'Multi line Hello World paragraph', - maxWidth: 200, - textStyle: TextStyle( - color: const Color.fromRGBO(0, 0, 0, 1.0), - background: Paint()..color = const Color.fromRGBO(50, 50, 255, 1.0), - fontFamily: 'Roboto', - fontSize: 30, - shadows: [ - const Shadow( - blurRadius: 0, - color: Color.fromRGBO(255, 0, 255, 1.0), - offset: Offset(10, 5), - ), - const Shadow( - blurRadius: 0, - color: Color.fromRGBO(0, 255, 255, 1.0), - offset: Offset(-10, -5), - ), - ], - ), - ), - const Offset(0, 40), - ); - } - - testEachCanvas('draws text with a background', (EngineCanvas canvas) { - drawTextWithBackground(canvas); - return scuba.diffCanvasScreenshot(canvas, 'text_background'); - }); - - testEachCanvas('draws text with a shadow', (EngineCanvas canvas) { - drawTextWithShadow(canvas); - return scuba.diffCanvasScreenshot(canvas, 'text_shadow', maxDiffRatePercent: 0.2); - }); - - testEachCanvas('Handles disabled strut style', (EngineCanvas canvas) { - // Flutter uses [StrutStyle.disabled] for the [SelectableText] widget. This - // translates into a strut style with a [height] of 0, which wasn't being - // handled correctly by the web engine. - final StrutStyle disabled = StrutStyle(height: 0, leading: 0); - canvas.drawParagraph( - paragraph( - 'Hello\nWorld', - paragraphStyle: ParagraphStyle(strutStyle: disabled), - ), - Offset.zero, - ); - return scuba.diffCanvasScreenshot( - canvas, - 'text_strut_style_disabled', - region: const Rect.fromLTRB(0, 0, 100, 100), - maxDiffRatePercent: 0.0, - ); - }); -}