Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit cd2937e

Browse files
authored
[web] Fix some clip and stroke calculations (#36801)
1 parent ffa98ce commit cd2937e

File tree

4 files changed

+311
-53
lines changed

4 files changed

+311
-53
lines changed

lib/web_ui/lib/src/engine/html/bitmap_canvas.dart

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -440,13 +440,10 @@ class BitmapCanvas extends EngineCanvas {
440440
@override
441441
void drawRect(ui.Rect rect, SurfacePaintData paint) {
442442
if (_useDomForRenderingFillAndStroke(paint)) {
443+
rect = adjustRectForDom(rect, paint);
443444
final DomHTMLElement element = buildDrawRectElement(
444445
rect, paint, 'draw-rect', _canvasPool.currentTransform);
445-
_drawElement(
446-
element,
447-
ui.Offset(
448-
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
449-
paint);
446+
_drawElement(element, rect.topLeft, paint);
450447
} else {
451448
setUpPaint(paint, rect);
452449
_canvasPool.drawRect(rect, paint.style);
@@ -482,16 +479,12 @@ class BitmapCanvas extends EngineCanvas {
482479

483480
@override
484481
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
485-
final ui.Rect rect = rrect.outerRect;
486482
if (_useDomForRenderingFillAndStroke(paint)) {
483+
final ui.Rect rect = adjustRectForDom(rrect.outerRect, paint);
487484
final DomHTMLElement element = buildDrawRectElement(
488485
rect, paint, 'draw-rrect', _canvasPool.currentTransform);
489486
applyRRectBorderRadius(element.style, rrect);
490-
_drawElement(
491-
element,
492-
ui.Offset(
493-
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
494-
paint);
487+
_drawElement(element, rect.topLeft, paint);
495488
} else {
496489
setUpPaint(paint, rrect.outerRect);
497490
_canvasPool.drawRRect(rrect, paint.style);
@@ -509,13 +502,10 @@ class BitmapCanvas extends EngineCanvas {
509502
@override
510503
void drawOval(ui.Rect rect, SurfacePaintData paint) {
511504
if (_useDomForRenderingFill(paint)) {
505+
rect = adjustRectForDom(rect, paint);
512506
final DomHTMLElement element = buildDrawRectElement(
513507
rect, paint, 'draw-oval', _canvasPool.currentTransform);
514-
_drawElement(
515-
element,
516-
ui.Offset(
517-
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
518-
paint);
508+
_drawElement(element, rect.topLeft, paint);
519509
element.style.borderRadius =
520510
'${rect.width / 2.0}px / ${rect.height / 2.0}px';
521511
} else {
@@ -527,15 +517,11 @@ class BitmapCanvas extends EngineCanvas {
527517

528518
@override
529519
void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) {
530-
final ui.Rect rect = ui.Rect.fromCircle(center: c, radius: radius);
531520
if (_useDomForRenderingFillAndStroke(paint)) {
521+
final ui.Rect rect = adjustRectForDom(ui.Rect.fromCircle(center: c, radius: radius), paint);
532522
final DomHTMLElement element = buildDrawRectElement(
533523
rect, paint, 'draw-circle', _canvasPool.currentTransform);
534-
_drawElement(
535-
element,
536-
ui.Offset(
537-
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
538-
paint);
524+
_drawElement(element, rect.topLeft, paint);
539525
element.style.borderRadius = '50%';
540526
} else {
541527
setUpPaint(
@@ -555,21 +541,19 @@ class BitmapCanvas extends EngineCanvas {
555541
final SurfacePath surfacePath = path as SurfacePath;
556542
final ui.Rect? pathAsLine = surfacePath.toStraightLine();
557543
if (pathAsLine != null) {
558-
final ui.Rect rect = (pathAsLine.top == pathAsLine.bottom)
544+
ui.Rect rect = (pathAsLine.top == pathAsLine.bottom)
559545
? ui.Rect.fromLTWH(
560546
pathAsLine.left, pathAsLine.top, pathAsLine.width, 1)
561547
: ui.Rect.fromLTWH(
562548
pathAsLine.left, pathAsLine.top, 1, pathAsLine.height);
563549

550+
rect = adjustRectForDom(rect, paint);
564551
final DomHTMLElement element = buildDrawRectElement(
565552
rect, paint, 'draw-rect', _canvasPool.currentTransform);
566-
_drawElement(
567-
element,
568-
ui.Offset(math.min(rect.left, rect.right),
569-
math.min(rect.top, rect.bottom)),
570-
paint);
553+
_drawElement(element, rect.topLeft, paint);
571554
return;
572555
}
556+
573557
final ui.Rect? pathAsRect = surfacePath.toRect();
574558
if (pathAsRect != null) {
575559
drawRect(pathAsRect, paint);

lib/web_ui/lib/src/engine/html/dom_canvas.dart

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,16 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
7575

7676
@override
7777
void drawRect(ui.Rect rect, SurfacePaintData paint) {
78+
rect = adjustRectForDom(rect, paint);
7879
currentElement.append(
7980
buildDrawRectElement(rect, paint, 'draw-rect', currentTransform));
8081
}
8182

8283
@override
8384
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
85+
final ui.Rect outerRect = adjustRectForDom(rrect.outerRect, paint);
8486
final DomElement element = buildDrawRectElement(
85-
rrect.outerRect, paint, 'draw-rrect', currentTransform);
87+
outerRect, paint, 'draw-rrect', currentTransform);
8688
applyRRectBorderRadius(element.style, rrect);
8789
currentElement.append(element);
8890
}
@@ -160,8 +162,77 @@ ui.Color blurColor(ui.Color color, double sigma) {
160162
return ui.Color((reducedAlpha & 0xff) << 24 | (color.value & 0x00ffffff));
161163
}
162164

165+
/// When drawing a shape (rect, rrect, circle, etc) in DOM/CSS, the [rect] given
166+
/// by Flutter needs to be adjusted to what DOM/CSS expect.
167+
///
168+
/// This method takes Flutter's [rect] and produces a new rect that can be used
169+
/// to generate the correct CSS properties to match Flutter's expectations.
170+
///
171+
///
172+
/// Here's what Flutter's given [rect] and [paint.strokeWidth] represent:
173+
///
174+
/// top-left ↓
175+
/// ┌──↓──────────────────────┐
176+
/// →→→→x x │←←
177+
/// │ ┌───────────────┐ │ |
178+
/// │ │ │ │ |
179+
/// │ │ │ │ | height
180+
/// │ │ │ │ |
181+
/// │ └───────────────┘ │ |
182+
/// │ x x │←←
183+
/// └─────────────────────────┘
184+
/// stroke-width ↑----↑ ↑
185+
/// ↑-------------------↑ width
186+
///
187+
///
188+
///
189+
/// In the DOM/CSS, here's how the coordinates should look like:
190+
///
191+
/// top-left ↓
192+
/// →→x─────────────────────────┐
193+
/// │ │
194+
/// │ x───────────────x │←←
195+
/// │ │ │ │ |
196+
/// │ │ │ │ | height
197+
/// │ │ │ │ |
198+
/// │ x───────────────x │←←
199+
/// │ │
200+
/// └─────────────────────────┘
201+
/// border-width ↑----↑ ↑
202+
/// ↑---------------↑ width
203+
///
204+
/// As shown in the drawing above, the width/height don't start at the top-left
205+
/// coordinates. Instead, they start from the inner top-left (inside the border).
206+
ui.Rect adjustRectForDom(ui.Rect rect, SurfacePaintData paint) {
207+
double left = math.min(rect.left, rect.right);
208+
double top = math.min(rect.top, rect.bottom);
209+
double width = rect.width.abs();
210+
double height = rect.height.abs();
211+
212+
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
213+
final double strokeWidth = paint.strokeWidth ?? 0.0;
214+
if (isStroke && strokeWidth > 0.0) {
215+
left -= strokeWidth / 2.0;
216+
top -= strokeWidth / 2.0;
217+
218+
// width and height shouldn't go below zero.
219+
width = math.max(0, width - strokeWidth);
220+
height = math.max(0, height - strokeWidth);
221+
}
222+
223+
if (left != rect.left ||
224+
top != rect.top ||
225+
width != rect.width ||
226+
height != rect.height) {
227+
return ui.Rect.fromLTWH(left, top, width, height);
228+
}
229+
return rect;
230+
}
231+
163232
DomHTMLElement buildDrawRectElement(
164233
ui.Rect rect, SurfacePaintData paint, String tagName, Matrix4 transform) {
234+
assert(rect.left <= rect.right);
235+
assert(rect.top <= rect.bottom);
165236
final DomHTMLElement rectangle = domDocument.createElement(tagName) as
166237
DomHTMLElement;
167238
assert(() {
@@ -172,26 +243,11 @@ DomHTMLElement buildDrawRectElement(
172243
String effectiveTransform;
173244
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
174245
final double strokeWidth = paint.strokeWidth ?? 0.0;
175-
final double left = math.min(rect.left, rect.right);
176-
final double right = math.max(rect.left, rect.right);
177-
final double top = math.min(rect.top, rect.bottom);
178-
final double bottom = math.max(rect.top, rect.bottom);
179246
if (transform.isIdentity()) {
180-
if (isStroke) {
181-
effectiveTransform =
182-
'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)';
183-
} else {
184-
effectiveTransform = 'translate(${left}px, ${top}px)';
185-
}
247+
effectiveTransform = 'translate(${rect.left}px, ${rect.top}px)';
186248
} else {
187-
// Clone to avoid mutating _transform.
188-
final Matrix4 translated = transform.clone();
189-
if (isStroke) {
190-
translated.translate(
191-
left - (strokeWidth / 2.0), top - (strokeWidth / 2.0));
192-
} else {
193-
translated.translate(left, top);
194-
}
249+
// Clone to avoid mutating `transform`.
250+
final Matrix4 translated = transform.clone()..translate(rect.left, rect.top);
195251
effectiveTransform = matrix4ToCssTransform(translated);
196252
}
197253
final DomCSSStyleDeclaration style = rectangle.style;
@@ -216,15 +272,14 @@ DomHTMLElement buildDrawRectElement(
216272
}
217273
}
218274

275+
style
276+
..width = '${rect.width}px'
277+
..height = '${rect.height}px';
278+
219279
if (isStroke) {
220-
style
221-
..width = '${right - left - strokeWidth}px'
222-
..height = '${bottom - top - strokeWidth}px'
223-
..border = '${_borderStrokeToCssUnit(strokeWidth)} solid $cssColor';
280+
style.border = '${_borderStrokeToCssUnit(strokeWidth)} solid $cssColor';
224281
} else {
225282
style
226-
..width = '${right - left}px'
227-
..height = '${bottom - top}px'
228283
..backgroundColor = cssColor
229284
..backgroundImage = _getBackgroundImageCssValue(paint.shader, rect);
230285
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:test/bootstrap/browser.dart';
6+
import 'package:test/test.dart';
7+
import 'package:ui/src/engine.dart';
8+
import 'package:ui/ui.dart';
9+
10+
void main() {
11+
internalBootstrapBrowserTest(() => testMain);
12+
}
13+
14+
Future<void> testMain() async {
15+
group('$adjustRectForDom', () {
16+
17+
test('does not change rect when not necessary', () async {
18+
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
19+
expect(
20+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
21+
rect,
22+
);
23+
expect(
24+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=0),
25+
rect,
26+
);
27+
});
28+
29+
test('takes stroke width into consideration', () async {
30+
const Rect rect = Rect.fromLTWH(10, 20, 140, 160);
31+
expect(
32+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=1),
33+
const Rect.fromLTWH(9.5, 19.5, 139, 159),
34+
);
35+
expect(
36+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=10),
37+
const Rect.fromLTWH(5, 15, 130, 150),
38+
);
39+
expect(
40+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=15),
41+
const Rect.fromLTWH(2.5, 12.5, 125, 145),
42+
);
43+
});
44+
45+
test('flips rect when necessary', () {
46+
Rect rect = const Rect.fromLTWH(100, 200, -40, -60);
47+
expect(
48+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
49+
const Rect.fromLTWH(60, 140, 40, 60),
50+
);
51+
52+
rect = const Rect.fromLTWH(100, 200, 40, -60);
53+
expect(
54+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
55+
const Rect.fromLTWH(100, 140, 40, 60),
56+
);
57+
58+
rect = const Rect.fromLTWH(100, 200, -40, 60);
59+
expect(
60+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.fill),
61+
const Rect.fromLTWH(60, 200, 40, 60),
62+
);
63+
});
64+
65+
test('handles stroke width greater than width or height', () {
66+
const Rect rect = Rect.fromLTWH(100, 200, 20, 70);
67+
expect(
68+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=50),
69+
const Rect.fromLTWH(75, 175, 0, 20),
70+
);
71+
expect(
72+
adjustRectForDom(rect, SurfacePaintData()..style=PaintingStyle.stroke..strokeWidth=80),
73+
const Rect.fromLTWH(60, 160, 0, 0),
74+
);
75+
});
76+
});
77+
}

0 commit comments

Comments
 (0)