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

Commit 187322c

Browse files
committed
Ensure sorted rects in ui.Canvas for legacy compatibility
1 parent 00d7d23 commit 187322c

File tree

2 files changed

+141
-72
lines changed

2 files changed

+141
-72
lines changed

lib/ui/painting.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5774,6 +5774,18 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas {
57745774
@Native<Void Function(Pointer<Void>)>(symbol: 'Canvas::save', isLeaf: true)
57755775
external void save();
57765776

5777+
static Rect _sorted(Rect rect) {
5778+
if (rect.isEmpty) {
5779+
rect = Rect.fromLTRB(
5780+
math.min(rect.left, rect.right),
5781+
math.min(rect.top, rect.bottom),
5782+
math.max(rect.left, rect.right),
5783+
math.max(rect.top, rect.bottom),
5784+
);
5785+
}
5786+
return rect;
5787+
}
5788+
57775789
@override
57785790
void saveLayer(Rect? bounds, Paint paint) {
57795791
if (bounds == null) {
@@ -5844,6 +5856,7 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas {
58445856
@override
58455857
void clipRect(Rect rect, { ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true }) {
58465858
assert(_rectIsValid(rect));
5859+
rect = _sorted(rect);
58475860
_clipRect(rect.left, rect.top, rect.right, rect.bottom, clipOp.index, doAntiAlias);
58485861
}
58495862

@@ -5916,6 +5929,7 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas {
59165929
@override
59175930
void drawRect(Rect rect, Paint paint) {
59185931
assert(_rectIsValid(rect));
5932+
rect = _sorted(rect);
59195933
_drawRect(rect.left, rect.top, rect.right, rect.bottom, paint._objects, paint._data);
59205934
}
59215935

@@ -5944,6 +5958,7 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas {
59445958
@override
59455959
void drawOval(Rect rect, Paint paint) {
59465960
assert(_rectIsValid(rect));
5961+
rect = _sorted(rect);
59475962
_drawOval(rect.left, rect.top, rect.right, rect.bottom, paint._objects, paint._data);
59485963
}
59495964

testing/dart/canvas_test.dart

Lines changed: 126 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,61 @@ void main() async {
557557
expect(tabToTofuComparison, isFalse);
558558
});
559559

560+
test('drawRect, drawOval, and clipRect render with unsorted rectangles', () async {
561+
final PictureRecorder recorder = PictureRecorder();
562+
final Canvas canvas = Canvas(recorder);
563+
564+
canvas.drawColor(const Color(0xFFE0E0E0), BlendMode.src);
565+
566+
void draw(Rect rect, double x, double y, Color color) {
567+
final Paint paint = Paint()
568+
..color = color
569+
..strokeWidth = 5.0;
570+
571+
canvas.save();
572+
canvas.translate(x, y);
573+
574+
paint.style = PaintingStyle.fill;
575+
canvas.drawRect(rect, paint);
576+
577+
canvas.save();
578+
canvas.translate(0, 100);
579+
paint.style = PaintingStyle.stroke;
580+
canvas.drawRect(rect, paint);
581+
canvas.restore();
582+
583+
584+
canvas.save();
585+
canvas.translate(100, 0);
586+
paint.style = PaintingStyle.fill;
587+
canvas.drawOval(rect, paint);
588+
canvas.restore();
589+
590+
canvas.save();
591+
canvas.translate(100, 100);
592+
paint.style = PaintingStyle.stroke;
593+
canvas.drawOval(rect, paint);
594+
canvas.restore();
595+
596+
canvas.save();
597+
canvas.translate(50, 50);
598+
canvas.clipRect(rect);
599+
canvas.drawPaint(paint);
600+
canvas.restore();
601+
602+
canvas.restore();
603+
}
604+
605+
draw(const Rect.fromLTRB(10, 10, 40, 40), 50, 50, const Color(0xFF2196F3));
606+
draw(const Rect.fromLTRB(40, 10, 10, 40), 250, 50, const Color(0xFF4CAF50));
607+
draw(const Rect.fromLTRB(10, 40, 40, 10), 50, 250, const Color(0xFF9C27B0));
608+
draw(const Rect.fromLTRB(40, 40, 10, 10), 250, 250, const Color(0xFFFF9800));
609+
610+
final Picture picture = recorder.endRecording();
611+
final Image image = await picture.toImage(450, 450);
612+
await comparer.addGoldenImage(image, 'render_unordered_rects.png');
613+
});
614+
560615
Matcher closeToTransform(Float64List expected) => (dynamic v) {
561616
Expect.type<Float64List>(v);
562617
final Float64List value = v as Float64List;
@@ -676,83 +731,82 @@ void main() async {
676731
Expect.fail('$value is too close to $expected');
677732
};
678733

679-
test('Canvas.clipRect(doAA=true) affects canvas.getClipBounds', () async {
680-
final PictureRecorder recorder = PictureRecorder();
681-
final Canvas canvas = Canvas(recorder);
682-
const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
683-
const Rect clipExpandedBounds = Rect.fromLTRB(10, 11, 21, 26);
684-
canvas.clipRect(clipBounds);
685-
686-
// Save initial return values for testing restored values
687-
final Rect initialLocalBounds = canvas.getLocalClipBounds();
688-
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
689-
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
690-
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
691-
692-
canvas.save();
693-
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15));
694-
// Both clip bounds have changed
695-
expect(canvas.getLocalClipBounds(), notCloseToRect(clipExpandedBounds));
696-
expect(canvas.getDestinationClipBounds(), notCloseToRect(clipExpandedBounds));
697-
// Previous return values have not changed
698-
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
699-
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
700-
canvas.restore();
701-
702-
// save/restore returned the values to their original values
703-
expect(canvas.getLocalClipBounds(), initialLocalBounds);
704-
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
705-
706-
canvas.save();
707-
canvas.scale(2, 2);
708-
const Rect scaledExpandedBounds = Rect.fromLTRB(5, 5.5, 10.5, 13);
709-
expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds));
710-
// Destination bounds are unaffected by transform
711-
expect(canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds));
712-
canvas.restore();
713-
714-
// save/restore returned the values to their original values
715-
expect(canvas.getLocalClipBounds(), initialLocalBounds);
716-
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
717-
});
718-
719-
test('Canvas.clipRect(doAA=false) affects canvas.getClipBounds', () async {
720-
final PictureRecorder recorder = PictureRecorder();
721-
final Canvas canvas = Canvas(recorder);
722-
const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7);
723-
canvas.clipRect(clipBounds, doAntiAlias: false);
734+
test('Canvas.clipRect affects canvas.getClipBounds', () async {
735+
void testRect(Rect clipRect, bool doAA) {
736+
final PictureRecorder recorder = PictureRecorder();
737+
final Canvas canvas = Canvas(recorder);
738+
canvas.clipRect(clipRect, doAntiAlias: doAA);
739+
740+
final Rect clipSortedBounds = Rect.fromLTRB(
741+
min(clipRect.left, clipRect.right),
742+
min(clipRect.top, clipRect.bottom),
743+
max(clipRect.left, clipRect.right),
744+
max(clipRect.top, clipRect.bottom),
745+
);
746+
Rect clipExpandedBounds;
747+
if (doAA) {
748+
clipExpandedBounds = Rect.fromLTRB(
749+
clipSortedBounds.left.floorToDouble(),
750+
clipSortedBounds.top.floorToDouble(),
751+
clipSortedBounds.right.ceilToDouble(),
752+
clipSortedBounds.bottom.ceilToDouble(),
753+
);
754+
} else {
755+
clipExpandedBounds = clipSortedBounds;
756+
}
724757

725-
// Save initial return values for testing restored values
726-
final Rect initialLocalBounds = canvas.getLocalClipBounds();
727-
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
728-
expect(initialLocalBounds, closeToRect(clipBounds));
729-
expect(initialDestinationBounds, closeToRect(clipBounds));
758+
// Save initial return values for testing restored values
759+
final Rect initialLocalBounds = canvas.getLocalClipBounds();
760+
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
761+
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
762+
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
763+
764+
canvas.save();
765+
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15));
766+
// Both clip bounds have changed
767+
expect(canvas.getLocalClipBounds(), notCloseToRect(clipExpandedBounds));
768+
expect(canvas.getDestinationClipBounds(), notCloseToRect(clipExpandedBounds));
769+
// Previous return values have not changed
770+
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
771+
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
772+
canvas.restore();
773+
774+
// save/restore returned the values to their original values
775+
expect(canvas.getLocalClipBounds(), initialLocalBounds);
776+
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
777+
778+
canvas.save();
779+
canvas.scale(2, 2);
780+
final Rect scaledExpandedBounds = Rect.fromLTRB(
781+
clipExpandedBounds.left / 2.0,
782+
clipExpandedBounds.top / 2.0,
783+
clipExpandedBounds.right / 2.0,
784+
clipExpandedBounds.bottom / 2.0,
785+
);
786+
expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds));
787+
// Destination bounds are unaffected by transform
788+
expect(canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds));
789+
canvas.restore();
790+
791+
// save/restore returned the values to their original values
792+
expect(canvas.getLocalClipBounds(), initialLocalBounds);
793+
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
794+
}
730795

731-
canvas.save();
732-
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15));
733-
// Both clip bounds have changed
734-
expect(canvas.getLocalClipBounds(), notCloseToRect(clipBounds));
735-
expect(canvas.getDestinationClipBounds(), notCloseToRect(clipBounds));
736-
// Previous return values have not changed
737-
expect(initialLocalBounds, closeToRect(clipBounds));
738-
expect(initialDestinationBounds, closeToRect(clipBounds));
739-
canvas.restore();
796+
testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), false);
797+
testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), true);
740798

741-
// save/restore returned the values to their original values
742-
expect(canvas.getLocalClipBounds(), initialLocalBounds);
743-
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
799+
// LR swapped
800+
testRect(const Rect.fromLTRB(20.4, 11.3, 10.2, 25.7), false);
801+
testRect(const Rect.fromLTRB(20.4, 11.3, 10.2, 25.7), true);
744802

745-
canvas.save();
746-
canvas.scale(2, 2);
747-
const Rect scaledClipBounds = Rect.fromLTRB(5.1, 5.65, 10.2, 12.85);
748-
expect(canvas.getLocalClipBounds(), closeToRect(scaledClipBounds));
749-
// Destination bounds are unaffected by transform
750-
expect(canvas.getDestinationClipBounds(), closeToRect(clipBounds));
751-
canvas.restore();
803+
// TB swapped
804+
testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), false);
805+
testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), true);
752806

753-
// save/restore returned the values to their original values
754-
expect(canvas.getLocalClipBounds(), initialLocalBounds);
755-
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
807+
// LR and TB swapped
808+
testRect(const Rect.fromLTRB(20.4, 25.7, 10.2, 11.3), false);
809+
testRect(const Rect.fromLTRB(20.4, 25.7, 10.2, 11.3), true);
756810
});
757811

758812
test('Canvas.clipRect with matrix affects canvas.getClipBounds', () async {

0 commit comments

Comments
 (0)