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

Commit b5daef7

Browse files
authored
Ensure sorted rects in ui.Canvas for legacy compatibility (#49309)
Fixes flutter/flutter#140490
1 parent ebae211 commit b5daef7

File tree

2 files changed

+184
-74
lines changed

2 files changed

+184
-74
lines changed

lib/ui/painting.dart

Lines changed: 24 additions & 2 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,10 @@ 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);
5860+
// Even if rect is still empty - which implies it has a zero dimension -
5861+
// we still need to perform the clipRect operation as it will effectively
5862+
// nullify any further rendering until the next restore call.
58475863
_clipRect(rect.left, rect.top, rect.right, rect.bottom, clipOp.index, doAntiAlias);
58485864
}
58495865

@@ -5916,7 +5932,10 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas {
59165932
@override
59175933
void drawRect(Rect rect, Paint paint) {
59185934
assert(_rectIsValid(rect));
5919-
_drawRect(rect.left, rect.top, rect.right, rect.bottom, paint._objects, paint._data);
5935+
rect = _sorted(rect);
5936+
if (paint.style != PaintingStyle.fill || !rect.isEmpty) {
5937+
_drawRect(rect.left, rect.top, rect.right, rect.bottom, paint._objects, paint._data);
5938+
}
59205939
}
59215940

59225941
@Native<Void Function(Pointer<Void>, Double, Double, Double, Double, Handle, Handle)>(symbol: 'Canvas::drawRect')
@@ -5944,7 +5963,10 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas {
59445963
@override
59455964
void drawOval(Rect rect, Paint paint) {
59465965
assert(_rectIsValid(rect));
5947-
_drawOval(rect.left, rect.top, rect.right, rect.bottom, paint._objects, paint._data);
5966+
rect = _sorted(rect);
5967+
if (paint.style != PaintingStyle.fill || !rect.isEmpty) {
5968+
_drawOval(rect.left, rect.top, rect.right, rect.bottom, paint._objects, paint._data);
5969+
}
59485970
}
59495971

59505972
@Native<Void Function(Pointer<Void>, Double, Double, Double, Double, Handle, Handle)>(symbol: 'Canvas::drawOval')

testing/dart/canvas_test.dart

Lines changed: 160 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,95 @@ 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+
final Rect tallThin = Rect.fromLTRB(
572+
min(rect.left, rect.right) - 10,
573+
rect.top,
574+
min(rect.left, rect.right) - 10,
575+
rect.bottom,
576+
);
577+
final Rect wideThin = Rect.fromLTRB(
578+
rect.left,
579+
min(rect.top, rect.bottom) - 10,
580+
rect.right,
581+
min(rect.top, rect.bottom) - 10,
582+
);
583+
584+
canvas.save();
585+
canvas.translate(x, y);
586+
587+
paint.style = PaintingStyle.fill;
588+
canvas.drawRect(rect, paint);
589+
canvas.drawRect(tallThin, paint);
590+
canvas.drawRect(wideThin, paint);
591+
592+
canvas.save();
593+
canvas.translate(0, 100);
594+
paint.style = PaintingStyle.stroke;
595+
canvas.drawRect(rect, paint);
596+
canvas.drawRect(tallThin, paint);
597+
canvas.drawRect(wideThin, paint);
598+
canvas.restore();
599+
600+
canvas.save();
601+
canvas.translate(100, 0);
602+
paint.style = PaintingStyle.fill;
603+
canvas.drawOval(rect, paint);
604+
canvas.drawOval(tallThin, paint);
605+
canvas.drawOval(wideThin, paint);
606+
canvas.restore();
607+
608+
canvas.save();
609+
canvas.translate(100, 100);
610+
paint.style = PaintingStyle.stroke;
611+
canvas.drawOval(rect, paint);
612+
canvas.drawOval(tallThin, paint);
613+
canvas.drawOval(wideThin, paint);
614+
canvas.restore();
615+
616+
canvas.save();
617+
canvas.translate(50, 50);
618+
619+
canvas.save();
620+
canvas.clipRect(rect);
621+
canvas.drawPaint(paint);
622+
canvas.restore();
623+
624+
canvas.save();
625+
canvas.clipRect(tallThin);
626+
canvas.drawPaint(paint);
627+
canvas.restore();
628+
629+
canvas.save();
630+
canvas.clipRect(wideThin);
631+
canvas.drawPaint(paint);
632+
canvas.restore();
633+
634+
canvas.restore();
635+
636+
canvas.restore();
637+
}
638+
639+
draw(const Rect.fromLTRB(10, 10, 40, 40), 50, 50, const Color(0xFF2196F3));
640+
draw(const Rect.fromLTRB(40, 10, 10, 40), 250, 50, const Color(0xFF4CAF50));
641+
draw(const Rect.fromLTRB(10, 40, 40, 10), 50, 250, const Color(0xFF9C27B0));
642+
draw(const Rect.fromLTRB(40, 40, 10, 10), 250, 250, const Color(0xFFFF9800));
643+
644+
final Picture picture = recorder.endRecording();
645+
final Image image = await picture.toImage(450, 450);
646+
await comparer.addGoldenImage(image, 'render_unordered_rects.png');
647+
});
648+
560649
Matcher closeToTransform(Float64List expected) => (dynamic v) {
561650
Expect.type<Float64List>(v);
562651
final Float64List value = v as Float64List;
@@ -676,83 +765,82 @@ void main() async {
676765
Expect.fail('$value is too close to $expected');
677766
};
678767

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);
768+
test('Canvas.clipRect affects canvas.getClipBounds', () async {
769+
void testRect(Rect clipRect, bool doAA) {
770+
final PictureRecorder recorder = PictureRecorder();
771+
final Canvas canvas = Canvas(recorder);
772+
canvas.clipRect(clipRect, doAntiAlias: doAA);
773+
774+
final Rect clipSortedBounds = Rect.fromLTRB(
775+
min(clipRect.left, clipRect.right),
776+
min(clipRect.top, clipRect.bottom),
777+
max(clipRect.left, clipRect.right),
778+
max(clipRect.top, clipRect.bottom),
779+
);
780+
Rect clipExpandedBounds;
781+
if (doAA) {
782+
clipExpandedBounds = Rect.fromLTRB(
783+
clipSortedBounds.left.floorToDouble(),
784+
clipSortedBounds.top.floorToDouble(),
785+
clipSortedBounds.right.ceilToDouble(),
786+
clipSortedBounds.bottom.ceilToDouble(),
787+
);
788+
} else {
789+
clipExpandedBounds = clipSortedBounds;
790+
}
724791

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));
792+
// Save initial return values for testing restored values
793+
final Rect initialLocalBounds = canvas.getLocalClipBounds();
794+
final Rect initialDestinationBounds = canvas.getDestinationClipBounds();
795+
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
796+
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
797+
798+
canvas.save();
799+
canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15));
800+
// Both clip bounds have changed
801+
expect(canvas.getLocalClipBounds(), notCloseToRect(clipExpandedBounds));
802+
expect(canvas.getDestinationClipBounds(), notCloseToRect(clipExpandedBounds));
803+
// Previous return values have not changed
804+
expect(initialLocalBounds, closeToRect(clipExpandedBounds));
805+
expect(initialDestinationBounds, closeToRect(clipExpandedBounds));
806+
canvas.restore();
807+
808+
// save/restore returned the values to their original values
809+
expect(canvas.getLocalClipBounds(), initialLocalBounds);
810+
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
811+
812+
canvas.save();
813+
canvas.scale(2, 2);
814+
final Rect scaledExpandedBounds = Rect.fromLTRB(
815+
clipExpandedBounds.left / 2.0,
816+
clipExpandedBounds.top / 2.0,
817+
clipExpandedBounds.right / 2.0,
818+
clipExpandedBounds.bottom / 2.0,
819+
);
820+
expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds));
821+
// Destination bounds are unaffected by transform
822+
expect(canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds));
823+
canvas.restore();
824+
825+
// save/restore returned the values to their original values
826+
expect(canvas.getLocalClipBounds(), initialLocalBounds);
827+
expect(canvas.getDestinationClipBounds(), initialDestinationBounds);
828+
}
730829

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();
830+
testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), false);
831+
testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), true);
740832

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

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();
837+
// TB swapped
838+
testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), false);
839+
testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), true);
752840

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

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

0 commit comments

Comments
 (0)