Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 12 additions & 28 deletions lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -440,13 +440,10 @@ class BitmapCanvas extends EngineCanvas {
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
if (_useDomForRenderingFillAndStroke(paint)) {
rect = adjustRectForDom(rect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-rect', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
} else {
setUpPaint(paint, rect);
_canvasPool.drawRect(rect, paint.style);
Expand Down Expand Up @@ -482,16 +479,12 @@ class BitmapCanvas extends EngineCanvas {

@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
final ui.Rect rect = rrect.outerRect;
if (_useDomForRenderingFillAndStroke(paint)) {
final ui.Rect rect = adjustRectForDom(rrect.outerRect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-rrect', _canvasPool.currentTransform);
applyRRectBorderRadius(element.style, rrect);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
} else {
setUpPaint(paint, rrect.outerRect);
_canvasPool.drawRRect(rrect, paint.style);
Expand All @@ -509,13 +502,10 @@ class BitmapCanvas extends EngineCanvas {
@override
void drawOval(ui.Rect rect, SurfacePaintData paint) {
if (_useDomForRenderingFill(paint)) {
rect = adjustRectForDom(rect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-oval', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
element.style.borderRadius =
'${rect.width / 2.0}px / ${rect.height / 2.0}px';
} else {
Expand All @@ -527,15 +517,11 @@ class BitmapCanvas extends EngineCanvas {

@override
void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) {
final ui.Rect rect = ui.Rect.fromCircle(center: c, radius: radius);
if (_useDomForRenderingFillAndStroke(paint)) {
final ui.Rect rect = adjustRectForDom(ui.Rect.fromCircle(center: c, radius: radius), paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-circle', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
element.style.borderRadius = '50%';
} else {
setUpPaint(
Expand All @@ -555,21 +541,19 @@ class BitmapCanvas extends EngineCanvas {
final SurfacePath surfacePath = path as SurfacePath;
final ui.Rect? pathAsLine = surfacePath.toStraightLine();
if (pathAsLine != null) {
final ui.Rect rect = (pathAsLine.top == pathAsLine.bottom)
ui.Rect rect = (pathAsLine.top == pathAsLine.bottom)
? ui.Rect.fromLTWH(
pathAsLine.left, pathAsLine.top, pathAsLine.width, 1)
: ui.Rect.fromLTWH(
pathAsLine.left, pathAsLine.top, 1, pathAsLine.height);

rect = adjustRectForDom(rect, paint);
final DomHTMLElement element = buildDrawRectElement(
rect, paint, 'draw-rect', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(math.min(rect.left, rect.right),
math.min(rect.top, rect.bottom)),
paint);
_drawElement(element, rect.topLeft, paint);
return;
}

final ui.Rect? pathAsRect = surfacePath.toRect();
if (pathAsRect != null) {
drawRect(pathAsRect, paint);
Expand Down
105 changes: 80 additions & 25 deletions lib/web_ui/lib/src/engine/html/dom_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,16 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {

@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
rect = adjustRectForDom(rect, paint);
currentElement.append(
buildDrawRectElement(rect, paint, 'draw-rect', currentTransform));
}

@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
final ui.Rect outerRect = adjustRectForDom(rrect.outerRect, paint);
final DomElement element = buildDrawRectElement(
rrect.outerRect, paint, 'draw-rrect', currentTransform);
outerRect, paint, 'draw-rrect', currentTransform);
applyRRectBorderRadius(element.style, rrect);
currentElement.append(element);
}
Expand Down Expand Up @@ -160,8 +162,77 @@ ui.Color blurColor(ui.Color color, double sigma) {
return ui.Color((reducedAlpha & 0xff) << 24 | (color.value & 0x00ffffff));
}

/// When drawing a shape (rect, rrect, circle, etc) in DOM/CSS, the [rect] given
/// by Flutter needs to be adjusted to what DOM/CSS expect.
///
/// This method takes Flutter's [rect] and produces a new rect that can be used
/// to generate the correct CSS properties to match Flutter's expectations.
///
///
/// Here's what Flutter's given [rect] and [paint.strokeWidth] represent:
///
/// top-left ↓
/// ┌──↓──────────────────────┐
/// →→→→x x │←←
/// │ ┌───────────────┐ │ |
/// │ │ │ │ |
/// │ │ │ │ | height
/// │ │ │ │ |
/// │ └───────────────┘ │ |
/// │ x x │←←
/// └─────────────────────────┘
/// stroke-width ↑----↑ ↑
/// ↑-------------------↑ width
///
///
///
/// In the DOM/CSS, here's how the coordinates should look like:
///
/// top-left ↓
/// →→x─────────────────────────┐
/// │ │
/// │ x───────────────x │←←
/// │ │ │ │ |
/// │ │ │ │ | height
/// │ │ │ │ |
/// │ x───────────────x │←←
/// │ │
/// └─────────────────────────┘
/// border-width ↑----↑ ↑
/// ↑---------------↑ width
///
/// As shown in the drawing above, the width/height don't start at the top-left
/// coordinates. Instead, they start from the inner top-left (inside the border).
ui.Rect adjustRectForDom(ui.Rect rect, SurfacePaintData paint) {
double left = math.min(rect.left, rect.right);
double top = math.min(rect.top, rect.bottom);
double width = rect.width.abs();
double height = rect.height.abs();

final bool isStroke = paint.style == ui.PaintingStyle.stroke;
final double strokeWidth = paint.strokeWidth ?? 0.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we remove both of the if blocks below, if we did the following ahead of time?

if (paint.style == ui.PaintingStyle.fill || strokeWidth == 0.0) {
  return rect;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we can't.

Stroke-related adjustments aren't the only ones needed. We need to also adjust left and top to avoid flipped rectangles.

E.g. Rect.fromLTRB(200, 200, 100, 100) is completely fine in Flutter, but the DOM doesn't like it. If you do rect.width on a flipped rectangle, you get a negative value. Setting a negative width/height on a DOM element is equivalent to zero.

buildDrawRectElement and various draw operations used to handle it. See this:

final double left = math.min(rect.left, rect.right);
final double right = math.max(rect.left, rect.right);
final double top = math.min(rect.top, rect.bottom);
final double bottom = math.max(rect.top, rect.bottom);

and:
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);

All of that can go away now because rects produced by the new adjustRectForDom are never flipped.

if (isStroke && strokeWidth > 0.0) {
left -= strokeWidth / 2.0;
top -= strokeWidth / 2.0;

// width and height shouldn't go below zero.
width = math.max(0, width - strokeWidth);
height = math.max(0, height - strokeWidth);
}

if (left != rect.left ||
top != rect.top ||
width != rect.width ||
height != rect.height) {
return ui.Rect.fromLTWH(left, top, width, height);
}
return rect;
}

DomHTMLElement buildDrawRectElement(
ui.Rect rect, SurfacePaintData paint, String tagName, Matrix4 transform) {
assert(rect.left <= rect.right);
assert(rect.top <= rect.bottom);
final DomHTMLElement rectangle = domDocument.createElement(tagName) as
DomHTMLElement;
assert(() {
Expand All @@ -172,26 +243,11 @@ DomHTMLElement buildDrawRectElement(
String effectiveTransform;
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
final double strokeWidth = paint.strokeWidth ?? 0.0;
final double left = math.min(rect.left, rect.right);
final double right = math.max(rect.left, rect.right);
final double top = math.min(rect.top, rect.bottom);
final double bottom = math.max(rect.top, rect.bottom);
if (transform.isIdentity()) {
if (isStroke) {
effectiveTransform =
'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)';
} else {
effectiveTransform = 'translate(${left}px, ${top}px)';
}
effectiveTransform = 'translate(${rect.left}px, ${rect.top}px)';
} else {
// Clone to avoid mutating _transform.
final Matrix4 translated = transform.clone();
if (isStroke) {
translated.translate(
left - (strokeWidth / 2.0), top - (strokeWidth / 2.0));
} else {
translated.translate(left, top);
}
// Clone to avoid mutating `transform`.
final Matrix4 translated = transform.clone()..translate(rect.left, rect.top);
effectiveTransform = matrix4ToCssTransform(translated);
}
final DomCSSStyleDeclaration style = rectangle.style;
Expand All @@ -216,15 +272,14 @@ DomHTMLElement buildDrawRectElement(
}
}

style
..width = '${rect.width}px'
..height = '${rect.height}px';

if (isStroke) {
style
..width = '${right - left - strokeWidth}px'
..height = '${bottom - top - strokeWidth}px'
..border = '${_borderStrokeToCssUnit(strokeWidth)} solid $cssColor';
style.border = '${_borderStrokeToCssUnit(strokeWidth)} solid $cssColor';
} else {
style
..width = '${right - left}px'
..height = '${bottom - top}px'
..backgroundColor = cssColor
..backgroundImage = _getBackgroundImageCssValue(paint.shader, rect);
}
Expand Down
77 changes: 77 additions & 0 deletions lib/web_ui/test/html/dom_canvas_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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';

void main() {
internalBootstrapBrowserTest(() => testMain);
}

Future<void> testMain() async {
group('$adjustRectForDom', () {

test('does not change rect when not necessary', () async {
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
rect,
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=0),
rect,
);
});

test('takes stroke width into consideration', () async {
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=1),
const Rect.fromLTWH(9.5, 19.5, 139, 159),
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=10),
const Rect.fromLTWH(5, 15, 130, 150),
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=15),
const Rect.fromLTWH(2.5, 12.5, 125, 145),
);
});

test('flips rect when necessary', () {
Rect rect = const Rect.fromLTWH(100, 200, -40, -60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
const Rect.fromLTWH(60, 140, 40, 60),
);

rect = const Rect.fromLTWH(100, 200, 40, -60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
const Rect.fromLTWH(100, 140, 40, 60),
);

rect = const Rect.fromLTWH(100, 200, -40, 60);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
const Rect.fromLTWH(60, 200, 40, 60),
);
});

test('handles stroke width greater than width or height', () {
const Rect rect = Rect.fromLTWH(100, 200, 20, 70);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=50),
const Rect.fromLTWH(75, 175, 0, 20),
);
expect(
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=80),
const Rect.fromLTWH(60, 160, 0, 0),
);
});
});
}
Loading