mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Merge 19188b4e6e1c10e11706fe7e70f5e703588d5ad8 into a8911cbac88ef8f73c083465d0cbc35a7537f35f
This commit is contained in:
commit
bb8a5d239e
@ -167,6 +167,8 @@ export 'engine/web_paragraph/debug.dart';
|
||||
export 'engine/web_paragraph/font_collection.dart';
|
||||
export 'engine/web_paragraph/layout.dart';
|
||||
export 'engine/web_paragraph/paint.dart';
|
||||
export 'engine/web_paragraph/paint_clusters.dart';
|
||||
export 'engine/web_paragraph/paint_paragraph.dart';
|
||||
export 'engine/web_paragraph/painter.dart';
|
||||
export 'engine/web_paragraph/paragraph.dart';
|
||||
export 'engine/web_paragraph/wrapper.dart';
|
||||
|
||||
@ -2,10 +2,16 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import '../../engine.dart';
|
||||
|
||||
typedef Entry = ({String group, String name});
|
||||
|
||||
/// Debugging utilities for WebParagraph.
|
||||
class WebParagraphDebug {
|
||||
static bool logging = false;
|
||||
static bool apiLogging = false;
|
||||
|
||||
/// Logs a debug message if logging is enabled.
|
||||
static void log(String arg) {
|
||||
assert(() {
|
||||
if (logging) {
|
||||
@ -15,6 +21,7 @@ class WebParagraphDebug {
|
||||
}());
|
||||
}
|
||||
|
||||
/// Logs an API trace message if API logging is enabled.
|
||||
static void apiTrace(String arg) {
|
||||
assert(() {
|
||||
if (apiLogging || logging) {
|
||||
@ -24,6 +31,7 @@ class WebParagraphDebug {
|
||||
}());
|
||||
}
|
||||
|
||||
/// Logs an API warning message.
|
||||
static void warning(String arg) {
|
||||
assert(() {
|
||||
print('WARNING: $arg');
|
||||
@ -31,6 +39,7 @@ class WebParagraphDebug {
|
||||
}());
|
||||
}
|
||||
|
||||
/// Logs an API error message.
|
||||
static void error(String arg) {
|
||||
assert(() {
|
||||
print('ERROR: $arg');
|
||||
@ -38,3 +47,37 @@ class WebParagraphDebug {
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// Profiler for WebParagraph related operations.
|
||||
class WebParagraphProfiler {
|
||||
static Map<String, Duration> durations = {};
|
||||
static Map<String, int> counts = {};
|
||||
|
||||
/// Register an engine benchmark callback to collect profiling data for WebParagraph operations.
|
||||
static void register() {
|
||||
if (!Profiler.isBenchmarkMode) {
|
||||
return;
|
||||
}
|
||||
Profiler.ensureInitialized();
|
||||
engineBenchmarkValueCallback = (String name, double value) {
|
||||
counts[name] = (counts[name] ?? 0) + 1;
|
||||
durations[name] = (durations[name] ?? Duration.zero) + Duration(microseconds: value.toInt());
|
||||
};
|
||||
}
|
||||
|
||||
/// Logs the collected profiling information to the console.
|
||||
static void log() {
|
||||
if (!Profiler.isBenchmarkMode) {
|
||||
return;
|
||||
}
|
||||
for (final MapEntry<String, Duration> entry in durations.entries) {
|
||||
print('${entry.key}: ${entry.value.inMilliseconds}ms');
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the collected profiling information.
|
||||
static void reset() {
|
||||
durations = {};
|
||||
counts = {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,15 +143,11 @@ class TextLayout {
|
||||
paragraph.paragraphStyle.textDirection,
|
||||
);
|
||||
|
||||
WebParagraphDebug.log('Bidis ${paragraph.paragraphStyle.textDirection}:${regions.length}');
|
||||
for (final region in regions) {
|
||||
// Regions operate in text indexes, not cluster indexes (one cluster can contain several text points)
|
||||
// We need to convert one into another
|
||||
final ClusterRange clusterRange = _mapping.toClusterRange(region.start, region.end);
|
||||
final run = BidiRun(clusterRange, region.level);
|
||||
WebParagraphDebug.log(
|
||||
'region ${region.level.isEven ? 'ltr' : 'rtl'} [${region.start}:${region.end}) => $clusterRange',
|
||||
);
|
||||
bidiRuns.add(run);
|
||||
}
|
||||
}
|
||||
@ -333,10 +329,6 @@ class TextLayout {
|
||||
|
||||
// This is the intersection of the bidi region + line + span.
|
||||
final ui.TextRange bidiLineSpanTextRange = bidiLineTextRange.intersect(span);
|
||||
WebParagraphDebug.log(
|
||||
'Style: ${span as ui.TextRange} & $bidiLineTextRange = $bidiLineSpanTextRange ',
|
||||
);
|
||||
|
||||
final ClusterRange bidiLineSpanRange = _mapping.toClusterRange(
|
||||
bidiLineSpanTextRange.start,
|
||||
bidiLineSpanTextRange.end,
|
||||
@ -384,9 +376,6 @@ class TextLayout {
|
||||
(line.visualBlocks.last as TextBlock).clusterRangeWithoutWhitespaces = _mapping
|
||||
.toClusterRange(blockLineNoWhitespaces.start, blockLineNoWhitespaces.end);
|
||||
(line.visualBlocks.last as TextBlock).whitespacesWidth = trailingSpacesWidth;
|
||||
WebParagraphDebug.log(
|
||||
'TRAILING: $bidiLineSpanTextRange $blockLineNoWhitespaces $trailingSpacesWidth',
|
||||
);
|
||||
}
|
||||
|
||||
// Line always counts multipled metrics (no need for the others)
|
||||
@ -422,8 +411,10 @@ class TextLayout {
|
||||
ellipsisBlock.shiftFromLineStart = blockShiftFromLineStart;
|
||||
ellipsisBlock.spanShiftFromLineStart = blockShiftFromLineStart;
|
||||
line.visualBlocks.add(ellipsisBlock);
|
||||
line.trailingSpacesWidth = 0.0;
|
||||
blockShiftFromLineStart += ellipsisBlock.advance.width;
|
||||
} else {
|
||||
// We place the ellipsis block aat the beginning of the line (for RTL paragraph)
|
||||
// We place the ellipsis block at the beginning of the line (for RTL paragraph)
|
||||
line.visualBlocks.insert(0, ellipsisBlock);
|
||||
}
|
||||
}
|
||||
@ -438,11 +429,6 @@ class TextLayout {
|
||||
// TODO(jlavrova): sort our alphabetic/ideographic baseline and how it affects ascent & descent
|
||||
line.fontBoundingBoxAscent = math.max(line.fontBoundingBoxAscent, block.ascent);
|
||||
line.fontBoundingBoxDescent = math.max(line.fontBoundingBoxDescent, block.descent);
|
||||
WebParagraphDebug.log(
|
||||
'Adjusted metrics: '
|
||||
'${line.fontBoundingBoxAscent} => ${math.max(line.fontBoundingBoxAscent, block.ascent)} '
|
||||
'${line.fontBoundingBoxDescent} => ${math.max(line.fontBoundingBoxDescent, block.descent)} ',
|
||||
);
|
||||
}
|
||||
|
||||
line.advance = ui.Rect.fromLTWH(
|
||||
@ -456,11 +442,6 @@ class TextLayout {
|
||||
line.trailingSpacesWidth = trailingSpacesWidth;
|
||||
lines.add(line);
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'Line [${line.textClusterRange.start}:${line.textClusterRange.end}) ${line.advance.left},${line.advance.top} ${line.advance.width}x${line.advance.height} '
|
||||
'${ellipsisClusters.isNotEmpty ? 'Ellipsis: "${paragraph.paragraphStyle.ellipsis}" ${ellipsisClusters.length}' : ''}',
|
||||
);
|
||||
|
||||
return line.advance.height;
|
||||
}
|
||||
|
||||
@ -513,10 +494,12 @@ class TextLayout {
|
||||
// TODO(mdebbar): Instead of nested loops, make them two consecutive loops.
|
||||
for (var lineIndex = 0; lineIndex < lines.length; ++lineIndex) {
|
||||
final TextLine line = lines[lineIndex];
|
||||
WebParagraphDebug.log(
|
||||
'Line: ${line.textClusterRange} & $textRange '
|
||||
'[${line.advance.left}:${line.advance.right} x ${line.advance.top}:${line.advance.bottom}] ',
|
||||
);
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'Line: ${line.textClusterRange} & $textRange '
|
||||
'[${line.advance.left}:${line.advance.right} x ${line.advance.top}:${line.advance.bottom}] ',
|
||||
);
|
||||
}
|
||||
// We take whitespaces in account
|
||||
if (!line.allLineTextRange.overlapsWith(start, end)) {
|
||||
continue;
|
||||
@ -524,14 +507,12 @@ class TextLayout {
|
||||
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
final ui.TextRange intersect = block.textRange.intersect(textRange);
|
||||
//if (boxWidthStyle == ui.BoxWidthStyle.tight) {
|
||||
// // Ignore whitespaces at the end of the line
|
||||
// intersect = intersect.intersect(line.textRange);
|
||||
//}
|
||||
WebParagraphDebug.log(
|
||||
'block: ${block.textRange} & $textRange = $intersect '
|
||||
'${block.span.start}',
|
||||
);
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'block: ${block.textRange} & $textRange = $intersect '
|
||||
'${block.span.start}',
|
||||
);
|
||||
}
|
||||
if (intersect.size <= 0) {
|
||||
continue;
|
||||
}
|
||||
@ -574,20 +555,29 @@ class TextLayout {
|
||||
strutStyle.strutAscent;
|
||||
bottom = top + strutStyle.strutAscent + strutStyle.strutDescent;
|
||||
case ui.BoxHeightStyle.includeLineSpacingMiddle:
|
||||
top =
|
||||
line.advance.top +
|
||||
(line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent) / 2;
|
||||
bottom =
|
||||
line.advance.top +
|
||||
line.fontBoundingBoxAscent +
|
||||
(line.fontBoundingBoxDescent + block.rawFontBoundingBoxDescent) / 2;
|
||||
final double shift = (line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent) / 2;
|
||||
top = line.advance.top + shift;
|
||||
bottom = line.advance.bottom + shift;
|
||||
if (lineIndex == 0) {
|
||||
top += shift;
|
||||
}
|
||||
if (lineIndex == lines.length - 1) {
|
||||
bottom -= shift;
|
||||
}
|
||||
case ui.BoxHeightStyle.includeLineSpacingTop:
|
||||
top = line.advance.top + line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent;
|
||||
bottom = line.advance.top + line.fontBoundingBoxAscent + line.fontBoundingBoxDescent;
|
||||
case ui.BoxHeightStyle.includeLineSpacingBottom:
|
||||
final double shift = line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent;
|
||||
top = line.advance.top;
|
||||
bottom =
|
||||
line.advance.top + line.fontBoundingBoxAscent + block.rawFontBoundingBoxDescent;
|
||||
bottom = line.advance.bottom;
|
||||
if (lineIndex == 0) {
|
||||
top += shift;
|
||||
}
|
||||
case ui.BoxHeightStyle.includeLineSpacingBottom:
|
||||
final double shift = line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent;
|
||||
top = line.advance.top + shift;
|
||||
bottom = line.advance.bottom + shift;
|
||||
if (lineIndex == lines.length - 1) {
|
||||
bottom -= shift;
|
||||
}
|
||||
}
|
||||
left = firstRect.left - (line.advance.left + line.formattingShift);
|
||||
right = left + firstRect.width;
|
||||
@ -628,11 +618,13 @@ class TextLayout {
|
||||
);
|
||||
}
|
||||
}
|
||||
WebParagraphDebug.log(
|
||||
'getBoxesForRange: [${line.advance.left}:${line.advance.right}x${line.advance.top}:${line.advance.bottom}]',
|
||||
);
|
||||
for (final rect in result) {
|
||||
WebParagraphDebug.log('[${rect.left}:${rect.right}x${rect.top}:${rect.bottom}]');
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'getBoxesForRange: [${line.advance.left}:${line.advance.right}x${line.advance.top}:${line.advance.bottom}]',
|
||||
);
|
||||
for (final rect in result) {
|
||||
WebParagraphDebug.log('[${rect.left}:${rect.right}x${rect.top}:${rect.bottom}]');
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@ -660,9 +652,11 @@ class TextLayout {
|
||||
);
|
||||
}
|
||||
}
|
||||
WebParagraphDebug.log('getBoxesForPlaceholders:');
|
||||
for (final rect in result) {
|
||||
WebParagraphDebug.log('[${rect.left}:${rect.right}x${rect.top}:${rect.bottom}]');
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log('getBoxesForPlaceholders:');
|
||||
for (final rect in result) {
|
||||
WebParagraphDebug.log('[${rect.left}:${rect.right}x${rect.top}:${rect.bottom}]');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -692,7 +686,9 @@ class TextLayout {
|
||||
// We are not there yet; we need a line closest to the offset.
|
||||
continue;
|
||||
}
|
||||
WebParagraphDebug.log('found line: ${line.textClusterRange} ${line.advance} vs $offset');
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log('found line: ${line.textClusterRange} ${line.advance} vs $offset');
|
||||
}
|
||||
|
||||
// We found the line that contains the offset; let's go through all the visual blocks to find the position
|
||||
final double lineShift = line.advance.left + line.formattingShift;
|
||||
@ -711,7 +707,9 @@ class TextLayout {
|
||||
);
|
||||
}
|
||||
|
||||
WebParagraphDebug.log('found block: $block $left:$right vs $offset');
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log('found block: $block $left:$right vs $offset');
|
||||
}
|
||||
// Found the block; let's go through all the clusters IN VISUAL ORDER to find the position
|
||||
final int start = block.isLtr ? block.clusterRange.start : block.clusterRange.end - 1;
|
||||
final int end = block.isLtr ? block.clusterRange.end : block.clusterRange.start - 1;
|
||||
@ -724,7 +722,9 @@ class TextLayout {
|
||||
final double right =
|
||||
cluster.advance.right + lineShift + block.spanShiftFromLineStart + epsilon;
|
||||
|
||||
WebParagraphDebug.log('test cluster: $left:$right vs $offset');
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log('test cluster: $left:$right vs $offset');
|
||||
}
|
||||
if (left <= offset.dx && right > offset.dx) {
|
||||
if (offset.dx - left <= right - offset.dx) {
|
||||
return ui.TextPosition(offset: cluster.start);
|
||||
@ -964,6 +964,8 @@ abstract class WebCluster {
|
||||
|
||||
void fillOnContext(DomCanvasRenderingContext2D context, {required double x, required double y});
|
||||
|
||||
void addToContext(DomCanvasRenderingContext2D context, double x, double y);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WebCluster [$start:$end)';
|
||||
@ -1002,6 +1004,11 @@ class TextCluster extends WebCluster {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void addToContext(DomCanvasRenderingContext2D context, double x, double y) {
|
||||
context.fillTextCluster(_cluster, /*left:*/ x, /*top:*/ y + span.fontBoundingBoxAscent);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TextCluster [$start:$end) ${end - start}';
|
||||
@ -1031,13 +1038,18 @@ class EmptyCluster extends WebCluster {
|
||||
|
||||
@override
|
||||
void fillOnContext(DomCanvasRenderingContext2D context, {required double x, required double y}) {
|
||||
assert(false, 'We should not call fillOnContext method on this object');
|
||||
throw UnsupportedError('We should not call "fillOnContext" on an EmptyCluster');
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EmptyCluster [$start:$end)';
|
||||
}
|
||||
|
||||
@override
|
||||
void addToContext(DomCanvasRenderingContext2D context, double x, double y) {
|
||||
throw UnsupportedError('We should not call "addToContext" on an EmptyCluster');
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceholderCluster extends WebCluster {
|
||||
@ -1065,6 +1077,11 @@ class PlaceholderCluster extends WebCluster {
|
||||
void fillOnContext(DomCanvasRenderingContext2D context, {required double x, required double y}) {
|
||||
// No-op. Placeholders don't draw anything.
|
||||
}
|
||||
|
||||
@override
|
||||
void addToContext(DomCanvasRenderingContext2D context, double x, double y) {
|
||||
throw UnsupportedError('We should not call "addToContext" on an PlaceholderCluster');
|
||||
}
|
||||
}
|
||||
|
||||
// This is the minimal range of cluster that belongs to the same bidi run and to the same style block
|
||||
@ -1146,6 +1163,31 @@ class TextBlock extends LineBlock {
|
||||
// TODO(jlavrova): Why are we defaulting to 1.0? In Chrome, the default line-height is `1.2` most of the time.
|
||||
double get _heightMultiplier => style.height == null ? 1.0 : style.height!;
|
||||
|
||||
int get visualClusterStart => isLtr ? clusterRange.start : clusterRange.end - 1;
|
||||
int get visualClusterEnd => isLtr ? clusterRange.end : clusterRange.start - 1;
|
||||
|
||||
/// Returns a list of pairs of clusters and their directions in the visual order.
|
||||
Iterable<(WebCluster, bool)> getTextClustersInVisualOrder(TextLayout layout) sync* {
|
||||
final int start = visualClusterStart;
|
||||
final int end = visualClusterEnd;
|
||||
final step = isLtr ? 1 : -1;
|
||||
for (var i = start; i != end; i += step) {
|
||||
final WebCluster clusterText = this is EllipsisBlock
|
||||
? layout.ellipsisClusters[i]
|
||||
: layout.allClusters[i];
|
||||
yield (
|
||||
clusterText,
|
||||
// We shape ellipsis with default direction coming from the attaching block
|
||||
// and all the other blocks with the default paragraph direction.
|
||||
// The reason for shaping ellipsis this way is that we literally attach it to the block
|
||||
// that overflows and we want to keep all the styling attributes (including text direction) consistent.
|
||||
this is EllipsisBlock
|
||||
? isLtr
|
||||
: layout.paragraph.paragraphStyle.textDirection == ui.TextDirection.ltr,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClusterRange clusterRangeWithoutWhitespaces;
|
||||
double whitespacesWidth;
|
||||
}
|
||||
@ -1181,9 +1223,6 @@ class PlaceholderBlock extends LineBlock {
|
||||
|
||||
final double height = span.height;
|
||||
final double offset = span.baselineOffset;
|
||||
WebParagraphDebug.log(
|
||||
'calculatePlaceholderAdvance($lineAscent, $lineDescent): height=$height offset=$offset',
|
||||
);
|
||||
switch (span.alignment) {
|
||||
case ui.PlaceholderAlignment.baseline:
|
||||
// Matches the baseline of the placeholder with the text baseline. You'll need to specify the TextBaseline to use
|
||||
@ -1221,7 +1260,6 @@ class PlaceholderBlock extends LineBlock {
|
||||
// The advance needs to be calculated relative to the line. In order to do that, we need to start
|
||||
// from the span's own advance within the line.
|
||||
advance = ui.Rect.fromLTWH(spanShiftFromLineStart, top, span.width, span.height);
|
||||
WebParagraphDebug.log('PlaceholderBlock calculated advance: $advance $ascent $descent');
|
||||
}
|
||||
|
||||
// TODO(jlavrova): Why are we using separate properties instead of `rawFontBoundingBoxAscent` and `rawFontBoundingBoxDescent`?
|
||||
|
||||
@ -2,250 +2,30 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../../engine.dart';
|
||||
|
||||
// TODO(jlavrova): We use it to paint shadows (with vertical shifts) so we need to make it tall enough as well.
|
||||
double? currentDevicePixelRatio;
|
||||
//final DomOffscreenCanvas paintCanvas = createDomOffscreenCanvas(0, 0);
|
||||
//final paintContext =
|
||||
// paintCanvas.getContext('2d', {'willReadFrequently': true})! as DomCanvasRenderingContext2D;
|
||||
|
||||
final DomHTMLCanvasElement paintCanvas =
|
||||
domDocument.createElement('canvas') as DomHTMLCanvasElement;
|
||||
final paintContext =
|
||||
paintCanvas.getContext('2d', {'willReadFrequently': true})! as DomCanvasRenderingContext2D;
|
||||
|
||||
/// Paints on a [WebParagraph].
|
||||
///
|
||||
/// It uses a [DomCanvasElement] to get text information
|
||||
class TextPaint {
|
||||
TextPaint(this.paragraph, this.painter);
|
||||
abstract class TextPaint {
|
||||
TextPaint(this.paragraph);
|
||||
|
||||
final WebParagraph paragraph;
|
||||
final Painter painter;
|
||||
|
||||
// TODO(jlavrova): painting the entire block could require a really big canvas
|
||||
// Answer: we only do blocks for background and decorations which we do not draw on canvas
|
||||
// but rather implement ourselves via CanvasKit API
|
||||
void _paintByBlocks(
|
||||
StyleElements styleElement,
|
||||
ui.Canvas canvas,
|
||||
TextLayout layout,
|
||||
TextLine line,
|
||||
double x,
|
||||
double y,
|
||||
) {
|
||||
// We traverse text in visual blocks order (broken by text styles and bidi runs, then reordered)
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
if (!block.style.hasElement(styleElement)) {
|
||||
continue;
|
||||
}
|
||||
// Placeholders do not need painting, just reserving the space
|
||||
if (block is PlaceholderBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Let's calculate the sizes
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateBlock(
|
||||
layout,
|
||||
block as TextBlock,
|
||||
ui.Offset(
|
||||
line.advance.left + line.formattingShift + block.shiftFromLineStart,
|
||||
line.advance.top + line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
),
|
||||
ui.Offset(x, y),
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'+_paintByBlocks: ${block.textRange} ${block.spanShiftFromLineStart} ${block.shiftFromLineStart} '
|
||||
'${line.advance} + ${line.formattingShift} '
|
||||
'\nsourceRect: $sourceRect targetRect: $targetRect',
|
||||
);
|
||||
// Let's draw whatever has to be drawn
|
||||
switch (styleElement) {
|
||||
case StyleElements.background:
|
||||
painter.paintBackground(canvas, block, sourceRect, targetRect);
|
||||
case StyleElements.decorations:
|
||||
painter.fillDecorations(block, sourceRect);
|
||||
painter.paintDecorations(canvas, sourceRect, targetRect);
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintByClusters(
|
||||
StyleElements styleElement,
|
||||
ui.Canvas canvas,
|
||||
TextLayout layout,
|
||||
TextLine line,
|
||||
double x,
|
||||
double y,
|
||||
) {
|
||||
// We traverse clusters in the order of visual blocks (broken by text styles and bidi runs, then reordered)
|
||||
// and then in visual order inside blocks
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
if (!block.style.hasElement(styleElement)) {
|
||||
continue;
|
||||
}
|
||||
// Placeholders do not need painting, just reserving the space
|
||||
if (block.clusterRange.size == 1 &&
|
||||
layout.allClusters[block.clusterRange.start] is PlaceholderCluster) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'+paintByClusters: ${block.textRange} ${block.clusterRange} ${(block as TextBlock).clusterRangeWithoutWhitespaces} ${block.whitespacesWidth} ${block.isLtr} ${line.advance.left} + ${line.formattingShift} + ${block.shiftFromLineStart}',
|
||||
);
|
||||
|
||||
// We are painting clusters in visual order so that if they step on each other, the paint
|
||||
// order is correct.
|
||||
final int start = block.isLtr
|
||||
? block.clusterRangeWithoutWhitespaces.start
|
||||
: block.clusterRangeWithoutWhitespaces.end - 1;
|
||||
final int end = block.isLtr
|
||||
? block.clusterRangeWithoutWhitespaces.end
|
||||
: block.clusterRangeWithoutWhitespaces.start - 1;
|
||||
final step = block.isLtr ? 1 : -1;
|
||||
for (var i = start; i != end; i += step) {
|
||||
final WebCluster clusterText = block is EllipsisBlock
|
||||
? layout.ellipsisClusters[i]
|
||||
: layout.allClusters[i];
|
||||
// We need to adjust the canvas size to fit the block in case there is scaling or zoom involved
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateCluster(
|
||||
layout,
|
||||
block,
|
||||
clusterText,
|
||||
ui.Offset(
|
||||
// TODO(mdebbar): Avoid use of `block.spanShiftFromLineStart` (similar to `getPositionForOffset`)
|
||||
line.advance.left + line.formattingShift + block.spanShiftFromLineStart,
|
||||
line.advance.top + line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
),
|
||||
ui.Offset(x, y),
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
|
||||
if (sourceRect.isEmpty) {
|
||||
// Let's skip empty clusters
|
||||
continue;
|
||||
}
|
||||
switch (styleElement) {
|
||||
case StyleElements.shadows:
|
||||
paintContext.save();
|
||||
for (final ui.Shadow shadow in clusterText.style.shadows!) {
|
||||
painter.fillShadow(
|
||||
clusterText,
|
||||
shadow,
|
||||
// We shape ellipsis with default direction coming from the attaching block
|
||||
// and all the other blocks with the default paragraph direction
|
||||
block is EllipsisBlock
|
||||
? block.isLtr
|
||||
: layout.paragraph.paragraphStyle.textDirection == ui.TextDirection.ltr,
|
||||
);
|
||||
painter.paintShadow(canvas, sourceRect, targetRect);
|
||||
}
|
||||
paintContext.restore();
|
||||
case StyleElements.text:
|
||||
painter.fillTextCluster(
|
||||
clusterText,
|
||||
// We shape ellipsis with default direction coming from the attaching block
|
||||
// and all the other blocks with the default paragraph direction.
|
||||
// The reason for shaping ellipsis this way is that we literally attach it to the block
|
||||
// that overflows and we want to keep all the styling attributes (including text direction) consistent.
|
||||
block is EllipsisBlock
|
||||
? block.isLtr
|
||||
: layout.paragraph.paragraphStyle.textDirection == ui.TextDirection.ltr,
|
||||
);
|
||||
painter.paintTextCluster(canvas, sourceRect, targetRect);
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintByClustersOnCanvas2D(
|
||||
StyleElements styleElement,
|
||||
DomHTMLCanvasElement canvas,
|
||||
TextLayout layout,
|
||||
TextLine line,
|
||||
double x,
|
||||
double y,
|
||||
) {
|
||||
// We traverse clusters in the order of visual blocks (broken by text styles and bidi runs, then reordered)
|
||||
// and then in visual order inside blocks
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
if (!block.style.hasElement(styleElement)) {
|
||||
continue;
|
||||
}
|
||||
// Placeholders do not need painting, just reserving the space
|
||||
if (block.clusterRange.size == 1 &&
|
||||
layout.allClusters[block.clusterRange.start] is PlaceholderCluster) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'+paintByClusters: ${block.textRange} ${block.clusterRange} ${(block as TextBlock).clusterRangeWithoutWhitespaces} ${block.whitespacesWidth} ${block.isLtr} ${line.advance.left} + ${line.formattingShift} + ${block.shiftFromLineStart}',
|
||||
);
|
||||
|
||||
// We are painting clusters in visual order so that if they step on each other, the paint
|
||||
// order is correct.
|
||||
final int start = block.isLtr
|
||||
? block.clusterRangeWithoutWhitespaces.start
|
||||
: block.clusterRangeWithoutWhitespaces.end - 1;
|
||||
final int end = block.isLtr
|
||||
? block.clusterRangeWithoutWhitespaces.end
|
||||
: block.clusterRangeWithoutWhitespaces.start - 1;
|
||||
final step = block.isLtr ? 1 : -1;
|
||||
for (var i = start; i != end; i += step) {
|
||||
final WebCluster clusterText = block is EllipsisBlock
|
||||
? layout.ellipsisClusters[i]
|
||||
: layout.allClusters[i];
|
||||
// We need to adjust the canvas size to fit the block in case there is scaling or zoom involved
|
||||
// We need to adjust the canvas size to fit the block in case there is scaling or zoom involved
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateCluster(
|
||||
layout,
|
||||
block,
|
||||
clusterText,
|
||||
ui.Offset(
|
||||
// TODO(mdebbar): Avoid use of `block.spanShiftFromLineStart` (similar to `getPositionForOffset`)
|
||||
line.advance.left + line.formattingShift + block.spanShiftFromLineStart,
|
||||
line.advance.top + line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
),
|
||||
ui.Offset(x, y),
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
|
||||
if (sourceRect.isEmpty) {
|
||||
// Let's skip empty clusters
|
||||
continue;
|
||||
}
|
||||
switch (styleElement) {
|
||||
case StyleElements.text:
|
||||
final WebTextStyle style = clusterText.style;
|
||||
paintContext.fillStyle = style.getForegroundColor().toCssString();
|
||||
// We fill the text cluster into a rectange [0,0,w,h]
|
||||
// but we need to shift the y coordinate by the font ascent
|
||||
// becase the text is drawn at the ascent, not at 0
|
||||
clusterText.fillOnContext(
|
||||
paintContext,
|
||||
/*ignore the text cluster shift from the text run*/
|
||||
x: (block.isLtr ? 0 : clusterText.advance.width),
|
||||
y: 0,
|
||||
);
|
||||
|
||||
final DomImageBitmap bitmap = paintCanvas.transferToImageBitmap();
|
||||
canvas.context2D.drawImage(
|
||||
bitmap,
|
||||
sourceRect.left,
|
||||
sourceRect.top,
|
||||
sourceRect.width,
|
||||
sourceRect.height,
|
||||
targetRect.left,
|
||||
targetRect.top,
|
||||
targetRect.width,
|
||||
targetRect.height,
|
||||
);
|
||||
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the source (on Canvas2D) and target (on the output canvas) rectangles for a text cluster
|
||||
(ui.Rect sourceRect, ui.Rect targetRect) calculateCluster(
|
||||
TextLayout layout,
|
||||
LineBlock block,
|
||||
@ -298,6 +78,7 @@ class TextPaint {
|
||||
return (sourceRect, targetRect);
|
||||
}
|
||||
|
||||
/// Calculates the source (on Canvas2D) and target (on the output canvas) rectangles for a text block
|
||||
(ui.Rect sourceRect, ui.Rect targetRect) calculateBlock(
|
||||
TextLayout layout,
|
||||
TextBlock block,
|
||||
@ -328,37 +109,197 @@ class TextPaint {
|
||||
.translate(blockOffset.dx, blockOffset.dy)
|
||||
.translate(paragraphOffset.dx, paragraphOffset.dy);
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'calculateBlock "${block.span.text}" ${block.textRange}-${block.span.start} ${block.clusterRange} '
|
||||
'source: ${sourceRect.left}:${sourceRect.right}x${sourceRect.top}:${sourceRect.bottom} => '
|
||||
'target: ${targetRect.left}:${targetRect.right}x${targetRect.top}:${targetRect.bottom}',
|
||||
);
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'calculateBlock "${block.span.text}" ${block.textRange}-${block.span.start} ${block.clusterRange} '
|
||||
'source: ${sourceRect.left}:${sourceRect.right}x${sourceRect.top}:${sourceRect.bottom} => '
|
||||
'target: ${targetRect.left}:${targetRect.right}x${targetRect.top}:${targetRect.bottom}',
|
||||
);
|
||||
}
|
||||
|
||||
return (sourceRect, targetRect);
|
||||
}
|
||||
|
||||
void paintLine(ui.Canvas canvas, TextLayout layout, TextLine line, double x, double y) {
|
||||
WebParagraphDebug.log('paintLineOnCanvasKit.Background: ${line.textRange}');
|
||||
_paintByBlocks(StyleElements.background, canvas, layout, line, x, y);
|
||||
/// Calculates the source (on Canvas2D) and target (on the output canvas) rectangles for the entire paragraph
|
||||
(ui.Rect sourceRect, ui.Rect targetRect) calculateParagraph(
|
||||
TextLayout layout,
|
||||
ui.Offset offset,
|
||||
double devicePixelRatio,
|
||||
) {
|
||||
// Calculate the longest line taking in account the formatting shifts
|
||||
double maxWidth = 0;
|
||||
for (final TextLine line in layout.lines) {
|
||||
final double lineWidth = line.advance.width + line.formattingShift + line.trailingSpacesWidth;
|
||||
if (lineWidth > maxWidth) {
|
||||
maxWidth = lineWidth;
|
||||
}
|
||||
}
|
||||
|
||||
WebParagraphDebug.log('paintLineOnCanvasKit.Shadows: ${line.textRange}');
|
||||
_paintByClusters(StyleElements.shadows, canvas, layout, line, x, y);
|
||||
// Define the paragraph rect (using advances, not selected rects)
|
||||
// Source rect must take in account the scaling
|
||||
final sourceRect = ui.Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
(maxWidth * devicePixelRatio).ceilToDouble(),
|
||||
(layout.paragraph.height * devicePixelRatio).ceilToDouble(),
|
||||
);
|
||||
// Target rect will be scaled by the canvas transform, so we don't scale it here
|
||||
final zeroRect = ui.Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
maxWidth.ceilToDouble(),
|
||||
layout.paragraph.height.ceilToDouble(),
|
||||
);
|
||||
final ui.Rect targetRect = zeroRect.translate(offset.dx, offset.dy);
|
||||
|
||||
WebParagraphDebug.log('paintLineOnCanvasKit.Text: ${line.textRange}');
|
||||
_paintByClusters(StyleElements.text, canvas, layout, line, x, y);
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'calculateParagraph source: ${sourceRect.left}:${sourceRect.right}x${sourceRect.top}:${sourceRect.bottom} => '
|
||||
'target: ${targetRect.left}:${targetRect.right}x${targetRect.top}:${targetRect.bottom}',
|
||||
);
|
||||
}
|
||||
|
||||
WebParagraphDebug.log('paintLineOnCanvasKit.Decorations: ${line.textRange}');
|
||||
_paintByBlocks(StyleElements.decorations, canvas, layout, line, x, y);
|
||||
return (sourceRect, targetRect);
|
||||
}
|
||||
|
||||
void paintLineOnCanvas2D(
|
||||
DomHTMLCanvasElement canvas,
|
||||
TextLayout layout,
|
||||
TextLine line,
|
||||
/// Calculates the thickness of the decoration line
|
||||
double calculateThickness(WebTextStyle textStyle) {
|
||||
return (textStyle.fontSize! / 14.0) * (textStyle.decorationThickness ?? 1.0);
|
||||
}
|
||||
|
||||
/// Calculates the position of the decoration line
|
||||
double calculatePosition(
|
||||
ui.TextDecoration decoration,
|
||||
double thickness,
|
||||
double height,
|
||||
double ascent,
|
||||
) {
|
||||
switch (decoration) {
|
||||
case ui.TextDecoration.underline:
|
||||
return thickness + ascent;
|
||||
case ui.TextDecoration.overline:
|
||||
return thickness / 2;
|
||||
case ui.TextDecoration.lineThrough:
|
||||
return height / 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Calculates and the position of the decoration line and paints it on Canvas2D
|
||||
void calculateWaves(
|
||||
double x,
|
||||
double y,
|
||||
WebTextStyle textStyle,
|
||||
ui.Rect textBounds,
|
||||
double thickness,
|
||||
) {
|
||||
WebParagraphDebug.log('paintLineOnCanvasKit.Text: ${line.textRange}');
|
||||
_paintByClustersOnCanvas2D(StyleElements.text, canvas, layout, line, x, y);
|
||||
final quarterWave = thickness;
|
||||
|
||||
var waveCount = 0;
|
||||
double xStart = 0;
|
||||
final double yStart = y + quarterWave;
|
||||
|
||||
paintContext.beginPath();
|
||||
paintContext.moveTo(x, yStart);
|
||||
while (xStart + quarterWave * 2 < textBounds.width) {
|
||||
final x1 = xStart;
|
||||
final double y1 = yStart + quarterWave * (waveCount.isEven ? 1 : -1);
|
||||
final double x2 = xStart + quarterWave * 2;
|
||||
final y2 = yStart;
|
||||
paintContext.quadraticCurveTo(x1, y1, x2, y2);
|
||||
xStart += quarterWave * 2;
|
||||
++waveCount;
|
||||
}
|
||||
|
||||
// The rest of the wave
|
||||
final double remaining = textBounds.width - xStart;
|
||||
if (remaining > 0) {
|
||||
final x1 = xStart;
|
||||
final double y1 = yStart + quarterWave * (waveCount.isEven ? 1 : -1);
|
||||
final double x2 = xStart + remaining;
|
||||
final y2 = yStart;
|
||||
paintContext.quadraticCurveTo(x1, y1, x2, y2);
|
||||
}
|
||||
paintContext.stroke();
|
||||
}
|
||||
|
||||
// TODO(jlavrova): implement decorations entirely on the resulting Canvas
|
||||
/// Paints text decorations on Canvas2D
|
||||
void fillDecorations(TextBlock block, ui.Rect sourceRect) {
|
||||
if (!block.style.hasElement(StyleElements.decorations) || block.style.decoration == null) {
|
||||
return;
|
||||
}
|
||||
paintContext.fillStyle = block.style.getForegroundColor().toCssString();
|
||||
|
||||
final double thickness = calculateThickness(block.style);
|
||||
|
||||
const DoubleDecorationSpacing = 3.0;
|
||||
|
||||
for (final ui.TextDecoration decoration in [
|
||||
ui.TextDecoration.lineThrough,
|
||||
ui.TextDecoration.underline,
|
||||
ui.TextDecoration.overline,
|
||||
]) {
|
||||
if (!block.style.decoration!.contains(decoration)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final double height =
|
||||
block.multipliedFontBoundingBoxAscent + block.multipliedFontBoundingBoxDescent;
|
||||
final double ascent = block.multipliedFontBoundingBoxAscent;
|
||||
final double position = calculatePosition(decoration, thickness, height, ascent);
|
||||
|
||||
final double width = sourceRect.width;
|
||||
final double x = sourceRect.left;
|
||||
final double y = sourceRect.top + position;
|
||||
|
||||
paintContext.save();
|
||||
paintContext.lineWidth = thickness;
|
||||
paintContext.strokeStyle = block.style.decorationColor!.toCssString();
|
||||
|
||||
switch (block.style.decorationStyle!) {
|
||||
case ui.TextDecorationStyle.wavy:
|
||||
calculateWaves(x, y, block.style, sourceRect, thickness);
|
||||
|
||||
case ui.TextDecorationStyle.double:
|
||||
final double bottom = y + DoubleDecorationSpacing + thickness;
|
||||
paintContext.beginPath();
|
||||
paintContext.moveTo(x, y);
|
||||
paintContext.lineTo(x + width, y);
|
||||
paintContext.moveTo(x, bottom);
|
||||
paintContext.lineTo(x + width, bottom);
|
||||
paintContext.stroke();
|
||||
|
||||
case ui.TextDecorationStyle.dashed:
|
||||
case ui.TextDecorationStyle.dotted:
|
||||
final dashes = Float32List(2)
|
||||
..[0] =
|
||||
thickness * (block.style.decorationStyle! == ui.TextDecorationStyle.dotted ? 1 : 4)
|
||||
..[1] = thickness;
|
||||
|
||||
paintContext.setLineDash(dashes);
|
||||
paintContext.beginPath();
|
||||
paintContext.moveTo(x, y);
|
||||
paintContext.lineTo(x + width, y);
|
||||
paintContext.stroke();
|
||||
|
||||
case ui.TextDecorationStyle.solid:
|
||||
paintContext.beginPath();
|
||||
paintContext.moveTo(x, y);
|
||||
paintContext.lineTo(x + width, y);
|
||||
paintContext.stroke();
|
||||
}
|
||||
|
||||
paintContext.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// Paints shadows of a text cluster on Canvas2D
|
||||
void fillTextCluster(WebCluster webTextCluster, bool isDefaultLtr);
|
||||
|
||||
/// Paints shadows of a text cluster on Canvas2D
|
||||
void fillShadowCluster(WebCluster webTextCluster, ui.Shadow shadow, bool isDefaultLtr);
|
||||
|
||||
/// Paints the entire paragraph on Canvas2D
|
||||
void paint(ui.Canvas canvas, TextLayout layout, Painter painter, double x, double y);
|
||||
}
|
||||
|
||||
@ -0,0 +1,228 @@
|
||||
// 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:ui/ui.dart' as ui;
|
||||
|
||||
import '../../engine.dart';
|
||||
|
||||
/// Paints on a [WebParagraph].
|
||||
///
|
||||
/// It uses a [DomCanvasElement] to get text information
|
||||
class PaintClusters extends TextPaint {
|
||||
PaintClusters(super.paragraph);
|
||||
|
||||
// TODO(jlavrova): painting the entire block could require a really big canvas
|
||||
// Answer: we only do blocks for background and decorations which we do not draw on canvas
|
||||
// but rather implement ourselves via CanvasKit API
|
||||
void _paintByBlocks(
|
||||
StyleElements styleElement,
|
||||
ui.Canvas canvas,
|
||||
TextLayout layout,
|
||||
TextLine line,
|
||||
Painter painter,
|
||||
double x,
|
||||
double y,
|
||||
) {
|
||||
// We traverse text in visual blocks order (broken by text styles and bidi runs, then reordered)
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
if (!block.style.hasElement(styleElement)) {
|
||||
continue;
|
||||
}
|
||||
// Placeholders do not need painting, just reserving the space
|
||||
if (block is PlaceholderBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Let's calculate the sizes
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateBlock(
|
||||
layout,
|
||||
block as TextBlock,
|
||||
ui.Offset(
|
||||
line.advance.left + line.formattingShift + block.shiftFromLineStart,
|
||||
line.advance.top + line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
),
|
||||
ui.Offset(x, y),
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'+_paintByBlocks: ${block.textRange} ${block.spanShiftFromLineStart} ${block.shiftFromLineStart} '
|
||||
'${line.advance} + ${line.formattingShift} '
|
||||
'\nsourceRect: $sourceRect targetRect: $targetRect',
|
||||
);
|
||||
}
|
||||
// Let's draw whatever has to be drawn
|
||||
switch (styleElement) {
|
||||
case StyleElements.background:
|
||||
painter.drawBackground(canvas, block, sourceRect, targetRect);
|
||||
case StyleElements.decorations:
|
||||
painter.resizePaintCanvas(
|
||||
ui.window.devicePixelRatio,
|
||||
sourceRect.width,
|
||||
sourceRect.height,
|
||||
);
|
||||
fillDecorations(block, sourceRect);
|
||||
painter.drawDecorations(canvas, sourceRect, targetRect);
|
||||
case StyleElements.text:
|
||||
throw Exception('Text should be drawn by clusters, not blocks');
|
||||
case StyleElements.shadows:
|
||||
throw Exception('Shadows should be drawn by clusters, not blocks');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintByClusters(
|
||||
StyleElements styleElement,
|
||||
ui.Canvas canvas,
|
||||
TextLayout layout,
|
||||
TextLine line,
|
||||
Painter painter,
|
||||
double x,
|
||||
double y,
|
||||
) {
|
||||
// We traverse clusters in the order of visual blocks (broken by text styles and bidi runs, then reordered)
|
||||
// and then in visual order inside blocks
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
if (!block.style.hasElement(styleElement)) {
|
||||
continue;
|
||||
}
|
||||
// Placeholders do not need painting, just reserving the space
|
||||
if (block is PlaceholderBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'+paintByClusters: ${block.textRange} ${block.clusterRange} ${(block as TextBlock).clusterRangeWithoutWhitespaces} ${block.whitespacesWidth} ${block.isLtr} ${line.advance.left} + ${line.formattingShift} + ${block.shiftFromLineStart}',
|
||||
);
|
||||
}
|
||||
|
||||
// We are painting clusters in visual order so that if they step on each other, the paint
|
||||
// order is correct.
|
||||
final int start = block.isLtr
|
||||
? (block as TextBlock).clusterRangeWithoutWhitespaces.start
|
||||
: (block as TextBlock).clusterRangeWithoutWhitespaces.end - 1;
|
||||
final int end = block.isLtr
|
||||
? (block as TextBlock).clusterRangeWithoutWhitespaces.end
|
||||
: (block as TextBlock).clusterRangeWithoutWhitespaces.start - 1;
|
||||
final step = block.isLtr ? 1 : -1;
|
||||
for (var i = start; i != end; i += step) {
|
||||
final WebCluster clusterText = block is EllipsisBlock
|
||||
? layout.ellipsisClusters[i]
|
||||
: layout.allClusters[i];
|
||||
// We need to adjust the canvas size to fit the block in case there is scaling or zoom involved
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateCluster(
|
||||
layout,
|
||||
block,
|
||||
clusterText,
|
||||
ui.Offset(
|
||||
// TODO(mdebbar): Avoid use of `block.spanShiftFromLineStart` (similar to `getPositionForOffset`)
|
||||
line.advance.left + line.formattingShift + block.spanShiftFromLineStart,
|
||||
line.advance.top + line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
),
|
||||
ui.Offset(x, y),
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
|
||||
if (sourceRect.isEmpty) {
|
||||
// Let's skip empty clusters
|
||||
continue;
|
||||
}
|
||||
painter.resizePaintCanvas(ui.window.devicePixelRatio, sourceRect.width, sourceRect.height);
|
||||
switch (styleElement) {
|
||||
case StyleElements.shadows:
|
||||
paintContext.save();
|
||||
for (final ui.Shadow shadow in clusterText.style.shadows!) {
|
||||
fillShadowCluster(
|
||||
clusterText,
|
||||
shadow,
|
||||
// We shape ellipsis with default direction coming from the attaching block
|
||||
// and all the other blocks with the default paragraph direction
|
||||
block is EllipsisBlock
|
||||
? block.isLtr
|
||||
: layout.paragraph.paragraphStyle.textDirection == ui.TextDirection.ltr,
|
||||
);
|
||||
painter.drawShadowCluster(canvas, sourceRect, targetRect);
|
||||
}
|
||||
paintContext.restore();
|
||||
case StyleElements.text:
|
||||
fillTextCluster(
|
||||
clusterText,
|
||||
// We shape ellipsis with default direction coming from the attaching block
|
||||
// and all the other blocks with the default paragraph direction.
|
||||
// The reason for shaping ellipsis this way is that we literally attach it to the block
|
||||
// that overflows and we want to keep all the styling attributes (including text direction) consistent.
|
||||
block is EllipsisBlock
|
||||
? block.isLtr
|
||||
: layout.paragraph.paragraphStyle.textDirection == ui.TextDirection.ltr,
|
||||
);
|
||||
painter.drawTextCluster(canvas, sourceRect, targetRect);
|
||||
case StyleElements.background:
|
||||
throw Exception('Background should be drawn by blocks, not clusters');
|
||||
case StyleElements.decorations:
|
||||
throw Exception('Decorations should be drawn by blocks, not clusters');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(ui.Canvas canvas, TextLayout layout, Painter painter, double x, double y) {
|
||||
for (final TextLine line in layout.lines) {
|
||||
// Paint background first
|
||||
_paintByBlocks(StyleElements.background, canvas, layout, line, painter, x, y);
|
||||
|
||||
// Paint all shadows on the line
|
||||
_paintByClusters(StyleElements.shadows, canvas, layout, line, painter, x, y);
|
||||
|
||||
// Paint the text on the line
|
||||
_paintByClusters(StyleElements.text, canvas, layout, line, painter, x, y);
|
||||
|
||||
// Paint decorations last
|
||||
_paintByBlocks(StyleElements.decorations, canvas, layout, line, painter, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void fillShadowCluster(WebCluster webTextCluster, ui.Shadow shadow, bool isDefaultLtr) {
|
||||
final WebTextStyle style = webTextCluster.style;
|
||||
|
||||
// TODO(jlavrova): see if we can implement shadowing ourself avoiding redrawing text clusters many times.
|
||||
// Answer: we cannot, and also there is a question of calculating the size of the shadow which we have to
|
||||
// take from Chrome as well (performing another measure text operation with shadow attribute set).
|
||||
paintContext.fillStyle = style.getForegroundColor().toCssString();
|
||||
paintContext.shadowColor = shadow.color.toCssString();
|
||||
paintContext.shadowBlur = shadow.blurRadius;
|
||||
paintContext.shadowOffsetX = shadow.offset.dx;
|
||||
paintContext.shadowOffsetY = shadow.offset.dy;
|
||||
|
||||
// We fill the text cluster into a rectange [0,0,w,h]
|
||||
// but we need to shift the y coordinate by the font ascent
|
||||
// becase the text is drawn at the ascent, not at 0
|
||||
webTextCluster.fillOnContext(
|
||||
paintContext,
|
||||
/*ignore the text cluster shift from the text run*/
|
||||
// TODO(jlavrova): calculate the shadow bounds without hardcoding the inflation
|
||||
// values. It is good enough for now to demonstrate the shadow effect
|
||||
x: (isDefaultLtr ? 0 : webTextCluster.advance.width) + 100,
|
||||
y: 100,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void fillTextCluster(WebCluster webTextCluster, bool isDefaultLtr) {
|
||||
final WebTextStyle style = webTextCluster.style;
|
||||
paintContext.fillStyle = style.getForegroundColor().toCssString();
|
||||
// We fill the text cluster into a rectange [0,0,w,h]
|
||||
// but we need to shift the y coordinate by the font ascent
|
||||
// becase the text is drawn at the ascent, not at 0
|
||||
webTextCluster.fillOnContext(
|
||||
paintContext,
|
||||
/*ignore the text cluster shift from the text run*/
|
||||
x: (isDefaultLtr ? 0 : webTextCluster.advance.width),
|
||||
y: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
// 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 'dart:typed_data';
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../../engine.dart';
|
||||
|
||||
/// Paints on a [WebParagraph].
|
||||
///
|
||||
/// It uses a [DomCanvasElement] to get text information
|
||||
class PaintParagraph extends TextPaint {
|
||||
PaintParagraph(super.paragraph);
|
||||
|
||||
void _fillAllBlocks(StyleElements styleElement, TextLayout layout) {
|
||||
// Paint the entire paragraph as a single image on Canvas2D
|
||||
double yOffset = 0;
|
||||
for (final TextLine line in layout.lines) {
|
||||
paintContext.save();
|
||||
paintContext.translate(line.formattingShift, yOffset);
|
||||
yOffset += line.advance.height;
|
||||
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
if (block is PlaceholderBlock) {
|
||||
// Placeholders do not need painting, just reserving the space
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'+_fillAllBlocks: ${block.textRange} ${block.clusterRange} ${paragraph.getText(block.textRange.start, block.textRange.end)} '
|
||||
'${(block as TextBlock).clusterRangeWithoutWhitespaces} ${block.whitespacesWidth} '
|
||||
'${block.isLtr} ${line.advance.left} + ${block.spanShiftFromLineStart}',
|
||||
);
|
||||
}
|
||||
|
||||
paintContext.save();
|
||||
switch (styleElement) {
|
||||
case StyleElements.shadows:
|
||||
// For text and shadows we need to shift to the start of the block
|
||||
paintContext.translate(
|
||||
block.spanShiftFromLineStart,
|
||||
line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
);
|
||||
_fillBlockShadows(layout, block as TextBlock);
|
||||
case StyleElements.text:
|
||||
// For text and shadows we need to shift to the start of the block
|
||||
paintContext.translate(
|
||||
block.spanShiftFromLineStart,
|
||||
line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
);
|
||||
_fillBlockText(layout, block as TextBlock);
|
||||
case StyleElements.decorations:
|
||||
// For decorations we need to shift to the start of the line
|
||||
paintContext.translate(block.shiftFromLineStart, 0);
|
||||
// Let's calculate the sizes
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateBlock(
|
||||
layout,
|
||||
block as TextBlock,
|
||||
ui.Offset(line.advance.left + line.formattingShift, line.advance.top),
|
||||
ui.Offset.zero, // We only need sourceRect here so we don't need the offset
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
fillDecorations(block, sourceRect);
|
||||
case StyleElements.background:
|
||||
throw Exception(
|
||||
'Background is drawn directly on the output canvas, not on the canvas2D',
|
||||
);
|
||||
}
|
||||
paintContext.restore();
|
||||
}
|
||||
|
||||
paintContext.restore();
|
||||
}
|
||||
}
|
||||
|
||||
void _drawAllBlocks(
|
||||
StyleElements styleElement,
|
||||
ui.Canvas canvas,
|
||||
TextLayout layout,
|
||||
Painter painter,
|
||||
double x,
|
||||
double y,
|
||||
) {
|
||||
for (final TextLine line in layout.lines) {
|
||||
for (final LineBlock block in line.visualBlocks) {
|
||||
if (block is PlaceholderBlock) {
|
||||
// Placeholders do not need painting, just reserving the space
|
||||
continue;
|
||||
}
|
||||
|
||||
// Let's calculate the sizes
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateBlock(
|
||||
layout,
|
||||
block as TextBlock,
|
||||
ui.Offset(
|
||||
line.advance.left + line.formattingShift + block.shiftFromLineStart,
|
||||
line.advance.top + line.fontBoundingBoxAscent - block.rawFontBoundingBoxAscent,
|
||||
),
|
||||
ui.Offset(x, y),
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'+_drawAllBlocks: ${block.textRange} ${block.clusterRange} ${paragraph.getText(block.textRange.start, block.textRange.end)} '
|
||||
'${block.clusterRangeWithoutWhitespaces} ${block.whitespacesWidth} '
|
||||
'${block.isLtr} ${line.advance.left} + ${block.spanShiftFromLineStart}',
|
||||
);
|
||||
|
||||
switch (styleElement) {
|
||||
case StyleElements.background:
|
||||
painter.drawBackground(canvas, block, sourceRect, targetRect);
|
||||
case StyleElements.decorations:
|
||||
throw Exception(
|
||||
'Decorations are painted on the canvas2D and then drawn as an image on the output canvas, not drawn directly on the output canvas',
|
||||
);
|
||||
case StyleElements.shadows:
|
||||
throw Exception(
|
||||
'Shadows are painted on the canvas2D and then drawn as an image on the output canvas, not drawn directly on the output canvas',
|
||||
);
|
||||
case StyleElements.text:
|
||||
throw Exception(
|
||||
'Texts are painted on the canvas2D and then drawn as an image on the output canvas, not drawn directly on the output canvas',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _fillBlockText(TextLayout layout, TextBlock block) {
|
||||
for (final (WebCluster clusterText, bool isLtr) in block.getTextClustersInVisualOrder(layout)) {
|
||||
fillTextCluster(clusterText, isLtr);
|
||||
}
|
||||
/*
|
||||
final int start = block.visualClusterStart;
|
||||
final int end = block.visualClusterEnd;
|
||||
final step = block.isLtr ? 1 : -1;
|
||||
for (var i = start; i != end; i += step) {
|
||||
final WebCluster clusterText = block is EllipsisBlock
|
||||
? layout.ellipsisClusters[i]
|
||||
: layout.allClusters[i];
|
||||
|
||||
fillTextCluster(
|
||||
clusterText,
|
||||
block is EllipsisBlock
|
||||
// We shape ellipsis with default direction coming from the attaching block
|
||||
// and all the other blocks with the default paragraph direction.
|
||||
// The reason for shaping ellipsis this way is that we literally attach it to the block
|
||||
// that overflows and we want to keep all the styling attributes (including text direction) consistent.
|
||||
? block.isLtr
|
||||
: layout.paragraph.paragraphStyle.textDirection == ui.TextDirection.ltr,
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void _fillBlockShadows(TextLayout layout, TextBlock block) {
|
||||
if (!block.style.hasElement(StyleElements.shadows) || block.style.shadows == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final (WebCluster clusterText, bool isLtr) in block.getTextClustersInVisualOrder(layout)) {
|
||||
for (final ui.Shadow shadow in clusterText.style.shadows!) {
|
||||
fillShadowCluster(clusterText, shadow, isLtr);
|
||||
}
|
||||
}
|
||||
/*
|
||||
final int start = block.visualClusterStart;
|
||||
final int end = block.visualClusterEnd;
|
||||
final step = block.isLtr ? 1 : -1;
|
||||
for (var i = start; i != end; i += step) {
|
||||
final WebCluster clusterText = block is EllipsisBlock
|
||||
? layout.ellipsisClusters[i]
|
||||
: layout.allClusters[i];
|
||||
|
||||
for (final ui.Shadow shadow in clusterText.style.shadows!) {
|
||||
fillShadowCluster(clusterText, shadow, block.isLtr);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@override
|
||||
void fillTextCluster(WebCluster webTextCluster, bool isDefaultLtr) {
|
||||
final WebTextStyle style = webTextCluster.style;
|
||||
paintContext.fillStyle = style.getForegroundColor().toCssString();
|
||||
webTextCluster.addToContext(paintContext, 0, 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void fillShadowCluster(WebCluster webTextCluster, ui.Shadow shadow, bool isDefaultLtr) {
|
||||
final WebTextStyle style = webTextCluster.style;
|
||||
|
||||
// TODO(jlavrova): see if we can implement shadowing ourself avoiding redrawing text clusters many times.
|
||||
// Answer: we cannot, and also there is a question of calculating the size of the shadow which we have to
|
||||
// take from Chrome as well (performing another measure text operation with shadow attribute set).
|
||||
paintContext.fillStyle = style.getForegroundColor().toCssString();
|
||||
paintContext.shadowColor = shadow.color.toCssString();
|
||||
paintContext.shadowBlur = shadow.blurRadius;
|
||||
paintContext.shadowOffsetX = shadow.offset.dx;
|
||||
paintContext.shadowOffsetY = shadow.offset.dy;
|
||||
WebParagraphDebug.log(
|
||||
'Shadow: x=${shadow.offset.dx} y=${shadow.offset.dy} blur=${shadow.blurRadius} color=${shadow.color.toCssString()}',
|
||||
);
|
||||
|
||||
webTextCluster.addToContext(paintContext, 0, 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(ui.Canvas canvas, TextLayout layout, Painter painter, double x, double y) {
|
||||
final (ui.Rect sourceRect, ui.Rect targetRect) = calculateParagraph(
|
||||
layout,
|
||||
ui.Offset(x, y),
|
||||
ui.window.devicePixelRatio,
|
||||
);
|
||||
// TODO(jlavrova): How resizing affects the cached image?
|
||||
painter.resizePaintCanvas(ui.window.devicePixelRatio, sourceRect.width, sourceRect.height);
|
||||
|
||||
if (!painter.hasSingleImageCache) {
|
||||
// Fill out all the blocks on Canvas2D canvas
|
||||
_fillAllBlocks(StyleElements.shadows, layout);
|
||||
_fillAllBlocks(StyleElements.text, layout);
|
||||
_fillAllBlocks(StyleElements.decorations, layout);
|
||||
|
||||
// Draw background blocks directly on the output canvas
|
||||
// so it will be cached together with the text blocks on Canvas2D canvas
|
||||
_drawAllBlocks(StyleElements.background, canvas, layout, painter, x, y);
|
||||
} else {
|
||||
// We already have cached image for the entire paragraph (including the backgrounds)
|
||||
}
|
||||
|
||||
// Draw the content of Canvas2D on the output canvas
|
||||
painter.drawParagraph(canvas, sourceRect, targetRect);
|
||||
}
|
||||
}
|
||||
@ -9,74 +9,84 @@ import 'package:ui/ui.dart' as ui;
|
||||
import '../canvaskit/canvaskit_api.dart';
|
||||
import '../canvaskit/image.dart';
|
||||
import '../dom.dart';
|
||||
import '../util.dart';
|
||||
import 'debug.dart';
|
||||
import 'layout.dart';
|
||||
import 'paragraph.dart';
|
||||
|
||||
// TODO(mdebbar): Discuss it: we use this canvas for painting the entire block (entire line)
|
||||
// so we need to make sure it's big enough to hold the biggest line.
|
||||
// Also, we use it to paint shadows (with vertical shifts) so we need to make it tall enough as well.
|
||||
const int _paintWidth = 1000;
|
||||
const int _paintHeight = 500;
|
||||
double? currentDevicePixelRatio;
|
||||
final DomOffscreenCanvas paintCanvas = createDomOffscreenCanvas(_paintWidth, _paintHeight);
|
||||
final paintContext = paintCanvas.getContext('2d')! as DomCanvasRenderingContext2D;
|
||||
import 'paint.dart';
|
||||
|
||||
/// Abstracts the interface for painting text clusters, shadows, and decorations.
|
||||
abstract class Painter {
|
||||
Painter();
|
||||
|
||||
/// Fills out the information needed to paint the text cluster.
|
||||
void fillTextCluster(WebCluster webTextCluster, bool isDefaultLtr);
|
||||
bool get hasSingleImageCache => false;
|
||||
|
||||
/// Paints the text cluster previously filled by [fillTextCluster].
|
||||
void paintTextCluster(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
/// Draws the previously filled on Canvas2D text cluster
|
||||
void drawTextCluster(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
|
||||
/// Fills out the information needed to paint the text cluster shadow.
|
||||
void fillShadow(WebCluster webTextCluster, ui.Shadow shadow, bool isDefaultLtr);
|
||||
/// Draws the previously filled on Canvas2D text cluster shadow
|
||||
void drawShadowCluster(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
|
||||
/// Paints the text cluster shadow previously filled by [fillShadow].
|
||||
void paintShadow(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
/// Draws the background directly on canvas
|
||||
void drawBackground(ui.Canvas canvas, TextBlock block, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
|
||||
/// Fills out the information needed to paint the background.
|
||||
void paintBackground(ui.Canvas canvas, TextBlock block, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
/// Draws the previously filled on Canvas2D text decorations
|
||||
void drawDecorations(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
|
||||
/// Fills out the information needed to paint the decorations.
|
||||
void fillDecorations(TextBlock block, ui.Rect sourceRect);
|
||||
void drawParagraph(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
|
||||
/// Paints the decorations previously filled by [fillDecorations].
|
||||
void paintDecorations(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect);
|
||||
void resetCache();
|
||||
bool hasCache();
|
||||
|
||||
/// Adjust the _paintCanvas scale based on device pixel ratio
|
||||
void resizePaintCanvas(double devicePixelRatio) {
|
||||
if (currentDevicePixelRatio == devicePixelRatio) {
|
||||
// Nothing changed
|
||||
void resizePaintCanvas(double devicePixelRatio, double width, double height) {
|
||||
// TODO(jlavrova): we need to investigate different approaches to resizing the canvas.
|
||||
// 1. Do we resize to 0, 0 at the end of each paint so we do not hold on to large buffers?
|
||||
// 2. Do we keep the canvas around (even big ones) and only resize when needed?
|
||||
// 3. Do we have a max size and reuse the canvas up to that size?
|
||||
if (currentDevicePixelRatio == devicePixelRatio &&
|
||||
paintCanvas.width == (width * devicePixelRatio).ceilToDouble() &&
|
||||
paintCanvas.height == (height * devicePixelRatio).ceilToDouble()) {
|
||||
// We need to resize canvas whenever the requested size changes
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentDevicePixelRatio != devicePixelRatio) {
|
||||
// We need to reset the scale transform whenever the device pixel ratio changes
|
||||
resetCache();
|
||||
}
|
||||
|
||||
// Since the output canvas is zoomed by device pixel ratio,
|
||||
// we need to adjust our offscreen canvas accordingly to avoid pixelation
|
||||
// that would happen if didn't resize it.
|
||||
if (currentDevicePixelRatio != null) {
|
||||
paintContext.restore(); // Restore to unscaled state
|
||||
}
|
||||
paintCanvas.width = (_paintWidth * devicePixelRatio).ceilToDouble();
|
||||
paintCanvas.height = (_paintHeight * devicePixelRatio).ceilToDouble();
|
||||
paintCanvas.width = (width * devicePixelRatio).ceilToDouble();
|
||||
paintCanvas.height = (height * devicePixelRatio).ceilToDouble();
|
||||
paintContext.scale(devicePixelRatio, devicePixelRatio);
|
||||
paintContext.save();
|
||||
|
||||
currentDevicePixelRatio = devicePixelRatio;
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'resizePaintCanvas: ${paintCanvas.width}x${paintCanvas.height} @ $devicePixelRatio',
|
||||
);
|
||||
if (WebParagraphDebug.logging) {
|
||||
WebParagraphDebug.log(
|
||||
'resizePaintCanvas: ${paintCanvas.width}x${paintCanvas.height} @ $devicePixelRatio',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DomHTMLCanvasElement? _domHtmlCanvasElement = null;
|
||||
// TODO(jlavrova): uncommend the next line if you want to use an alternative approach
|
||||
// final DomHTMLCanvasElement? _domHtmlCanvasElement = domDocument.createElement('canvas') as DomHTMLCanvasElement;
|
||||
|
||||
class CanvasKitPainter extends Painter {
|
||||
CkImage? _singleImageCache;
|
||||
|
||||
@override
|
||||
void paintBackground(ui.Canvas canvas, LineBlock block, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
bool get hasSingleImageCache => _singleImageCache != null;
|
||||
|
||||
@override
|
||||
void drawBackground(ui.Canvas canvas, LineBlock block, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
// We need to snap the block edges because Skia draws rectangles with subpixel accuracy
|
||||
// and we end up with overlaps (this is only a problem when colors have transparency)
|
||||
// or gaps between blocks (which looks unacceptable - vertical lines between blocks).
|
||||
@ -92,254 +102,81 @@ class CanvasKitPainter extends Painter {
|
||||
}
|
||||
|
||||
@override
|
||||
void fillDecorations(TextBlock block, ui.Rect sourceRect) {
|
||||
paintContext.fillStyle = block.style.getForegroundColor().toCssString();
|
||||
|
||||
final double thickness = calculateThickness(block.style);
|
||||
|
||||
const DoubleDecorationSpacing = 3.0;
|
||||
|
||||
for (final ui.TextDecoration decoration in [
|
||||
ui.TextDecoration.lineThrough,
|
||||
ui.TextDecoration.underline,
|
||||
ui.TextDecoration.overline,
|
||||
]) {
|
||||
if (!block.style.decoration!.contains(decoration)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO(jlavrova): Why using these instead of multiplied values?
|
||||
final double height = block.rawFontBoundingBoxAscent + block.rawFontBoundingBoxDescent;
|
||||
final double ascent = block.rawFontBoundingBoxAscent;
|
||||
final double position = calculatePosition(decoration, thickness, height, ascent);
|
||||
WebParagraphDebug.log('decoration=$decoration thickness=$thickness position=$position');
|
||||
|
||||
final double width = sourceRect.width;
|
||||
final double x = sourceRect.left;
|
||||
final double y = sourceRect.top + position;
|
||||
|
||||
paintContext.reset();
|
||||
paintContext.lineWidth = thickness;
|
||||
paintContext.strokeStyle = block.style.decorationColor!.toCssString();
|
||||
|
||||
switch (block.style.decorationStyle!) {
|
||||
case ui.TextDecorationStyle.wavy:
|
||||
calculateWaves(x, y, block.style, sourceRect, thickness);
|
||||
|
||||
case ui.TextDecorationStyle.double:
|
||||
final double bottom = y + DoubleDecorationSpacing + thickness;
|
||||
paintContext.beginPath();
|
||||
paintContext.moveTo(x, y);
|
||||
paintContext.lineTo(x + width, y);
|
||||
paintContext.moveTo(x, bottom);
|
||||
paintContext.lineTo(x + width, bottom);
|
||||
paintContext.stroke();
|
||||
WebParagraphDebug.log('double: $x:${x + width}, $y:$bottom');
|
||||
|
||||
case ui.TextDecorationStyle.dashed:
|
||||
case ui.TextDecorationStyle.dotted:
|
||||
final dashes = Float32List(2)
|
||||
..[0] =
|
||||
thickness * (block.style.decorationStyle! == ui.TextDecorationStyle.dotted ? 1 : 4)
|
||||
..[1] = thickness;
|
||||
|
||||
paintContext.setLineDash(dashes);
|
||||
paintContext.beginPath();
|
||||
paintContext.moveTo(x, y);
|
||||
paintContext.lineTo(x + width, y);
|
||||
paintContext.stroke();
|
||||
WebParagraphDebug.log('dashed/dotted: $x:${x + width}, $y');
|
||||
|
||||
case ui.TextDecorationStyle.solid:
|
||||
paintContext.beginPath();
|
||||
paintContext.moveTo(x, y);
|
||||
paintContext.lineTo(x + width, y);
|
||||
paintContext.stroke();
|
||||
WebParagraphDebug.log(
|
||||
'solid: $x:${x + width}, $y ${block.style.decorationColor!.toCssString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
void drawDecorations(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
throw UnimplementedError('Decoration drawing is not implemented yet');
|
||||
}
|
||||
|
||||
@override
|
||||
void paintDecorations(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
final DomImageBitmap bitmap = paintCanvas.transferToImageBitmap();
|
||||
|
||||
final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true);
|
||||
if (skImage == null) {
|
||||
throw Exception('Failed to convert text image bitmap to an SkImage.');
|
||||
}
|
||||
|
||||
final ckImage = CkImage(skImage, imageSource: ImageBitmapImageSource(bitmap));
|
||||
canvas.drawImageRect(
|
||||
ckImage,
|
||||
sourceRect,
|
||||
targetRect,
|
||||
ui.Paint()..filterQuality = ui.FilterQuality.none,
|
||||
);
|
||||
void drawShadowCluster(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
throw UnimplementedError('Shadow drawing is not implemented yet');
|
||||
}
|
||||
|
||||
@override
|
||||
void fillShadow(WebCluster webTextCluster, ui.Shadow shadow, bool isDefaultLtr) {
|
||||
final WebTextStyle style = webTextCluster.style;
|
||||
|
||||
// TODO(jlavrova): see if we can implement shadowing ourself avoiding redrawing text clusters many times.
|
||||
// Answer: we cannot, and also there is a question of calculating the size of the shadow which we have to
|
||||
// take from Chrome as well (performing another measure text operation with shadow attribute set).
|
||||
paintContext.fillStyle = style.getForegroundColor().toCssString();
|
||||
paintContext.shadowColor = shadow.color.toCssString();
|
||||
paintContext.shadowBlur = shadow.blurRadius;
|
||||
paintContext.shadowOffsetX = shadow.offset.dx;
|
||||
paintContext.shadowOffsetY = shadow.offset.dy;
|
||||
WebParagraphDebug.log(
|
||||
'Shadow: x=${shadow.offset.dx} y=${shadow.offset.dy} blur=${shadow.blurRadius} color=${shadow.color.toCssString()}',
|
||||
);
|
||||
|
||||
// We fill the text cluster into a rectange [0,0,w,h]
|
||||
// but we need to shift the y coordinate by the font ascent
|
||||
// becase the text is drawn at the ascent, not at 0
|
||||
webTextCluster.fillOnContext(
|
||||
paintContext,
|
||||
/*ignore the text cluster shift from the text run*/
|
||||
// TODO(jlavrova): calculate the proper shift for the shadow
|
||||
x: (isDefaultLtr ? 0 : webTextCluster.advance.width) + 100,
|
||||
y: 100,
|
||||
);
|
||||
void drawTextCluster(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
throw UnimplementedError('Text cluster drawing is not implemented yet');
|
||||
}
|
||||
|
||||
@override
|
||||
void paintShadow(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
// TODO(jlavrova): calculate the shadow bounds properly
|
||||
final ui.Rect shadowSourceRect = sourceRect.inflate(100).translate(100, 100);
|
||||
final ui.Rect shadowTargetRect = targetRect.inflate(100);
|
||||
|
||||
final DomImageBitmap bitmap = paintCanvas.transferToImageBitmap();
|
||||
|
||||
final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true);
|
||||
if (skImage == null) {
|
||||
throw Exception('Failed to convert text image bitmap to an SkImage.');
|
||||
}
|
||||
final ckImage = CkImage(skImage, imageSource: ImageBitmapImageSource(bitmap));
|
||||
canvas.drawImageRect(
|
||||
ckImage,
|
||||
shadowSourceRect,
|
||||
shadowTargetRect,
|
||||
ui.Paint()..filterQuality = ui.FilterQuality.none,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void fillTextCluster(WebCluster webTextCluster, bool isDefaultLtr) {
|
||||
final WebTextStyle style = webTextCluster.style;
|
||||
paintContext.fillStyle = style.getForegroundColor().toCssString();
|
||||
// We fill the text cluster into a rectange [0,0,w,h]
|
||||
// but we need to shift the y coordinate by the font ascent
|
||||
// becase the text is drawn at the ascent, not at 0
|
||||
webTextCluster.fillOnContext(
|
||||
paintContext,
|
||||
/*ignore the text cluster shift from the text run*/
|
||||
x: (isDefaultLtr ? 0 : webTextCluster.advance.width),
|
||||
y: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void paintTextCluster(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
final DomImageBitmap bitmap = paintCanvas.transferToImageBitmap();
|
||||
|
||||
final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true);
|
||||
if (skImage == null) {
|
||||
throw Exception('Failed to convert text image bitmap to an SkImage.');
|
||||
}
|
||||
|
||||
final ckImage = CkImage(skImage, imageSource: ImageBitmapImageSource(bitmap));
|
||||
canvas.drawImageRect(
|
||||
ckImage,
|
||||
sourceRect,
|
||||
targetRect,
|
||||
ui.Paint()..filterQuality = ui.FilterQuality.none,
|
||||
);
|
||||
}
|
||||
|
||||
double calculateThickness(WebTextStyle textStyle) {
|
||||
return (textStyle.fontSize! / 14.0) * (textStyle.decorationThickness ?? 1.0);
|
||||
}
|
||||
|
||||
double calculatePosition(
|
||||
ui.TextDecoration decoration,
|
||||
double thickness,
|
||||
double height,
|
||||
double ascent,
|
||||
) {
|
||||
switch (decoration) {
|
||||
case ui.TextDecoration.underline:
|
||||
WebParagraphDebug.log(
|
||||
'calculatePosition underline: $thickness + $ascent = ${thickness + ascent}',
|
||||
void drawParagraph(ui.Canvas canvas, ui.Rect sourceRect, ui.Rect targetRect) {
|
||||
if (!hasSingleImageCache) {
|
||||
// We should have resized the small canvas before calling this method
|
||||
if (sourceRect.width != paintCanvas.width || sourceRect.height != paintCanvas.height) {
|
||||
assert(
|
||||
false,
|
||||
'resizePaintCanvas needed: '
|
||||
'canvas=${paintCanvas.width}x${paintCanvas.height} vs bounds=${sourceRect.width}x${sourceRect.height}',
|
||||
);
|
||||
return thickness + ascent;
|
||||
case ui.TextDecoration.overline:
|
||||
WebParagraphDebug.log('calculatePosition overline: 0');
|
||||
return thickness / 2;
|
||||
case ui.TextDecoration.lineThrough:
|
||||
WebParagraphDebug.log('calculatePosition through: $height / 2 = ${height / 2}');
|
||||
return height / 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void calculateWaves(
|
||||
double x,
|
||||
double y,
|
||||
WebTextStyle textStyle,
|
||||
ui.Rect textBounds,
|
||||
double thickness,
|
||||
) {
|
||||
final quarterWave = thickness;
|
||||
|
||||
var waveCount = 0;
|
||||
double xStart = 0;
|
||||
final double yStart = y + quarterWave;
|
||||
|
||||
WebParagraphDebug.log(
|
||||
'calculateWaves($x, $y, '
|
||||
'${textBounds.left}:${textBounds.right}x${textBounds.top}:${textBounds.bottom} )'
|
||||
'$thickness $xStart $yStart',
|
||||
);
|
||||
paintContext.beginPath();
|
||||
//paintContext.moveTo(x, y + quarterWave);
|
||||
while (xStart + quarterWave * 2 < textBounds.width) {
|
||||
final x1 = xStart;
|
||||
final double y1 = yStart + quarterWave * (waveCount.isEven ? 1 : -1);
|
||||
final double x2 = xStart + quarterWave * 2;
|
||||
final y2 = yStart;
|
||||
WebParagraphDebug.log('wave: $x1, $y1, $x2, $y2');
|
||||
paintContext.quadraticCurveTo(x1, y1, x2, y2);
|
||||
xStart += quarterWave * 2;
|
||||
++waveCount;
|
||||
}
|
||||
|
||||
// The rest of the wave
|
||||
final double remaining = textBounds.width - xStart;
|
||||
if (remaining > 0) {
|
||||
final x1 = xStart;
|
||||
final double y1 = yStart + quarterWave * (waveCount.isEven ? 1 : -1);
|
||||
//final double y1 = yStart + remaining / 2 * (waveCount.isEven ? 1 : -1);
|
||||
final double x2 = xStart + remaining;
|
||||
final y2 = yStart;
|
||||
//final double y2 = yStart + remaining + remaining / quarterWave * y1;
|
||||
WebParagraphDebug.log(
|
||||
'remaining: ${textBounds.width} - $xStart = $remaining '
|
||||
'$x1, $y1, $x2, $y2',
|
||||
}
|
||||
final DomImageData imageData = paintContext.getImageData(
|
||||
0,
|
||||
0,
|
||||
sourceRect.width.ceil(),
|
||||
sourceRect.height.ceil(),
|
||||
);
|
||||
paintContext.quadraticCurveTo(x1, y1, x2, y2);
|
||||
|
||||
final imageInfo = SkImageInfo(
|
||||
alphaType: canvasKit.AlphaType.Premul,
|
||||
colorType: canvasKit.ColorType.RGBA_8888,
|
||||
colorSpace: SkColorSpaceSRGB,
|
||||
width: sourceRect.width,
|
||||
height: sourceRect.height,
|
||||
);
|
||||
|
||||
final SkImage? skImage = canvasKit.MakeImage(
|
||||
imageInfo,
|
||||
Uint8List.view(imageData.data.buffer),
|
||||
4 * sourceRect.width,
|
||||
);
|
||||
|
||||
// Transfer the buffer from the small canvas
|
||||
// This is synchronous and returns the handle immediately
|
||||
//final DomImageBitmap bitmap = paintCanvas.transferToImageBitmap();
|
||||
//final SkImage? skImage = canvasKit.MakeLazyImageFromImageBitmap(bitmap, true);
|
||||
|
||||
if (skImage == null) {
|
||||
throw Exception('Failed to convert text image bitmap to an SkImage.');
|
||||
}
|
||||
_singleImageCache = CkImage(skImage);
|
||||
}
|
||||
paintContext.stroke();
|
||||
|
||||
canvas.drawImageRect(
|
||||
_singleImageCache!,
|
||||
sourceRect,
|
||||
targetRect,
|
||||
ui.Paint()..filterQuality = ui.FilterQuality.none,
|
||||
);
|
||||
}
|
||||
|
||||
void drawLineAsRect(double x, double y, double width, double thickness) {
|
||||
final double radius = thickness / 2;
|
||||
paintContext.fillRect(x, y - radius, x + width, y + radius);
|
||||
WebParagraphDebug.log('paintContext.fillRect($x, $y - $radius, $x + $width, $y + $radius);');
|
||||
@override
|
||||
void resetCache() {
|
||||
if (_singleImageCache != null) {
|
||||
_singleImageCache!.dispose();
|
||||
_singleImageCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hasCache() {
|
||||
return _singleImageCache != null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import '../view_embedder/style_manager.dart';
|
||||
import 'debug.dart';
|
||||
import 'layout.dart';
|
||||
import 'paint.dart';
|
||||
import 'paint_paragraph.dart';
|
||||
import 'painter.dart';
|
||||
|
||||
@visibleForTesting
|
||||
@ -145,6 +146,8 @@ enum StyleElements {
|
||||
text,
|
||||
}
|
||||
|
||||
enum ShadowDirection { left, right, top, bottom }
|
||||
|
||||
class WebTextStyle implements ui.TextStyle {
|
||||
factory WebTextStyle({
|
||||
String? fontFamily,
|
||||
@ -967,17 +970,7 @@ class WebParagraph implements ui.Paragraph {
|
||||
}
|
||||
|
||||
void paint(ui.Canvas canvas, ui.Offset offset) {
|
||||
_paint.painter.resizePaintCanvas(ui.window.devicePixelRatio);
|
||||
for (final TextLine line in _layout.lines) {
|
||||
_paint.paintLine(canvas, _layout, line, offset.dx, offset.dy);
|
||||
}
|
||||
}
|
||||
|
||||
void paintOnCanvas2D(DomHTMLCanvasElement canvas, ui.Offset offset) {
|
||||
_paint.painter.resizePaintCanvas(ui.window.devicePixelRatio);
|
||||
for (final TextLine line in _layout.lines) {
|
||||
_paint.paintLineOnCanvas2D(canvas, _layout, line, offset.dx, offset.dy);
|
||||
}
|
||||
_paint.paint(canvas, _layout, _painter, offset.dx, offset.dy);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1087,7 +1080,8 @@ class WebParagraph implements ui.Paragraph {
|
||||
}
|
||||
|
||||
late final TextLayout _layout = TextLayout(this);
|
||||
late final TextPaint _paint = TextPaint(this, CanvasKitPainter());
|
||||
late final TextPaint _paint = PaintParagraph(this);
|
||||
late final Painter _painter = CanvasKitPainter();
|
||||
}
|
||||
|
||||
class WebLineMetrics implements ui.LineMetrics {
|
||||
@ -1294,9 +1288,11 @@ class WebParagraphBuilder implements ui.ParagraphBuilder {
|
||||
final text = _fullTextBuffer.toString();
|
||||
|
||||
final paragraph = WebParagraph(_paragraphStyle, _spans, text);
|
||||
WebParagraphDebug.apiTrace('WebParagraphBuilder.build(): "$text" ${_spans.length}');
|
||||
for (var i = 0; i < _spans.length; ++i) {
|
||||
WebParagraphDebug.log('$i: ${_spans[i]}');
|
||||
if (WebParagraphDebug.apiLogging) {
|
||||
WebParagraphDebug.apiTrace('WebParagraphBuilder.build(): "$text" ${_spans.length}');
|
||||
for (var i = 0; i < _spans.length; ++i) {
|
||||
WebParagraphDebug.log('$i: ${_spans[i]}');
|
||||
}
|
||||
}
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
@ -58,8 +58,6 @@ class TextWrapper {
|
||||
|
||||
if (hardLineBreak) {
|
||||
// Break the line and then continue with the current cluster as usual
|
||||
WebParagraphDebug.log('isHardLineBreak: $index');
|
||||
|
||||
line.consumePendingText();
|
||||
|
||||
// This is the case when the ellipsis will be added to the empty line; weird...
|
||||
@ -70,7 +68,6 @@ class TextWrapper {
|
||||
}
|
||||
} else if (_isSoftLineBreak(cluster) && line.isNotEmpty) {
|
||||
// Mark the potential line break and then continue with the current cluster as usual
|
||||
WebParagraphDebug.log('isSoftLineBreak: $index');
|
||||
if (line.hasLeadingWhitespaces) {
|
||||
// There is one case when we have to ignore this soft line break: if we only had whitespaces so far -
|
||||
// these are the leading spaces and Flutter wants them to be preserved
|
||||
@ -164,20 +161,6 @@ class TextWrapper {
|
||||
_top +=_layout.addLine(emptyClusterRange, 0.0, emptyClusterRange, 0.0, false, _top,);
|
||||
}
|
||||
*/
|
||||
/*
|
||||
if (WebParagraphDebug.logging) {
|
||||
for (int i = 0; i < _layout.lines.length; ++i) {
|
||||
final TextLine line = _layout.lines[i];
|
||||
final String text = _text.substring(line.textRange.start, line.textRange.end);
|
||||
final String whitespaces =
|
||||
!line.whitespacesRange.isEmpty ? '${line.whitespacesRange.width}' : 'no';
|
||||
final String hardLineBreak = line.hardLineBreak ? 'hardlineBreak' : '';
|
||||
WebParagraphDebug.log(
|
||||
'$i: "$text" [${line.textRange.start}:${line.textRange.end}) $width $hardLineBreak ($whitespaces trailing whitespaces)',
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,8 +395,9 @@ class _LineBuilder {
|
||||
// We have removed all the clusters in this line and still can't fit the ellipsis
|
||||
// Not sure what to do in this case
|
||||
// TODO(jlavrova): Implement this case
|
||||
assert(false, 'Ellipsizing requires removing the whole line, not implemented yet');
|
||||
return false;
|
||||
throw UnimplementedError(
|
||||
'Ellipsizing requires removing the whole line, not implemented yet',
|
||||
);
|
||||
}
|
||||
final WebCluster cluster = _layout.allClusters[clusterIndex - 1];
|
||||
final double widthCluster = cluster.advance.width;
|
||||
@ -426,33 +410,23 @@ class _LineBuilder {
|
||||
? ui.TextDirection.ltr
|
||||
: ui.TextDirection.rtl,
|
||||
);
|
||||
WebParagraphDebug.log(
|
||||
'Ellipsize: $clusterIndex $_widthConsumedText $_widthWhitespaces $_widthPendingText - $cutOffWidth - $widthCluster + ${ellipsisSpan.advanceWidth()!} ??? $_maxWidth',
|
||||
);
|
||||
cutOffWidth += widthCluster;
|
||||
if (_isWhitespace(cluster)) {
|
||||
// We skip whitespaces when cutting off for ellipsis, so just continue
|
||||
WebParagraphDebug.log('Ellipsize: whitespace');
|
||||
} else if (canFit(ellipsisSpan.advanceWidth()! - cutOffWidth)) {
|
||||
WebParagraphDebug.log('Ellipsize: stop $clusterIndex');
|
||||
// We can fit the ellipsis now
|
||||
_layout.ellipsisClusters = ellipsisSpan.extractClusters();
|
||||
break;
|
||||
} else {
|
||||
WebParagraphDebug.log('Ellipsize: continue $clusterIndex');
|
||||
}
|
||||
// Remove this cluster, correct the structures and try again
|
||||
clusterIndex -= 1;
|
||||
if (clusterIndex >= _whitespaceEnd) {
|
||||
WebParagraphDebug.log('Ellipsize: pending text >= $_whitespaceEnd');
|
||||
_widthPendingText -= widthCluster;
|
||||
_pendingTextEnd = clusterIndex;
|
||||
} else if (clusterIndex >= _whitespaceStart) {
|
||||
WebParagraphDebug.log('Ellipsize: whitespaces => $_whitespaceStart');
|
||||
_widthWhitespaces -= widthCluster;
|
||||
_whitespaceEnd = clusterIndex;
|
||||
} else {
|
||||
WebParagraphDebug.log('Ellipsize: consumed text >= $start');
|
||||
_widthConsumedText -= widthCluster;
|
||||
_whitespaceStart = clusterIndex;
|
||||
_whitespaceEnd = clusterIndex;
|
||||
|
||||
@ -0,0 +1 @@
|
||||
../webparagraph/paragraph_performance_test.dart
|
||||
@ -167,11 +167,21 @@ Future<void> testMain() async {
|
||||
|
||||
final ui.TextBox top0 = rectsTop[0];
|
||||
final ui.TextBox top1 = rectsTop[1];
|
||||
final ui.TextBox bottom0 = rectsBottom[0];
|
||||
final ui.TextBox bottom1 = rectsBottom[1];
|
||||
final ui.TextBox middle0 = rectsMiddle[0];
|
||||
final ui.TextBox middle1 = rectsMiddle[1];
|
||||
expect((top0.bottom - bottom1.top).abs() < EPSILON, true);
|
||||
expect(middle1.top > bottom1.top, true);
|
||||
expect(middle1.top < top1.top, true);
|
||||
|
||||
expect((top0.top - bottom0.top).abs() < EPSILON, true);
|
||||
expect((top0.top - middle0.top).abs() < EPSILON, true);
|
||||
expect(top0.bottom < middle0.bottom, true);
|
||||
expect(middle0.bottom < bottom0.bottom, true);
|
||||
|
||||
expect((top0.bottom - top1.top).abs() < EPSILON, true);
|
||||
expect((middle0.bottom - middle1.top).abs() < EPSILON, true);
|
||||
expect((bottom0.bottom - bottom1.top).abs() < EPSILON, true);
|
||||
expect(top1.bottom < middle1.bottom, true);
|
||||
expect(middle1.bottom < bottom1.bottom, true);
|
||||
});
|
||||
|
||||
test('Paragraph getBoxesForRange 1 finite line', () {
|
||||
|
||||
@ -0,0 +1,155 @@
|
||||
// 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';
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
import '../common/test_initialization.dart';
|
||||
import '../ui/utils.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
typedef AsyncAction<R> = Future<R> Function();
|
||||
|
||||
Future<R> timeActionAsync<R>(String name, AsyncAction<R> action) async {
|
||||
if (!Profiler.isBenchmarkMode) {
|
||||
return action();
|
||||
} else {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final R result = await action();
|
||||
stopwatch.stop();
|
||||
Profiler.instance.benchmark(name, stopwatch.elapsedMicroseconds.toDouble());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
WebParagraphProfiler.register();
|
||||
setUpUnitTests(withImplicitView: true, setUpTestViewDimensions: false);
|
||||
|
||||
Future<void> draw(
|
||||
String image,
|
||||
String text,
|
||||
String testName,
|
||||
int countLayouts,
|
||||
int countPaints,
|
||||
) async {
|
||||
WebParagraphProfiler.reset();
|
||||
final recorder = PictureRecorder();
|
||||
const region = Rect.fromLTWH(0, 0, 1000, 1000);
|
||||
final paragraphs = <Paragraph>[];
|
||||
for (var i = 0; i < countLayouts; i++) {
|
||||
final Paragraph paragraph = timeAction((i == 0 ? 'build.first' : 'build'), () {
|
||||
final arialStyle = ParagraphStyle(fontFamily: 'Roboto', fontSize: 20);
|
||||
final builder = ParagraphBuilder(arialStyle);
|
||||
builder.pushStyle(TextStyle(color: const Color(0xFF000000)));
|
||||
builder.addText('$text$i');
|
||||
return builder.build();
|
||||
});
|
||||
paragraphs.add(paragraph);
|
||||
timeAction((i == 0 ? 'layout.first' : 'layout'), () {
|
||||
paragraph.layout(const ParagraphConstraints(width: 1000));
|
||||
});
|
||||
}
|
||||
for (var j = 0; j < countPaints; ++j) {
|
||||
for (final paragraph in paragraphs) {
|
||||
final canvas = Canvas(recorder, region);
|
||||
canvas.drawColor(const Color(0xFFFFFFFF), BlendMode.src);
|
||||
await timeActionAsync((j == 0 ? 'paint.first' : 'paint'), () async {
|
||||
canvas.drawParagraph(paragraph, const Offset(20, 20));
|
||||
await drawPictureUsingCurrentRenderer(
|
||||
recorder.endRecording(),
|
||||
); // This is a hack to make sure the canvas is flushed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await matchGoldenFile('$image.png', region: region);
|
||||
WebParagraphProfiler.log();
|
||||
}
|
||||
|
||||
test('Dummy test to warm up GPU', () async {
|
||||
await draw('dummyText', 'Dummy text', 'Dummy text', 1, 1);
|
||||
}, timeout: Timeout.none);
|
||||
|
||||
test('Build/Layout/Paint small text', () async {
|
||||
await draw('smallText', 'Abcdef', 'Small text', 10, 100);
|
||||
}, timeout: Timeout.none);
|
||||
|
||||
test('Build/Layout/Paint medium text', () async {
|
||||
await draw('mediumText', 'Abcdef ghijkl mnopqrs tuvwxyz.', 'Medium text', 10, 100);
|
||||
}, timeout: Timeout.none);
|
||||
|
||||
test('Build/Layout/Paint large text', () async {
|
||||
await draw(
|
||||
'largeText',
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. '
|
||||
'Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz. Abcdef ghijkl mnopqrs tuvwxyz',
|
||||
'Large text',
|
||||
10,
|
||||
100,
|
||||
);
|
||||
}, timeout: Timeout.none);
|
||||
|
||||
test(
|
||||
'Paint text by sizes',
|
||||
() async {
|
||||
WebParagraphProfiler.reset();
|
||||
final recorder = PictureRecorder();
|
||||
const region = Rect.fromLTWH(0, 0, 1000, 1000);
|
||||
for (var textSize = 10; textSize < 1000; textSize += (textSize == 10 ? 40 : 50)) {
|
||||
final Paragraph paragraph = timeAction('build$textSize', () {
|
||||
final arialStyle = ParagraphStyle(fontFamily: 'Roboto', fontSize: 20);
|
||||
final builder = ParagraphBuilder(arialStyle);
|
||||
builder.pushStyle(TextStyle(color: const Color(0xFF000000)));
|
||||
builder.addText('0123456789' * (textSize ~/ 10));
|
||||
return builder.build();
|
||||
});
|
||||
timeAction('layout$textSize', () {
|
||||
paragraph.layout(const ParagraphConstraints(width: 1000));
|
||||
});
|
||||
final canvas = Canvas(recorder, region);
|
||||
canvas.drawColor(const Color(0xFFFFFFFF), BlendMode.src);
|
||||
await timeActionAsync('paint$textSize', () async {
|
||||
canvas.drawParagraph(paragraph, const Offset(20, 20));
|
||||
await drawPictureUsingCurrentRenderer(
|
||||
recorder.endRecording(),
|
||||
); // This is a hack to make sure the canvas is flushed
|
||||
});
|
||||
}
|
||||
|
||||
await matchGoldenFile('textSize.png', region: region);
|
||||
WebParagraphProfiler.log();
|
||||
},
|
||||
timeout: Timeout.none,
|
||||
skip: true,
|
||||
);
|
||||
}
|
||||
@ -390,7 +390,7 @@ Future<void> testMain() async {
|
||||
Shadow(color: Color(0xFF00FF00), offset: Offset(0, -10), blurRadius: 2.0),
|
||||
Shadow(color: Color(0xFFFF0000), offset: Offset(-10, 0), blurRadius: 2.0),
|
||||
Shadow(color: Color(0xFF0000FF), offset: Offset(10, 0), blurRadius: 2.0),
|
||||
Shadow(color: Color(0xFF888888), offset: Offset(0, 10), blurRadius: 2.0),
|
||||
Shadow(color: Color(0xFFFF00FF), offset: Offset(0, 10), blurRadius: 2.0),
|
||||
],
|
||||
);
|
||||
final leftShadow = WebTextStyle(
|
||||
@ -455,7 +455,11 @@ Future<void> testMain() async {
|
||||
const greenColor = Color(0xFF00FF00);
|
||||
const grayColor = Color(0xFF888888);
|
||||
|
||||
final paragraphStyle = WebParagraphStyle(fontFamily: 'Roboto', fontSize: 40);
|
||||
final paragraphStyle = WebParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 40,
|
||||
color: const Color(0xFF000000),
|
||||
);
|
||||
|
||||
final defaultStyle = WebTextStyle(foreground: blackPaint);
|
||||
|
||||
@ -663,6 +667,7 @@ Future<void> testMain() async {
|
||||
canvas.drawRect(rect.toRect(), bluePaint);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
final List<TextBox> rects = paragraph.getBoxesForRange(
|
||||
0,
|
||||
@ -674,6 +679,7 @@ Future<void> testMain() async {
|
||||
canvas.drawRect(rect.toRect(), redPaint);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
final List<TextBox> rects = paragraph.getBoxesForRange(
|
||||
0,
|
||||
@ -983,7 +989,6 @@ Future<void> testMain() async {
|
||||
|
||||
{
|
||||
final builder = WebParagraphBuilder(paragraphStyle);
|
||||
|
||||
builder.pushStyle(style30);
|
||||
builder.addText('This is a long text that should be ellipsized at the end');
|
||||
builder.pop();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user