diff --git a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
index b2b93abbb6700..a051183395568 100644
--- a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
+++ b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
@@ -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);
@@ -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);
@@ -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 {
@@ -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(
@@ -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);
diff --git a/lib/web_ui/lib/src/engine/html/dom_canvas.dart b/lib/web_ui/lib/src/engine/html/dom_canvas.dart
index 41956fd11f064..c7caeea6cde89 100644
--- a/lib/web_ui/lib/src/engine/html/dom_canvas.dart
+++ b/lib/web_ui/lib/src/engine/html/dom_canvas.dart
@@ -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);
}
@@ -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;
+ 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(() {
@@ -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;
@@ -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);
}
diff --git a/lib/web_ui/test/html/dom_canvas_test.dart b/lib/web_ui/test/html/dom_canvas_test.dart
new file mode 100644
index 0000000000000..a81c36031dcdf
--- /dev/null
+++ b/lib/web_ui/test/html/dom_canvas_test.dart
@@ -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 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),
+ );
+ });
+ });
+}
diff --git a/lib/web_ui/test/html/drawing/dom_clip_stroke_golden_test.dart b/lib/web_ui/test/html/drawing/dom_clip_stroke_golden_test.dart
new file mode 100644
index 0000000000000..648353e924749
--- /dev/null
+++ b/lib/web_ui/test/html/drawing/dom_clip_stroke_golden_test.dart
@@ -0,0 +1,142 @@
+// 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';
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+Future testMain() async {
+ test('rect stroke with clip', () async {
+ const Rect region = Rect.fromLTWH(0, 0, 250, 250);
+ // Set `hasParagraphs` to true to force DOM rendering.
+ final BitmapCanvas canvas =
+ BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
+
+ const Rect rect = Rect.fromLTWH(0, 0, 150, 150);
+
+ canvas.clipRect(rect.inflate(10.0), ClipOp.intersect);
+
+ canvas.drawRect(
+ rect,
+ SurfacePaintData()
+ ..color = const Color(0x6fff0000)
+ ..strokeWidth = 20.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ canvas.drawRect(
+ rect,
+ SurfacePaintData()
+ ..color = const Color(0x6f0000ff)
+ ..strokeWidth = 10.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ canvas.drawRect(
+ rect,
+ SurfacePaintData()
+ ..color = const Color(0xff000000)
+ ..strokeWidth = 1.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ domDocument.body!.style.margin = '0px';
+ domDocument.body!.append(canvas.rootElement);
+ await matchGoldenFile('rect_clip_strokes_dom.png', region: region);
+ canvas.rootElement.remove();
+ });
+
+ test('rrect stroke with clip', () async {
+ const Rect region = Rect.fromLTWH(0, 0, 250, 250);
+ // Set `hasParagraphs` to true to force DOM rendering.
+ final BitmapCanvas canvas =
+ BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
+
+ final RRect rrect = RRect.fromRectAndRadius(
+ const Rect.fromLTWH(0, 0, 150, 150),
+ const Radius.circular(20),
+ );
+
+ canvas.clipRect(rrect.outerRect.inflate(10.0), ClipOp.intersect);
+
+ canvas.drawRRect(
+ rrect,
+ SurfacePaintData()
+ ..color = const Color(0x6fff0000)
+ ..strokeWidth = 20.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ canvas.drawRRect(
+ rrect,
+ SurfacePaintData()
+ ..color = const Color(0x6f0000ff)
+ ..strokeWidth = 10.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ canvas.drawRRect(
+ rrect,
+ SurfacePaintData()
+ ..color = const Color(0xff000000)
+ ..strokeWidth = 1.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ domDocument.body!.style.margin = '0px';
+ domDocument.body!.append(canvas.rootElement);
+ await matchGoldenFile('rrect_clip_strokes_dom.png', region: region);
+ canvas.rootElement.remove();
+ });
+
+ test('circle stroke with clip', () async {
+ const Rect region = Rect.fromLTWH(0, 0, 250, 250);
+ // Set `hasParagraphs` to true to force DOM rendering.
+ final BitmapCanvas canvas =
+ BitmapCanvas(region, RenderStrategy()..hasParagraphs = true);
+
+ const Rect rect = Rect.fromLTWH(0, 0, 150, 150);
+
+ canvas.clipRect(rect.inflate(10.0), ClipOp.intersect);
+
+ canvas.drawCircle(
+ rect.center,
+ rect.width / 2,
+ SurfacePaintData()
+ ..color = const Color(0x6fff0000)
+ ..strokeWidth = 20.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ canvas.drawCircle(
+ rect.center,
+ rect.width / 2,
+ SurfacePaintData()
+ ..color = const Color(0x6f0000ff)
+ ..strokeWidth = 10.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ canvas.drawCircle(
+ rect.center,
+ rect.width / 2,
+ SurfacePaintData()
+ ..color = const Color(0xff000000)
+ ..strokeWidth = 1.0
+ ..style = PaintingStyle.stroke,
+ );
+
+ domDocument.body!.style.margin = '0px';
+ domDocument.body!.append(canvas.rootElement);
+ await matchGoldenFile('circle_clip_strokes_dom.png', region: region);
+ canvas.rootElement.remove();
+ });
+}