From 5b7a3c8b2043f1a587452769735e72c6441136f8 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 21 Feb 2025 15:58:26 -0800 Subject: [PATCH] [Engine] Add RoundSuperellipse to drawing OP (#160883) This PR adds support for clipping round superellipse to the engine. For what a rounded superellipse is, see [this design doc](https://docs.google.com/document/d/1CJXULKJGQt22FOFsrlm2TKVjKBtif1yU4U50cMfL6Kc/edit?tab=t.0). Video demos can be found at https://github.com/flutter/engine/pull/56726 and https://github.com/flutter/flutter/pull/161409. Only impeller can actually render it. On Skia and Web, this shape falls back to `RRect`. Part of https://github.com/flutter/flutter/issues/139321 and https://github.com/flutter/flutter/issues/13914, also related to https://github.com/flutter/flutter/issues/91523. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/ci/licenses_golden/excluded_files | 1 + .../ci/licenses_golden/licenses_flutter | 8 + .../benchmarking/dl_complexity_gl.cc | 6 + .../benchmarking/dl_complexity_gl.h | 1 + .../benchmarking/dl_complexity_metal.cc | 6 + .../benchmarking/dl_complexity_metal.h | 1 + .../src/flutter/display_list/display_list.cc | 3 + .../src/flutter/display_list/display_list.h | 167 ++-- .../display_list/display_list_unittests.cc | 9 +- engine/src/flutter/display_list/dl_builder.cc | 57 ++ engine/src/flutter/display_list/dl_builder.h | 15 + engine/src/flutter/display_list/dl_canvas.h | 5 + engine/src/flutter/display_list/dl_op_flags.h | 4 + .../src/flutter/display_list/dl_op_receiver.h | 4 + .../src/flutter/display_list/dl_op_records.h | 5 + .../display_list/geometry/dl_geometry_types.h | 12 + .../geometry/dl_geometry_types_unittests.cc | 9 + .../flutter/display_list/skia/dl_sk_canvas.cc | 13 + .../flutter/display_list/skia/dl_sk_canvas.h | 5 + .../display_list/skia/dl_sk_dispatcher.cc | 11 + .../display_list/skia/dl_sk_dispatcher.h | 4 + .../display_list/testing/dl_test_snippets.cc | 39 + .../display_list/testing/dl_test_snippets.h | 2 + .../utils/dl_matrix_clip_tracker.cc | 84 +- .../utils/dl_matrix_clip_tracker.h | 4 + .../display_list/utils/dl_receiver_utils.h | 4 + engine/src/flutter/flow/BUILD.gn | 3 + .../flow/layers/clip_rsuperellipse_layer.cc | 23 + .../flow/layers/clip_rsuperellipse_layer.h | 28 + .../clip_rsuperellipse_layer_unittests.cc | 628 +++++++++++++ .../flutter/flow/layers/layer_state_stack.cc | 54 ++ .../flutter/flow/layers/layer_state_stack.h | 6 + .../flutter/impeller/display_list/canvas.cc | 23 + .../flutter/impeller/display_list/canvas.h | 2 + .../impeller/display_list/dl_dispatcher.cc | 27 + .../impeller/display_list/dl_dispatcher.h | 9 + .../impeller/entity/geometry/geometry.cc | 7 + .../impeller/entity/geometry/geometry.h | 3 + .../geometry/round_superellipse_geometry.h | 14 +- .../impeller/geometry/round_superellipse.cc | 8 + .../impeller/geometry/round_superellipse.h | 10 + engine/src/flutter/lib/ui/BUILD.gn | 2 + engine/src/flutter/lib/ui/compositing.dart | 55 ++ .../lib/ui/compositing/scene_builder.cc | 16 + .../lib/ui/compositing/scene_builder.h | 5 + engine/src/flutter/lib/ui/dart_ui.cc | 5 +- engine/src/flutter/lib/ui/geometry.dart | 887 ++++++++++++------ engine/src/flutter/lib/ui/painting.dart | 46 + engine/src/flutter/lib/ui/painting/canvas.cc | 20 + engine/src/flutter/lib/ui/painting/canvas.h | 6 + .../flutter/lib/ui/painting/rsuperellipse.cc | 59 ++ .../flutter/lib/ui/painting/rsuperellipse.h | 45 + engine/src/flutter/lib/web_ui/lib/canvas.dart | 2 + .../flutter/lib/web_ui/lib/compositing.dart | 7 + .../src/flutter/lib/web_ui/lib/geometry.dart | 806 ++++++++++------ .../web_ui/lib/src/engine/canvas_pool.dart | 19 + .../lib/src/engine/canvaskit/canvas.dart | 6 + .../engine/canvaskit/canvaskit_canvas.dart | 16 + .../lib/src/engine/canvaskit/layer.dart | 16 + .../engine/canvaskit/layer_scene_builder.dart | 9 + .../src/engine/canvaskit/layer_visitor.dart | 55 ++ .../web_ui/lib/src/engine/engine_canvas.dart | 13 + .../lib/src/engine/html/bitmap_canvas.dart | 7 + .../web_ui/lib/src/engine/html/canvas.dart | 16 + .../lib/web_ui/lib/src/engine/html/clip.dart | 65 ++ .../lib/src/engine/html/dom_canvas.dart | 5 + .../lib/src/engine/html/scene_builder.dart | 16 + .../lib/web_ui/lib/src/engine/layers.dart | 50 + .../web_ui/lib/src/engine/scene_builder.dart | 11 + .../src/engine/skwasm/skwasm_impl/canvas.dart | 14 + .../lib/web_ui/lib/src/engine/validators.dart | 8 + .../test/common/mock_engine_canvas.dart | 5 + .../test/engine/scene_builder_utils.dart | 6 + engine/src/flutter/shell/common/dl_op_spy.cc | 3 + engine/src/flutter/shell/common/dl_op_spy.h | 1 + .../flutter/testing/display_list_testing.cc | 16 +- .../flutter/testing/display_list_testing.h | 19 + 77 files changed, 3001 insertions(+), 660 deletions(-) create mode 100644 engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc create mode 100644 engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h create mode 100644 engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc create mode 100644 engine/src/flutter/lib/ui/painting/rsuperellipse.cc create mode 100644 engine/src/flutter/lib/ui/painting/rsuperellipse.h diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index 1ed250e2a76..5af997027e7 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -58,6 +58,7 @@ ../../../flutter/flow/layers/clip_path_layer_unittests.cc ../../../flutter/flow/layers/clip_rect_layer_unittests.cc ../../../flutter/flow/layers/clip_rrect_layer_unittests.cc +../../../flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc ../../../flutter/flow/layers/color_filter_layer_unittests.cc ../../../flutter/flow/layers/container_layer_unittests.cc ../../../flutter/flow/layers/display_list_layer_unittests.cc diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index b586f30a7e4..04303fdd4a3 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -41379,6 +41379,8 @@ ORIGIN: ../../../flutter/flow/layers/clip_rect_layer.cc + ../../../flutter/LICEN ORIGIN: ../../../flutter/flow/layers/clip_rect_layer.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/flow/layers/clip_rrect_layer.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/flow/layers/clip_rrect_layer.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/flow/layers/clip_rsuperellipse_layer.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/flow/layers/clip_rsuperellipse_layer.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/flow/layers/clip_shape_layer.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/flow/layers/color_filter_layer.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/flow/layers/color_filter_layer.h + ../../../flutter/LICENSE @@ -42490,6 +42492,8 @@ ORIGIN: ../../../flutter/lib/ui/painting/picture_recorder.cc + ../../../flutter/ ORIGIN: ../../../flutter/lib/ui/painting/picture_recorder.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/rrect.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/rrect.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/ui/painting/rsuperellipse.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/ui/painting/rsuperellipse.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/shader.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/shader.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/painting/single_frame_codec.cc + ../../../flutter/LICENSE @@ -44342,6 +44346,8 @@ FILE: ../../../flutter/flow/layers/clip_rect_layer.cc FILE: ../../../flutter/flow/layers/clip_rect_layer.h FILE: ../../../flutter/flow/layers/clip_rrect_layer.cc FILE: ../../../flutter/flow/layers/clip_rrect_layer.h +FILE: ../../../flutter/flow/layers/clip_rsuperellipse_layer.cc +FILE: ../../../flutter/flow/layers/clip_rsuperellipse_layer.h FILE: ../../../flutter/flow/layers/clip_shape_layer.h FILE: ../../../flutter/flow/layers/color_filter_layer.cc FILE: ../../../flutter/flow/layers/color_filter_layer.h @@ -45457,6 +45463,8 @@ FILE: ../../../flutter/lib/ui/painting/picture_recorder.cc FILE: ../../../flutter/lib/ui/painting/picture_recorder.h FILE: ../../../flutter/lib/ui/painting/rrect.cc FILE: ../../../flutter/lib/ui/painting/rrect.h +FILE: ../../../flutter/lib/ui/painting/rsuperellipse.cc +FILE: ../../../flutter/lib/ui/painting/rsuperellipse.h FILE: ../../../flutter/lib/ui/painting/shader.cc FILE: ../../../flutter/lib/ui/painting/shader.h FILE: ../../../flutter/lib/ui/painting/single_frame_codec.cc diff --git a/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.cc b/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.cc index a30993fe656..1729f5c74f0 100644 --- a/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.cc +++ b/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.cc @@ -335,6 +335,12 @@ void DisplayListGLComplexityCalculator::GLHelper::drawDiffRoundRect( AccumulateComplexity(complexity); } +void DisplayListGLComplexityCalculator::GLHelper::drawRoundSuperellipse( + const DlRoundSuperellipse& rse) { + // Drawing RSEs on Skia falls back to RRect. + drawRoundRect(rse.ToApproximateRoundRect()); +} + void DisplayListGLComplexityCalculator::GLHelper::drawPath(const DlPath& path) { if (IsComplex()) { return; diff --git a/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.h b/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.h index 7b9910f1b0b..516503e10d9 100644 --- a/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.h +++ b/engine/src/flutter/display_list/benchmarking/dl_complexity_gl.h @@ -51,6 +51,7 @@ class DisplayListGLComplexityCalculator void drawRoundRect(const DlRoundRect& rrect) override; void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override; + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override; void drawPath(const DlPath& path) override; void drawArc(const DlRect& oval_bounds, DlScalar start_degrees, diff --git a/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.cc b/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.cc index 713096731cd..75223c87bc9 100644 --- a/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.cc +++ b/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.cc @@ -329,6 +329,12 @@ void DisplayListMetalComplexityCalculator::MetalHelper::drawDiffRoundRect( AccumulateComplexity(complexity); } +void DisplayListMetalComplexityCalculator::MetalHelper::drawRoundSuperellipse( + const DlRoundSuperellipse& rse) { + // Drawing RSEs on Skia falls back to RRect. + drawRoundRect(rse.ToApproximateRoundRect()); +} + void DisplayListMetalComplexityCalculator::MetalHelper::drawPath( const DlPath& path) { if (IsComplex()) { diff --git a/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.h b/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.h index fc0e430dd0a..02c965b8cb8 100644 --- a/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.h +++ b/engine/src/flutter/display_list/benchmarking/dl_complexity_metal.h @@ -51,6 +51,7 @@ class DisplayListMetalComplexityCalculator void drawRoundRect(const DlRoundRect& rrect) override; void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override; + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override; void drawPath(const DlPath& path) override; void drawArc(const DlRect& oval_bounds, DlScalar start_degrees, diff --git a/engine/src/flutter/display_list/display_list.cc b/engine/src/flutter/display_list/display_list.cc index 91f7f29f151..3e5505b8f78 100644 --- a/engine/src/flutter/display_list/display_list.cc +++ b/engine/src/flutter/display_list/display_list.cc @@ -300,10 +300,12 @@ DisplayListOpCategory DisplayList::GetOpCategory(DisplayListOpType type) { case DisplayListOpType::kClipIntersectRect: case DisplayListOpType::kClipIntersectOval: case DisplayListOpType::kClipIntersectRoundRect: + case DisplayListOpType::kClipIntersectRoundSuperellipse: case DisplayListOpType::kClipIntersectPath: case DisplayListOpType::kClipDifferenceRect: case DisplayListOpType::kClipDifferenceOval: case DisplayListOpType::kClipDifferenceRoundRect: + case DisplayListOpType::kClipDifferenceRoundSuperellipse: case DisplayListOpType::kClipDifferencePath: return DisplayListOpCategory::kClip; @@ -316,6 +318,7 @@ DisplayListOpCategory DisplayList::GetOpCategory(DisplayListOpType type) { case DisplayListOpType::kDrawCircle: case DisplayListOpType::kDrawRoundRect: case DisplayListOpType::kDrawDiffRoundRect: + case DisplayListOpType::kDrawRoundSuperellipse: case DisplayListOpType::kDrawArc: case DisplayListOpType::kDrawPath: case DisplayListOpType::kDrawPoints: diff --git a/engine/src/flutter/display_list/display_list.h b/engine/src/flutter/display_list/display_list.h index 1e2f35ca84f..afe569a484b 100644 --- a/engine/src/flutter/display_list/display_list.h +++ b/engine/src/flutter/display_list/display_list.h @@ -52,88 +52,91 @@ namespace flutter { -#define FOR_EACH_DISPLAY_LIST_OP(V) \ - V(SetAntiAlias) \ - V(SetInvertColors) \ - \ - V(SetStrokeCap) \ - V(SetStrokeJoin) \ - \ - V(SetStyle) \ - V(SetStrokeWidth) \ - V(SetStrokeMiter) \ - \ - V(SetColor) \ - V(SetBlendMode) \ - \ - V(ClearColorFilter) \ - V(SetPodColorFilter) \ - \ - V(ClearColorSource) \ - V(SetPodColorSource) \ - V(SetImageColorSource) \ - V(SetRuntimeEffectColorSource) \ - \ - V(ClearImageFilter) \ - V(SetPodImageFilter) \ - V(SetSharedImageFilter) \ - \ - V(ClearMaskFilter) \ - V(SetPodMaskFilter) \ - \ - V(Save) \ - V(SaveLayer) \ - V(SaveLayerBackdrop) \ - V(Restore) \ - \ - V(Translate) \ - V(Scale) \ - V(Rotate) \ - V(Skew) \ - V(Transform2DAffine) \ - V(TransformFullPerspective) \ - V(TransformReset) \ - \ - V(ClipIntersectRect) \ - V(ClipIntersectOval) \ - V(ClipIntersectRoundRect) \ - V(ClipIntersectPath) \ - V(ClipDifferenceRect) \ - V(ClipDifferenceOval) \ - V(ClipDifferenceRoundRect) \ - V(ClipDifferencePath) \ - \ - V(DrawPaint) \ - V(DrawColor) \ - \ - V(DrawLine) \ - V(DrawDashedLine) \ - V(DrawRect) \ - V(DrawOval) \ - V(DrawCircle) \ - V(DrawRoundRect) \ - V(DrawDiffRoundRect) \ - V(DrawArc) \ - V(DrawPath) \ - \ - V(DrawPoints) \ - V(DrawLines) \ - V(DrawPolygon) \ - V(DrawVertices) \ - \ - V(DrawImage) \ - V(DrawImageWithAttr) \ - V(DrawImageRect) \ - V(DrawImageNine) \ - V(DrawImageNineWithAttr) \ - V(DrawAtlas) \ - V(DrawAtlasCulled) \ - \ - V(DrawDisplayList) \ - V(DrawTextBlob) \ - V(DrawTextFrame) \ - \ - V(DrawShadow) \ +#define FOR_EACH_DISPLAY_LIST_OP(V) \ + V(SetAntiAlias) \ + V(SetInvertColors) \ + \ + V(SetStrokeCap) \ + V(SetStrokeJoin) \ + \ + V(SetStyle) \ + V(SetStrokeWidth) \ + V(SetStrokeMiter) \ + \ + V(SetColor) \ + V(SetBlendMode) \ + \ + V(ClearColorFilter) \ + V(SetPodColorFilter) \ + \ + V(ClearColorSource) \ + V(SetPodColorSource) \ + V(SetImageColorSource) \ + V(SetRuntimeEffectColorSource) \ + \ + V(ClearImageFilter) \ + V(SetPodImageFilter) \ + V(SetSharedImageFilter) \ + \ + V(ClearMaskFilter) \ + V(SetPodMaskFilter) \ + \ + V(Save) \ + V(SaveLayer) \ + V(SaveLayerBackdrop) \ + V(Restore) \ + \ + V(Translate) \ + V(Scale) \ + V(Rotate) \ + V(Skew) \ + V(Transform2DAffine) \ + V(TransformFullPerspective) \ + V(TransformReset) \ + \ + V(ClipIntersectRect) \ + V(ClipIntersectOval) \ + V(ClipIntersectRoundRect) \ + V(ClipIntersectRoundSuperellipse) \ + V(ClipIntersectPath) \ + V(ClipDifferenceRect) \ + V(ClipDifferenceOval) \ + V(ClipDifferenceRoundRect) \ + V(ClipDifferenceRoundSuperellipse) \ + V(ClipDifferencePath) \ + \ + V(DrawPaint) \ + V(DrawColor) \ + \ + V(DrawLine) \ + V(DrawDashedLine) \ + V(DrawRect) \ + V(DrawOval) \ + V(DrawCircle) \ + V(DrawRoundRect) \ + V(DrawDiffRoundRect) \ + V(DrawRoundSuperellipse) \ + V(DrawArc) \ + V(DrawPath) \ + \ + V(DrawPoints) \ + V(DrawLines) \ + V(DrawPolygon) \ + V(DrawVertices) \ + \ + V(DrawImage) \ + V(DrawImageWithAttr) \ + V(DrawImageRect) \ + V(DrawImageNine) \ + V(DrawImageNineWithAttr) \ + V(DrawAtlas) \ + V(DrawAtlasCulled) \ + \ + V(DrawDisplayList) \ + V(DrawTextBlob) \ + V(DrawTextFrame) \ + \ + V(DrawShadow) \ V(DrawShadowTransparentOccluder) #define DL_OP_TO_ENUM_VALUE(name) k##name, diff --git a/engine/src/flutter/display_list/display_list_unittests.cc b/engine/src/flutter/display_list/display_list_unittests.cc index 63de9626590..2f68ceb2ce7 100644 --- a/engine/src/flutter/display_list/display_list_unittests.cc +++ b/engine/src/flutter/display_list/display_list_unittests.cc @@ -4512,7 +4512,7 @@ TEST_F(DisplayListTest, DrawDisplayListForwardsBackdropFlag) { #define CLIP_EXPECTOR(name) ClipExpector name(__FILE__, __LINE__) struct ClipExpectation { - std::variant shape; + std::variant shape; bool is_oval; DlClipOp clip_op; bool is_aa; @@ -4524,6 +4524,8 @@ struct ClipExpectation { case 1: return "DlRoundRect"; case 2: + return "DlRoundSuperellipse"; + case 3: return "DlPath"; default: return "Unknown"; @@ -4632,6 +4634,11 @@ class ClipExpector : public virtual DlOpReceiver, bool is_aa) override { check(rrect, clip_op, is_aa); } + void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) override { + check(rse, clip_op, is_aa); + } void clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) override { check(path, clip_op, is_aa); } diff --git a/engine/src/flutter/display_list/dl_builder.cc b/engine/src/flutter/display_list/dl_builder.cc index 134cb1f8f5e..a30612efb33 100644 --- a/engine/src/flutter/display_list/dl_builder.cc +++ b/engine/src/flutter/display_list/dl_builder.cc @@ -1025,6 +1025,42 @@ void DisplayListBuilder::ClipRoundRect(const DlRoundRect& rrect, break; } } +void DisplayListBuilder::ClipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) { + if (rse.IsRect()) { + ClipRect(rse.GetBounds(), clip_op, is_aa); + return; + } + if (rse.IsOval()) { + ClipOval(rse.GetBounds(), clip_op, is_aa); + return; + } + if (current_info().is_nop) { + return; + } + if (current_info().has_valid_clip && clip_op == DlClipOp::kIntersect && + layer_local_state().rsuperellipse_covers_cull(rse)) { + return; + } + global_state().clipRSuperellipse(rse, clip_op, is_aa); + layer_local_state().clipRSuperellipse(rse, clip_op, is_aa); + if (global_state().is_cull_rect_empty() || + layer_local_state().is_cull_rect_empty()) { + current_info().is_nop = true; + return; + } + current_info().has_valid_clip = true; + checkForDeferredSave(); + switch (clip_op) { + case DlClipOp::kIntersect: + Push(0, rse, is_aa); + break; + case DlClipOp::kDifference: + Push(0, rse, is_aa); + break; + } +} void DisplayListBuilder::ClipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) { @@ -1214,6 +1250,27 @@ void DisplayListBuilder::DrawDiffRoundRect(const DlRoundRect& outer, SetAttributesFromPaint(paint, DisplayListOpFlags::kDrawDRRectFlags); drawDiffRoundRect(outer, inner); } +void DisplayListBuilder::drawRoundSuperellipse(const DlRoundSuperellipse& rse) { + if (rse.IsRect()) { + drawRect(rse.GetBounds()); + } else if (rse.IsOval()) { + drawOval(rse.GetBounds()); + } else { + DisplayListAttributeFlags flags = kDrawRSuperellipseFlags; + OpResult result = PaintResult(current_, flags); + if (result != OpResult::kNoEffect && + AccumulateOpBounds(rse.GetBounds(), flags)) { + Push(0, rse); + CheckLayerOpacityCompatibility(); + UpdateLayerResult(result); + } + } +} +void DisplayListBuilder::DrawRoundSuperellipse(const DlRoundSuperellipse& rse, + const DlPaint& paint) { + SetAttributesFromPaint(paint, DisplayListOpFlags::kDrawRSuperellipseFlags); + drawRoundSuperellipse(rse); +} void DisplayListBuilder::drawPath(const DlPath& path) { DisplayListAttributeFlags flags = kDrawPathFlags; OpResult result = PaintResult(current_, flags); diff --git a/engine/src/flutter/display_list/dl_builder.h b/engine/src/flutter/display_list/dl_builder.h index a84df2651b2..6da6acc3211 100644 --- a/engine/src/flutter/display_list/dl_builder.h +++ b/engine/src/flutter/display_list/dl_builder.h @@ -118,6 +118,10 @@ class DisplayListBuilder final : public virtual DlCanvas, DlClipOp clip_op = DlClipOp::kIntersect, bool is_aa = false) override; // |DlCanvas| + void ClipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op = DlClipOp::kIntersect, + bool is_aa = false) override; + // |DlCanvas| void ClipPath(const DlPath& path, DlClipOp clip_op = DlClipOp::kIntersect, bool is_aa = false) override; @@ -172,6 +176,9 @@ class DisplayListBuilder final : public virtual DlCanvas, const DlRoundRect& inner, const DlPaint& paint) override; // |DlCanvas| + void DrawRoundSuperellipse(const DlRoundSuperellipse& rse, + const DlPaint& paint) override; + // |DlCanvas| void DrawPath(const DlPath& path, const DlPaint& paint) override; // |DlCanvas| void DrawArc(const DlRect& bounds, @@ -409,6 +416,12 @@ class DisplayListBuilder final : public virtual DlCanvas, ClipRoundRect(rrect, clip_op, is_aa); } // |DlOpReceiver| + void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) override { + ClipRoundSuperellipse(rse, clip_op, is_aa); + } + // |DlOpReceiver| void clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) override { ClipPath(path, clip_op, is_aa); } @@ -438,6 +451,8 @@ class DisplayListBuilder final : public virtual DlCanvas, void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override; // |DlOpReceiver| + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override; + // |DlOpReceiver| void drawPath(const DlPath& path) override; // |DlOpReceiver| void drawArc(const DlRect& bounds, diff --git a/engine/src/flutter/display_list/dl_canvas.h b/engine/src/flutter/display_list/dl_canvas.h index f255628aaf2..2eff3b1f9d3 100644 --- a/engine/src/flutter/display_list/dl_canvas.h +++ b/engine/src/flutter/display_list/dl_canvas.h @@ -85,6 +85,9 @@ class DlCanvas { virtual void ClipRoundRect(const DlRoundRect& rrect, DlClipOp clip_op = DlClipOp::kIntersect, bool is_aa = false) = 0; + virtual void ClipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op = DlClipOp::kIntersect, + bool is_aa = false) = 0; virtual void ClipPath(const DlPath& path, DlClipOp clip_op = DlClipOp::kIntersect, bool is_aa = false) = 0; @@ -125,6 +128,8 @@ class DlCanvas { virtual void DrawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner, const DlPaint& paint) = 0; + virtual void DrawRoundSuperellipse(const DlRoundSuperellipse& rse, + const DlPaint& paint) = 0; virtual void DrawPath(const DlPath& path, const DlPaint& paint) = 0; virtual void DrawArc(const DlRect& bounds, DlScalar start, diff --git a/engine/src/flutter/display_list/dl_op_flags.h b/engine/src/flutter/display_list/dl_op_flags.h index f5d4cfb5c00..4cdf089af94 100644 --- a/engine/src/flutter/display_list/dl_op_flags.h +++ b/engine/src/flutter/display_list/dl_op_flags.h @@ -323,6 +323,10 @@ class DisplayListOpFlags : DisplayListFlags { kBasePaintFlags | // kBaseStrokeOrFillFlags // }; + static constexpr DisplayListAttributeFlags kDrawRSuperellipseFlags{ + kBasePaintFlags | // + kBaseStrokeOrFillFlags // + }; static constexpr DisplayListAttributeFlags kDrawPathFlags{ kBasePaintFlags | // kBaseStrokeOrFillFlags | // diff --git a/engine/src/flutter/display_list/dl_op_receiver.h b/engine/src/flutter/display_list/dl_op_receiver.h index 87eb26c78b9..0d65bad7e0c 100644 --- a/engine/src/flutter/display_list/dl_op_receiver.h +++ b/engine/src/flutter/display_list/dl_op_receiver.h @@ -291,6 +291,9 @@ class DlOpReceiver { virtual void clipRoundRect(const DlRoundRect& rrect, DlClipOp clip_op, bool is_aa) = 0; + virtual void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) = 0; virtual void clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) = 0; // The following rendering methods all take their rendering attributes @@ -313,6 +316,7 @@ class DlOpReceiver { virtual void drawRoundRect(const DlRoundRect& rrect) = 0; virtual void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) = 0; + virtual void drawRoundSuperellipse(const DlRoundSuperellipse& rse) = 0; virtual void drawPath(const DlPath& path) = 0; virtual void drawArc(const DlRect& oval_bounds, DlScalar start_degrees, diff --git a/engine/src/flutter/display_list/dl_op_records.h b/engine/src/flutter/display_list/dl_op_records.h index 8611a98f602..8ce8db76ba4 100644 --- a/engine/src/flutter/display_list/dl_op_records.h +++ b/engine/src/flutter/display_list/dl_op_records.h @@ -486,6 +486,7 @@ struct TransformResetOp final : TransformClipOpBase { // DlRect is 16 more bytes, which packs efficiently into 24 bytes total // DlRoundRect is 48 more bytes, which rounds up to 48 bytes // which packs into 56 bytes total +// DlRoundSuperellipse is the same as DlRoundRect // CacheablePath is 128 more bytes, which packs efficiently into 136 bytes total // // We could pack the clip_op and the bool both into the free 4 bytes after @@ -509,9 +510,11 @@ struct TransformResetOp final : TransformClipOpBase { DEFINE_CLIP_SHAPE_OP(Rect, DlRect, Intersect) DEFINE_CLIP_SHAPE_OP(Oval, DlRect, Intersect) DEFINE_CLIP_SHAPE_OP(RoundRect, DlRoundRect, Intersect) +DEFINE_CLIP_SHAPE_OP(RoundSuperellipse, DlRoundSuperellipse, Intersect) DEFINE_CLIP_SHAPE_OP(Rect, DlRect, Difference) DEFINE_CLIP_SHAPE_OP(Oval, DlRect, Difference) DEFINE_CLIP_SHAPE_OP(RoundRect, DlRoundRect, Difference) +DEFINE_CLIP_SHAPE_OP(RoundSuperellipse, DlRoundSuperellipse, Difference) #undef DEFINE_CLIP_SHAPE_OP // 4 byte header + 20 byte payload packs evenly into 24 bytes @@ -578,6 +581,7 @@ struct DrawColorOp final : DrawOpBase { // SkOval is same as DlRect // DlRoundRect is 48 more bytes, using 52 bytes which rounds up to 56 bytes // total (4 bytes unused) +// DlRoundSuperellipse is the same as DlRoundRect #define DEFINE_DRAW_1ARG_OP(op_name, arg_type, arg_name) \ struct Draw##op_name##Op final : DrawOpBase { \ static constexpr auto kType = DisplayListOpType::kDraw##op_name; \ @@ -594,6 +598,7 @@ struct DrawColorOp final : DrawOpBase { DEFINE_DRAW_1ARG_OP(Rect, DlRect, rect) DEFINE_DRAW_1ARG_OP(Oval, DlRect, oval) DEFINE_DRAW_1ARG_OP(RoundRect, DlRoundRect, rrect) +DEFINE_DRAW_1ARG_OP(RoundSuperellipse, DlRoundSuperellipse, rse) #undef DEFINE_DRAW_1ARG_OP // 4 byte header + 16 byte payload uses 20 bytes but is rounded diff --git a/engine/src/flutter/display_list/geometry/dl_geometry_types.h b/engine/src/flutter/display_list/geometry/dl_geometry_types.h index 2e645149c72..cc8034b4348 100644 --- a/engine/src/flutter/display_list/geometry/dl_geometry_types.h +++ b/engine/src/flutter/display_list/geometry/dl_geometry_types.h @@ -9,6 +9,7 @@ #include "flutter/impeller/geometry/path.h" #include "flutter/impeller/geometry/rect.h" #include "flutter/impeller/geometry/round_rect.h" +#include "flutter/impeller/geometry/round_superellipse.h" #include "flutter/impeller/geometry/rstransform.h" #include "flutter/impeller/geometry/scalar.h" @@ -32,6 +33,7 @@ using DlISize = impeller::ISize32; using DlRect = impeller::Rect; using DlIRect = impeller::IRect32; using DlRoundRect = impeller::RoundRect; +using DlRoundSuperellipse = impeller::RoundSuperellipse; using DlRoundingRadii = impeller::RoundingRadii; using DlMatrix = impeller::Matrix; using DlQuad = impeller::Quad; @@ -203,6 +205,16 @@ inline const SkRRect ToSkRRect(const DlRoundRect& round_rect) { return rrect; }; +// Approximates a rounded superellipse with a round rectangle to the +// best practical accuracy. +// +// Skia does not support rounded superellipses directly, so rendering +// `DlRoundSuperellipses` on Skia requires falling back to RRect. +inline constexpr const SkRRect ToApproximateSkRRect( + const DlRoundSuperellipse& rse) { + return ToSkRRect(rse.ToApproximateRoundRect()); +}; + inline constexpr SkMatrix ToSkMatrix(const DlMatrix& matrix) { return SkMatrix::MakeAll(matrix.m[0], matrix.m[4], matrix.m[12], // matrix.m[1], matrix.m[5], matrix.m[13], // diff --git a/engine/src/flutter/display_list/geometry/dl_geometry_types_unittests.cc b/engine/src/flutter/display_list/geometry/dl_geometry_types_unittests.cc index 192fdc5d1eb..cab3ecceb49 100644 --- a/engine/src/flutter/display_list/geometry/dl_geometry_types_unittests.cc +++ b/engine/src/flutter/display_list/geometry/dl_geometry_types_unittests.cc @@ -68,5 +68,14 @@ TEST(DisplayListGeometryTypes, VectorToSizeConversion) { EXPECT_NE(ToDlSize(sk_v), dl_s); } +TEST(DisplayListGeometryTypes, RSEToRRectConversion) { + DlRoundSuperellipse dl_rse = DlRoundSuperellipse::MakeRectRadius( + DlRect::MakeLTRB(10, 20, 30, 40), 1.0f); + SkRRect sk_rrect = + SkRRect::MakeRectXY(SkRect::MakeLTRB(10, 20, 30, 40), 1.0f, 1.0f); + + EXPECT_EQ(sk_rrect, ToApproximateSkRRect(dl_rse)); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/display_list/skia/dl_sk_canvas.cc b/engine/src/flutter/display_list/skia/dl_sk_canvas.cc index ca5a8431e81..2163619a812 100644 --- a/engine/src/flutter/display_list/skia/dl_sk_canvas.cc +++ b/engine/src/flutter/display_list/skia/dl_sk_canvas.cc @@ -157,6 +157,13 @@ void DlSkCanvasAdapter::ClipRoundRect(const DlRoundRect& rrect, delegate_->clipRRect(ToSkRRect(rrect), ToSk(clip_op), is_aa); } +void DlSkCanvasAdapter::ClipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) { + // Skia doesn't support round superellipse, thus fall back to round rectangle. + delegate_->clipRRect(ToApproximateSkRRect(rse), ToSk(clip_op), is_aa); +} + void DlSkCanvasAdapter::ClipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) { @@ -235,6 +242,12 @@ void DlSkCanvasAdapter::DrawDiffRoundRect(const DlRoundRect& outer, delegate_->drawDRRect(ToSkRRect(outer), ToSkRRect(inner), ToSk(paint)); } +void DlSkCanvasAdapter::DrawRoundSuperellipse(const DlRoundSuperellipse& rse, + const DlPaint& paint) { + // Skia doesn't support round superellipse, thus fall back to round rectangle. + delegate_->drawRRect(ToApproximateSkRRect(rse), ToSk(paint)); +} + void DlSkCanvasAdapter::DrawPath(const DlPath& path, const DlPaint& paint) { path.WillRenderSkPath(); delegate_->drawPath(path.GetSkPath(), ToSk(paint)); diff --git a/engine/src/flutter/display_list/skia/dl_sk_canvas.h b/engine/src/flutter/display_list/skia/dl_sk_canvas.h index afac61564b1..0d4e9c66530 100644 --- a/engine/src/flutter/display_list/skia/dl_sk_canvas.h +++ b/engine/src/flutter/display_list/skia/dl_sk_canvas.h @@ -69,6 +69,9 @@ class DlSkCanvasAdapter final : public virtual DlCanvas { void ClipRoundRect(const DlRoundRect& rrect, DlClipOp clip_op, bool is_aa) override; + void ClipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) override; void ClipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) override; /// Conservative estimate of the bounds of all outstanding clip operations @@ -104,6 +107,8 @@ class DlSkCanvasAdapter final : public virtual DlCanvas { void DrawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner, const DlPaint& paint) override; + void DrawRoundSuperellipse(const DlRoundSuperellipse& rse, + const DlPaint& paint) override; void DrawPath(const DlPath& path, const DlPaint& paint) override; void DrawArc(const DlRect& bounds, DlScalar start, diff --git a/engine/src/flutter/display_list/skia/dl_sk_dispatcher.cc b/engine/src/flutter/display_list/skia/dl_sk_dispatcher.cc index a218fcf8bc6..b767eefaea0 100644 --- a/engine/src/flutter/display_list/skia/dl_sk_dispatcher.cc +++ b/engine/src/flutter/display_list/skia/dl_sk_dispatcher.cc @@ -140,6 +140,12 @@ void DlSkCanvasDispatcher::clipRoundRect(const DlRoundRect& rrect, bool is_aa) { canvas_->clipRRect(ToSkRRect(rrect), ToSk(clip_op), is_aa); } +void DlSkCanvasDispatcher::clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) { + // Skia doesn't support round superellipse, thus fall back to round rectangle. + canvas_->clipRRect(ToApproximateSkRRect(rse), ToSk(clip_op), is_aa); +} void DlSkCanvasDispatcher::clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) { @@ -192,6 +198,11 @@ void DlSkCanvasDispatcher::drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) { canvas_->drawDRRect(ToSkRRect(outer), ToSkRRect(inner), paint()); } +void DlSkCanvasDispatcher::drawRoundSuperellipse( + const DlRoundSuperellipse& rse) { + // Skia doesn't support round superellipse, thus fall back to round rectangle. + canvas_->drawRRect(ToApproximateSkRRect(rse), paint()); +} void DlSkCanvasDispatcher::drawPath(const DlPath& path) { path.WillRenderSkPath(); canvas_->drawPath(path.GetSkPath(), paint()); diff --git a/engine/src/flutter/display_list/skia/dl_sk_dispatcher.h b/engine/src/flutter/display_list/skia/dl_sk_dispatcher.h index cfb5b529283..e06a75b175c 100644 --- a/engine/src/flutter/display_list/skia/dl_sk_dispatcher.h +++ b/engine/src/flutter/display_list/skia/dl_sk_dispatcher.h @@ -56,6 +56,9 @@ class DlSkCanvasDispatcher : public virtual DlOpReceiver, void clipRoundRect(const DlRoundRect& rrect, DlClipOp clip_op, bool is_aa) override; + void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) override; void clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) override; void drawPaint() override; @@ -71,6 +74,7 @@ class DlSkCanvasDispatcher : public virtual DlOpReceiver, void drawRoundRect(const DlRoundRect& rrect) override; void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override; + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override; void drawPath(const DlPath& path) override; void drawArc(const DlRect& bounds, DlScalar start, diff --git a/engine/src/flutter/display_list/testing/dl_test_snippets.cc b/engine/src/flutter/display_list/testing/dl_test_snippets.cc index 39f9ae146f7..fa69a34518d 100644 --- a/engine/src/flutter/display_list/testing/dl_test_snippets.cc +++ b/engine/src/flutter/display_list/testing/dl_test_snippets.cc @@ -546,6 +546,34 @@ std::vector CreateAllClipOps() { r.clipRoundRect(kTestRRect, DlClipOp::kDifference, false); }}, }}, + {"ClipRSuperellipse", + { + {1, 56, 0, + [](DlOpReceiver& r) { + r.clipRoundSuperellipse(kTestRSuperellipse, DlClipOp::kIntersect, + true); + }}, + {1, 56, 0, + [](DlOpReceiver& r) { + r.clipRoundSuperellipse(kTestRSuperellipse.Shift(1, 1), + DlClipOp::kIntersect, true); + }}, + {1, 56, 0, + [](DlOpReceiver& r) { + r.clipRoundSuperellipse(kTestRSuperellipse, DlClipOp::kIntersect, + false); + }}, + {1, 56, 0, + [](DlOpReceiver& r) { + r.clipRoundSuperellipse(kTestRSuperellipse, DlClipOp::kDifference, + true); + }}, + {1, 56, 0, + [](DlOpReceiver& r) { + r.clipRoundSuperellipse(kTestRSuperellipse, DlClipOp::kDifference, + false); + }}, + }}, {"ClipPath", { {1, 24, 0, @@ -725,6 +753,17 @@ std::vector CreateAllRenderingOps() { {1, 56, 1, [](DlOpReceiver& r) { r.drawRoundRect(kTestRRect.Shift(5, 5)); }}, }}, + {"DrawRSuperellipse", + { + {1, 56, 1, + [](DlOpReceiver& r) { + r.drawRoundSuperellipse(kTestRSuperellipse); + }}, + {1, 56, 1, + [](DlOpReceiver& r) { + r.drawRoundSuperellipse(kTestRSuperellipse.Shift(5, 5)); + }}, + }}, {"DrawDRRect", { {1, 104, 1, diff --git a/engine/src/flutter/display_list/testing/dl_test_snippets.h b/engine/src/flutter/display_list/testing/dl_test_snippets.h index b4ba1cd6984..afb532d529d 100644 --- a/engine/src/flutter/display_list/testing/dl_test_snippets.h +++ b/engine/src/flutter/display_list/testing/dl_test_snippets.h @@ -170,6 +170,8 @@ constexpr DlRect kTestBounds = DlRect::MakeLTRB(10, 10, 50, 60); constexpr SkRect kTestSkBounds = SkRect::MakeLTRB(10, 10, 50, 60); static const DlRoundRect kTestRRect = DlRoundRect::MakeRectXY(kTestBounds, 5, 5); +static const DlRoundSuperellipse kTestRSuperellipse = + DlRoundSuperellipse::MakeRectXY(kTestBounds, 3, 3); static const SkRRect kTestSkRRect = SkRRect::MakeRectXY(kTestSkBounds, 5, 5); static const SkRRect kTestRRectRect = SkRRect::MakeRect(kTestSkBounds); static const DlRoundRect kTestInnerRRect = diff --git a/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.cc b/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.cc index 9d8494e6195..91417bca901 100644 --- a/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.cc +++ b/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.cc @@ -6,6 +6,7 @@ #include "flutter/display_list/dl_builder.h" #include "flutter/fml/logging.h" +#include "flutter/impeller/geometry/round_superellipse_param.h" namespace flutter { @@ -67,6 +68,20 @@ void DisplayListMatrixClipState::clipOval(const DlRect& bounds, } } +namespace { +inline std::array RoundingRadiiSafeRects( + const DlRect& bounds, + const impeller::RoundingRadii& radii) { + return { + bounds.Expand( // + -std::max(radii.top_left.width, radii.bottom_left.width), 0, + -std::max(radii.top_right.width, radii.bottom_right.width), 0), + bounds.Expand( + 0, -std::max(radii.top_left.height, radii.top_right.height), // + 0, -std::max(radii.bottom_left.height, radii.bottom_right.height))}; +} +} // namespace + void DisplayListMatrixClipState::clipRRect(const DlRoundRect& rrect, DlClipOp op, bool is_aa) { @@ -83,15 +98,34 @@ void DisplayListMatrixClipState::clipRRect(const DlRoundRect& rrect, cull_rect_ = DlRect(); return; } - auto radii = rrect.GetRadii(); - DlRect safe = bounds.Expand( - -std::max(radii.top_left.width, radii.bottom_left.width), 0, - -std::max(radii.top_right.width, radii.bottom_right.width), 0); - adjustCullRect(safe, op, is_aa); - safe = bounds.Expand( - 0, -std::max(radii.top_left.height, radii.top_right.height), // - 0, -std::max(radii.bottom_left.height, radii.bottom_right.height)); - adjustCullRect(safe, op, is_aa); + auto safe_rects = RoundingRadiiSafeRects(bounds, rrect.GetRadii()); + adjustCullRect(safe_rects[0], op, is_aa); + adjustCullRect(safe_rects[1], op, is_aa); + break; + } + } +} + +void DisplayListMatrixClipState::clipRSuperellipse( + const DlRoundSuperellipse& rse, + DlClipOp op, + bool is_aa) { + DlRect bounds = rse.GetBounds(); + if (rse.IsRect()) { + return clipRect(bounds, op, is_aa); + } + switch (op) { + case DlClipOp::kIntersect: + adjustCullRect(bounds, op, is_aa); + break; + case DlClipOp::kDifference: { + if (rsuperellipse_covers_cull(rse)) { + cull_rect_ = DlRect(); + return; + } + auto safe_rects = RoundingRadiiSafeRects(bounds, rse.GetRadii()); + adjustCullRect(safe_rects[0], op, is_aa); + adjustCullRect(safe_rects[1], op, is_aa); break; } } @@ -299,6 +333,38 @@ bool DisplayListMatrixClipState::rrect_covers_cull( return true; } +bool DisplayListMatrixClipState::rsuperellipse_covers_cull( + const DlRoundSuperellipse& content) const { + if (content.IsEmpty()) { + return false; + } + if (cull_rect_.IsEmpty()) { + return true; + } + if (content.IsRect()) { + return rect_covers_cull(content.GetBounds()); + } + if (content.IsOval()) { + return oval_covers_cull(content.GetBounds()); + } + DlPoint corners[4]; + if (!getLocalCullCorners(corners)) { + return false; + } + auto outer = content.GetBounds(); + auto param = impeller::RoundSuperellipseParam::MakeBoundsRadii( + outer, content.GetRadii()); + for (auto corner : corners) { + if (!outer.Contains(corner)) { + return false; + } + if (!param.Contains(corner)) { + return false; + } + } + return true; +} + bool DisplayListMatrixClipState::getLocalCullCorners(DlPoint corners[4]) const { if (!is_matrix_invertable()) { return false; diff --git a/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.h b/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.h index 09df43670bb..b1323f53ed8 100644 --- a/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.h +++ b/engine/src/flutter/display_list/utils/dl_matrix_clip_tracker.h @@ -43,6 +43,7 @@ class DisplayListMatrixClipState { bool rect_covers_cull(const DlRect& content) const; bool oval_covers_cull(const DlRect& content_bounds) const; bool rrect_covers_cull(const DlRoundRect& content) const; + bool rsuperellipse_covers_cull(const DlRoundSuperellipse& content) const; bool content_culled(const DlRect& content_bounds) const; bool is_cull_rect_empty() const { return cull_rect_.IsEmpty(); } @@ -107,6 +108,9 @@ class DisplayListMatrixClipState { void clipRect(const DlRect& rect, DlClipOp op, bool is_aa); void clipOval(const DlRect& bounds, DlClipOp op, bool is_aa); void clipRRect(const DlRoundRect& rrect, DlClipOp op, bool is_aa); + void clipRSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp op, + bool is_aa); void clipPath(const DlPath& path, DlClipOp op, bool is_aa); private: diff --git a/engine/src/flutter/display_list/utils/dl_receiver_utils.h b/engine/src/flutter/display_list/utils/dl_receiver_utils.h index 0b24eb48092..a518f1ef6bd 100644 --- a/engine/src/flutter/display_list/utils/dl_receiver_utils.h +++ b/engine/src/flutter/display_list/utils/dl_receiver_utils.h @@ -47,6 +47,9 @@ class IgnoreClipDispatchHelper : public virtual DlOpReceiver { DlClipOp clip_op, bool is_aa) override {} void clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) override {} + void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) override {} }; // A utility class that will ignore all DlOpReceiver methods relating @@ -92,6 +95,7 @@ class IgnoreDrawDispatchHelper : public virtual DlOpReceiver { void drawRoundRect(const DlRoundRect& rrect) override {} void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override {} + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override {} void drawPath(const DlPath& path) override {} void drawArc(const DlRect& oval_bounds, DlScalar start_degrees, diff --git a/engine/src/flutter/flow/BUILD.gn b/engine/src/flutter/flow/BUILD.gn index 1820cf43cd3..ce4afa97812 100644 --- a/engine/src/flutter/flow/BUILD.gn +++ b/engine/src/flutter/flow/BUILD.gn @@ -31,6 +31,8 @@ source_set("flow") { "layers/clip_rect_layer.h", "layers/clip_rrect_layer.cc", "layers/clip_rrect_layer.h", + "layers/clip_rsuperellipse_layer.cc", + "layers/clip_rsuperellipse_layer.h", "layers/clip_shape_layer.h", "layers/color_filter_layer.cc", "layers/color_filter_layer.h", @@ -161,6 +163,7 @@ if (enable_unittests) { "layers/clip_path_layer_unittests.cc", "layers/clip_rect_layer_unittests.cc", "layers/clip_rrect_layer_unittests.cc", + "layers/clip_rsuperellipse_layer_unittests.cc", "layers/color_filter_layer_unittests.cc", "layers/container_layer_unittests.cc", "layers/display_list_layer_unittests.cc", diff --git a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc new file mode 100644 index 00000000000..d7f05013958 --- /dev/null +++ b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.cc @@ -0,0 +1,23 @@ +// 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. + +#include "flutter/flow/layers/clip_rsuperellipse_layer.h" + +namespace flutter { + +ClipRSuperellipseLayer::ClipRSuperellipseLayer( + const DlRoundSuperellipse& clip_rsuperellipse, + Clip clip_behavior) + : ClipShapeLayer(clip_rsuperellipse, clip_behavior) {} + +const DlRect ClipRSuperellipseLayer::clip_shape_bounds() const { + return clip_shape().GetBounds(); +} + +void ClipRSuperellipseLayer::ApplyClip( + LayerStateStack::MutatorContext& mutator) const { + mutator.clipRSuperellipse(clip_shape(), clip_behavior() != Clip::kHardEdge); +} + +} // namespace flutter diff --git a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h new file mode 100644 index 00000000000..8f1ba25f614 --- /dev/null +++ b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer.h @@ -0,0 +1,28 @@ +// 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. + +#ifndef FLUTTER_FLOW_LAYERS_CLIP_RSUPERELLIPSE_LAYER_H_ +#define FLUTTER_FLOW_LAYERS_CLIP_RSUPERELLIPSE_LAYER_H_ + +#include "flutter/flow/layers/clip_shape_layer.h" + +namespace flutter { + +class ClipRSuperellipseLayer : public ClipShapeLayer { + public: + ClipRSuperellipseLayer(const DlRoundSuperellipse& clip_rsuperellipse, + Clip clip_behavior); + + protected: + const DlRect clip_shape_bounds() const override; + + void ApplyClip(LayerStateStack::MutatorContext& mutator) const override; + + private: + FML_DISALLOW_COPY_AND_ASSIGN(ClipRSuperellipseLayer); +}; + +} // namespace flutter + +#endif // FLUTTER_FLOW_LAYERS_CLIP_RSUPERELLIPSE_LAYER_H_ diff --git a/engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc new file mode 100644 index 00000000000..5c88ae28d05 --- /dev/null +++ b/engine/src/flutter/flow/layers/clip_rsuperellipse_layer_unittests.cc @@ -0,0 +1,628 @@ +// 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. + +#include "flutter/flow/layers/clip_rsuperellipse_layer.h" + +#include "flutter/flow/layers/layer_tree.h" +#include "flutter/flow/layers/opacity_layer.h" +#include "flutter/flow/layers/platform_view_layer.h" +#include "flutter/flow/testing/layer_test.h" +#include "flutter/flow/testing/mock_embedder.h" +#include "flutter/flow/testing/mock_layer.h" +#include "flutter/fml/macros.h" + +// TODO(zanderso): https://github.com/flutter/flutter/issues/127701 +// NOLINTBEGIN(bugprone-unchecked-optional-access) + +namespace flutter { +namespace testing { + +using ClipRSuperellipseLayerTest = LayerTest; + +#ifndef NDEBUG +TEST_F(ClipRSuperellipseLayerTest, ClipNoneBehaviorDies) { + const DlRoundSuperellipse layer_rsuperellipse = DlRoundSuperellipse(); + EXPECT_DEATH_IF_SUPPORTED( + auto clip = std::make_shared(layer_rsuperellipse, + Clip::kNone), + "clip_behavior != Clip::kNone"); +} + +TEST_F(ClipRSuperellipseLayerTest, PaintingEmptyLayerDies) { + const DlRoundSuperellipse layer_rsuperellipse = DlRoundSuperellipse(); + auto layer = std::make_shared(layer_rsuperellipse, + Clip::kHardEdge); + + layer->Preroll(preroll_context()); + + // Untouched + EXPECT_EQ(preroll_context()->state_stack.device_cull_rect(), kGiantRect); + EXPECT_TRUE(preroll_context()->state_stack.is_empty()); + + EXPECT_EQ(layer->paint_bounds(), DlRect()); + EXPECT_EQ(layer->child_paint_bounds(), DlRect()); + EXPECT_FALSE(layer->needs_painting(paint_context())); + + EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), + "needs_painting\\(context\\)"); +} + +TEST_F(ClipRSuperellipseLayerTest, PaintBeforePrerollDies) { + const DlRect layer_bounds = DlRect::MakeXYWH(0.5, 1.0, 5.0, 6.0); + const DlRoundSuperellipse layer_rsuperellipse = + DlRoundSuperellipse::MakeRect(layer_bounds); + auto layer = std::make_shared(layer_rsuperellipse, + Clip::kHardEdge); + EXPECT_EQ(layer->paint_bounds(), DlRect()); + EXPECT_EQ(layer->child_paint_bounds(), DlRect()); + EXPECT_FALSE(layer->needs_painting(paint_context())); + + EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), + "needs_painting\\(context\\)"); +} + +TEST_F(ClipRSuperellipseLayerTest, PaintingCulledLayerDies) { + const DlMatrix initial_matrix = DlMatrix::MakeTranslation({0.5f, 1.0f}); + const DlRect child_bounds = DlRect::MakeXYWH(1.0, 2.0, 2.0, 2.0); + const DlRect layer_bounds = DlRect::MakeXYWH(0.5, 1.0, 5.0, 6.0); + const DlRect distant_bounds = DlRect::MakeXYWH(100.0, 100.0, 10.0, 10.0); + const DlPath child_path = DlPath::MakeRect(child_bounds); + const DlRoundSuperellipse layer_rsuperellipse = + DlRoundSuperellipse::MakeRect(layer_bounds); + const DlPaint child_paint = DlPaint(DlColor::kYellow()); + auto mock_layer = std::make_shared(child_path, child_paint); + auto layer = std::make_shared(layer_rsuperellipse, + Clip::kHardEdge); + layer->Add(mock_layer); + + // Cull these children + preroll_context()->state_stack.set_preroll_delegate(distant_bounds, + initial_matrix); + layer->Preroll(preroll_context()); + + // Untouched + EXPECT_EQ(preroll_context()->state_stack.device_cull_rect(), distant_bounds); + EXPECT_TRUE(preroll_context()->state_stack.is_empty()); + + EXPECT_EQ(mock_layer->paint_bounds(), child_bounds); + EXPECT_EQ(layer->paint_bounds(), child_bounds); + EXPECT_EQ(layer->child_paint_bounds(), child_bounds); + EXPECT_TRUE(mock_layer->needs_painting(paint_context())); + EXPECT_TRUE(layer->needs_painting(paint_context())); + EXPECT_EQ(mock_layer->parent_cull_rect(), DlRect()); + EXPECT_EQ(mock_layer->parent_matrix(), initial_matrix); + EXPECT_EQ(mock_layer->parent_mutators(), + std::vector({Mutator(ToApproximateSkRRect(layer_rsuperellipse))})); + + auto mutator = paint_context().state_stack.save(); + mutator.clipRect(distant_bounds, false); + EXPECT_FALSE(mock_layer->needs_painting(paint_context())); + EXPECT_FALSE(layer->needs_painting(paint_context())); + EXPECT_DEATH_IF_SUPPORTED(layer->Paint(paint_context()), + "needs_painting\\(context\\)"); +} +#endif + +TEST_F(ClipRSuperellipseLayerTest, ChildOutsideBounds) { + const DlMatrix initial_matrix = DlMatrix::MakeTranslation({0.5f, 1.0f}); + const DlRect local_cull_bounds = DlRect::MakeXYWH(0.0, 0.0, 2.0, 4.0); + const DlRect device_cull_bounds = + local_cull_bounds.TransformAndClipBounds(initial_matrix); + const DlRect child_bounds = DlRect::MakeXYWH(2.5, 5.0, 4.5, 4.0); + const DlRect clip_bounds = DlRect::MakeXYWH(0.5, 1.0, 5.0, 6.0); + const DlPath child_path = DlPath::MakeRect(child_bounds); + const DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRect(clip_bounds); + const DlPaint child_paint = DlPaint(DlColor::kYellow()); + auto mock_layer = std::make_shared(child_path, child_paint); + auto layer = std::make_shared(clip_rsuperellipse, + Clip::kHardEdge); + layer->Add(mock_layer); + + auto clip_cull_rect = clip_bounds.Intersection(local_cull_bounds); + ASSERT_TRUE(clip_cull_rect.has_value()); + auto clip_layer_bounds = child_bounds.Intersection(clip_bounds); + ASSERT_TRUE(clip_layer_bounds.has_value()); + + // Set up both contexts to cull clipped child + preroll_context()->state_stack.set_preroll_delegate(device_cull_bounds, + initial_matrix); + paint_context().canvas->ClipRect(device_cull_bounds); + paint_context().canvas->Transform(initial_matrix); + + layer->Preroll(preroll_context()); + // Untouched + EXPECT_EQ(preroll_context()->state_stack.device_cull_rect(), + device_cull_bounds); + EXPECT_EQ(preroll_context()->state_stack.local_cull_rect(), + local_cull_bounds); + EXPECT_TRUE(preroll_context()->state_stack.is_empty()); + + EXPECT_EQ(mock_layer->paint_bounds(), child_bounds); + EXPECT_EQ(layer->paint_bounds(), clip_layer_bounds.value()); + EXPECT_EQ(layer->child_paint_bounds(), child_bounds); + EXPECT_EQ(mock_layer->parent_cull_rect(), clip_cull_rect.value()); + EXPECT_EQ(mock_layer->parent_matrix(), initial_matrix); + EXPECT_EQ(mock_layer->parent_mutators(), + std::vector({Mutator(ToApproximateSkRRect(clip_rsuperellipse))})); + + EXPECT_FALSE(mock_layer->needs_painting(paint_context())); + ASSERT_FALSE(layer->needs_painting(paint_context())); + // Top level layer not visible so calling layer->Paint() + // would trip an FML_DCHECK +} + +TEST_F(ClipRSuperellipseLayerTest, FullyContainedChild) { + const DlMatrix initial_matrix = DlMatrix::MakeTranslation({0.5f, 1.0f}); + const DlRect child_bounds = DlRect::MakeXYWH(1.0, 2.0, 2.0, 2.0); + const DlRect layer_bounds = DlRect::MakeXYWH(0.5, 1.0, 5.0, 6.0); + const DlPath child_path = DlPath::MakeRect(child_bounds) + + DlPath::MakeOval(child_bounds.Expand(-0.1f)); + const DlRoundSuperellipse layer_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(layer_bounds, 0.1, 0.1); + const DlPaint child_paint = DlPaint(DlColor::kYellow()); + auto mock_layer = std::make_shared(child_path, child_paint); + auto layer = std::make_shared(layer_rsuperellipse, + Clip::kHardEdge); + layer->Add(mock_layer); + + preroll_context()->state_stack.set_preroll_delegate(initial_matrix); + layer->Preroll(preroll_context()); + + // Untouched + EXPECT_EQ(preroll_context()->state_stack.device_cull_rect(), kGiantRect); + EXPECT_TRUE(preroll_context()->state_stack.is_empty()); + + EXPECT_EQ(mock_layer->paint_bounds(), child_bounds); + EXPECT_EQ(layer->paint_bounds(), mock_layer->paint_bounds()); + EXPECT_EQ(layer->child_paint_bounds(), child_bounds); + EXPECT_TRUE(mock_layer->needs_painting(paint_context())); + EXPECT_TRUE(layer->needs_painting(paint_context())); + EXPECT_EQ(mock_layer->parent_cull_rect(), layer_bounds); + EXPECT_EQ(mock_layer->parent_matrix(), initial_matrix); + EXPECT_EQ(mock_layer->parent_mutators(), + std::vector({Mutator(ToApproximateSkRRect(layer_rsuperellipse))})); + + layer->Paint(display_list_paint_context()); + DisplayListBuilder expected_builder; + /* (ClipRSuperellipse)layer::Paint */ { + expected_builder.Save(); + { + expected_builder.ClipRoundSuperellipse(layer_rsuperellipse); + /* mock_layer::Paint */ { + expected_builder.DrawPath(child_path, child_paint); + } + } + expected_builder.Restore(); + } + EXPECT_TRUE(DisplayListsEQ_Verbose(display_list(), expected_builder.Build())); +} + +TEST_F(ClipRSuperellipseLayerTest, PartiallyContainedChild) { + const DlMatrix initial_matrix = DlMatrix::MakeTranslation({0.5f, 1.0f}); + const DlRect local_cull_bounds = DlRect::MakeXYWH(0.0, 0.0, 4.0, 5.5); + const DlRect device_cull_bounds = + local_cull_bounds.TransformAndClipBounds(initial_matrix); + const DlRect child_bounds = DlRect::MakeXYWH(2.5, 5.0, 4.5, 4.0); + const DlRect clip_bounds = DlRect::MakeXYWH(0.5, 1.0, 5.0, 6.0); + const DlPath child_path = DlPath::MakeRect(child_bounds) + + DlPath::MakeOval(child_bounds.Expand(-0.1f)); + const DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(clip_bounds, 0.1, 0.1); + const DlPaint child_paint = DlPaint(DlColor::kYellow()); + auto mock_layer = std::make_shared(child_path, child_paint); + auto layer = std::make_shared(clip_rsuperellipse, + Clip::kHardEdge); + layer->Add(mock_layer); + + auto clip_cull_rect = clip_bounds.Intersection(local_cull_bounds); + ASSERT_TRUE(clip_cull_rect.has_value()); + auto clip_layer_bounds = child_bounds.Intersection(clip_bounds); + ASSERT_TRUE(clip_layer_bounds.has_value()); + + preroll_context()->state_stack.set_preroll_delegate(device_cull_bounds, + initial_matrix); + + layer->Preroll(preroll_context()); + // Untouched + EXPECT_EQ(preroll_context()->state_stack.device_cull_rect(), + device_cull_bounds); + EXPECT_EQ(preroll_context()->state_stack.local_cull_rect(), + local_cull_bounds); + EXPECT_TRUE(preroll_context()->state_stack.is_empty()); + + EXPECT_EQ(mock_layer->paint_bounds(), child_bounds); + EXPECT_EQ(layer->paint_bounds(), clip_layer_bounds.value()); + EXPECT_EQ(layer->child_paint_bounds(), child_bounds); + EXPECT_EQ(mock_layer->parent_cull_rect(), clip_cull_rect.value()); + EXPECT_EQ(mock_layer->parent_matrix(), initial_matrix); + EXPECT_EQ(mock_layer->parent_mutators(), + std::vector({Mutator(ToApproximateSkRRect(clip_rsuperellipse))})); + + layer->Paint(display_list_paint_context()); + DisplayListBuilder expected_builder; + /* (ClipRSuperellipse)layer::Paint */ { + expected_builder.Save(); + { + expected_builder.ClipRoundSuperellipse(clip_rsuperellipse); + /* mock_layer::Paint */ { + expected_builder.DrawPath(child_path, child_paint); + } + } + expected_builder.Restore(); + } + EXPECT_TRUE(DisplayListsEQ_Verbose(display_list(), expected_builder.Build())); +} + +static bool ReadbackResult(PrerollContext* context, + Clip clip_behavior, + const std::shared_ptr& child, + bool before) { + const DlRect layer_bounds = DlRect::MakeXYWH(0.5, 1.0, 5.0, 6.0); + const DlRoundSuperellipse layer_rsuperellipse = + DlRoundSuperellipse::MakeRect(layer_bounds); + auto layer = std::make_shared(layer_rsuperellipse, + clip_behavior); + if (child != nullptr) { + layer->Add(child); + } + context->surface_needs_readback = before; + layer->Preroll(context); + return context->surface_needs_readback; +} + +TEST_F(ClipRSuperellipseLayerTest, Readback) { + PrerollContext* context = preroll_context(); + DlPath path; + DlPaint paint; + + const Clip hard = Clip::kHardEdge; + const Clip soft = Clip::kAntiAlias; + const Clip save_layer = Clip::kAntiAliasWithSaveLayer; + + std::shared_ptr nochild; + auto reader = std::make_shared(path, paint); + reader->set_fake_reads_surface(true); + auto nonreader = std::make_shared(path, paint); + + // No children, no prior readback -> no readback after + EXPECT_FALSE(ReadbackResult(context, hard, nochild, false)); + EXPECT_FALSE(ReadbackResult(context, soft, nochild, false)); + EXPECT_FALSE(ReadbackResult(context, save_layer, nochild, false)); + + // No children, prior readback -> readback after + EXPECT_TRUE(ReadbackResult(context, hard, nochild, true)); + EXPECT_TRUE(ReadbackResult(context, soft, nochild, true)); + EXPECT_TRUE(ReadbackResult(context, save_layer, nochild, true)); + + // Non readback child, no prior readback -> no readback after + EXPECT_FALSE(ReadbackResult(context, hard, nonreader, false)); + EXPECT_FALSE(ReadbackResult(context, soft, nonreader, false)); + EXPECT_FALSE(ReadbackResult(context, save_layer, nonreader, false)); + + // Non readback child, prior readback -> readback after + EXPECT_TRUE(ReadbackResult(context, hard, nonreader, true)); + EXPECT_TRUE(ReadbackResult(context, soft, nonreader, true)); + EXPECT_TRUE(ReadbackResult(context, save_layer, nonreader, true)); + + // Readback child, no prior readback -> readback after unless SaveLayer + EXPECT_TRUE(ReadbackResult(context, hard, reader, false)); + EXPECT_TRUE(ReadbackResult(context, soft, reader, false)); + EXPECT_FALSE(ReadbackResult(context, save_layer, reader, false)); + + // Readback child, prior readback -> readback after + EXPECT_TRUE(ReadbackResult(context, hard, reader, true)); + EXPECT_TRUE(ReadbackResult(context, soft, reader, true)); + EXPECT_TRUE(ReadbackResult(context, save_layer, reader, true)); +} + +TEST_F(ClipRSuperellipseLayerTest, OpacityInheritance) { + auto path1 = DlPath::MakeRectLTRB(10, 10, 30, 30); + auto mock1 = MockLayer::MakeOpacityCompatible(path1); + DlRect clip_rect = DlRect::MakeWH(500, 500); + DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(clip_rect, 20, 20); + auto clip_rsuperellipse_layer = std::make_shared( + clip_rsuperellipse, Clip::kHardEdge); + clip_rsuperellipse_layer->Add(mock1); + + // ClipRectLayer will pass through compatibility from a compatible child + PrerollContext* context = preroll_context(); + clip_rsuperellipse_layer->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, + LayerStateStack::kCallerCanApplyOpacity); + + auto path2 = DlPath::MakeRectLTRB(40, 40, 50, 50); + auto mock2 = MockLayer::MakeOpacityCompatible(path2); + clip_rsuperellipse_layer->Add(mock2); + + // ClipRectLayer will pass through compatibility from multiple + // non-overlapping compatible children + clip_rsuperellipse_layer->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, + LayerStateStack::kCallerCanApplyOpacity); + + auto path3 = DlPath::MakeRectLTRB(20, 20, 40, 40); + auto mock3 = MockLayer::MakeOpacityCompatible(path3); + clip_rsuperellipse_layer->Add(mock3); + + // ClipRectLayer will not pass through compatibility from multiple + // overlapping children even if they are individually compatible + clip_rsuperellipse_layer->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, 0); + + { + // ClipRectLayer(aa with saveLayer) will always be compatible + auto clip_rsuperellipse_savelayer = + std::make_shared(clip_rsuperellipse, + Clip::kAntiAliasWithSaveLayer); + clip_rsuperellipse_savelayer->Add(mock1); + clip_rsuperellipse_savelayer->Add(mock2); + + // Double check first two children are compatible and non-overlapping + clip_rsuperellipse_savelayer->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, Layer::kSaveLayerRenderFlags); + + // Now add the overlapping child and test again, should still be compatible + clip_rsuperellipse_savelayer->Add(mock3); + clip_rsuperellipse_savelayer->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, Layer::kSaveLayerRenderFlags); + } + + // An incompatible, but non-overlapping child for the following tests + auto path4 = DlPath::MakeRectLTRB(60, 60, 70, 70); + auto mock4 = MockLayer::Make(path4); + + { + // ClipRectLayer with incompatible child will not be compatible + auto clip_rsuperellipse_bad_child = + std::make_shared(clip_rsuperellipse, + Clip::kHardEdge); + clip_rsuperellipse_bad_child->Add(mock1); + clip_rsuperellipse_bad_child->Add(mock2); + + // Double check first two children are compatible and non-overlapping + clip_rsuperellipse_bad_child->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, + LayerStateStack::kCallerCanApplyOpacity); + + clip_rsuperellipse_bad_child->Add(mock4); + + // The third child is non-overlapping, but not compatible so the + // TransformLayer should end up incompatible + clip_rsuperellipse_bad_child->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, 0); + } + + { + // ClipRectLayer(aa with saveLayer) will always be compatible + auto clip_rsuperellipse_savelayer_bad_child = + std::make_shared(clip_rsuperellipse, + Clip::kAntiAliasWithSaveLayer); + clip_rsuperellipse_savelayer_bad_child->Add(mock1); + clip_rsuperellipse_savelayer_bad_child->Add(mock2); + + // Double check first two children are compatible and non-overlapping + clip_rsuperellipse_savelayer_bad_child->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, Layer::kSaveLayerRenderFlags); + + // Now add the incompatible child and test again, should still be compatible + clip_rsuperellipse_savelayer_bad_child->Add(mock4); + clip_rsuperellipse_savelayer_bad_child->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, Layer::kSaveLayerRenderFlags); + } +} + +TEST_F(ClipRSuperellipseLayerTest, OpacityInheritancePainting) { + auto path1 = DlPath::MakeRectLTRB(10, 10, 30, 30); + auto mock1 = MockLayer::MakeOpacityCompatible(path1); + auto path2 = DlPath::MakeRectLTRB(40, 40, 50, 50); + auto mock2 = MockLayer::MakeOpacityCompatible(path2); + DlRect clip_rect = DlRect::MakeWH(500, 500); + DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(clip_rect, 20, 20); + auto clip_rect_layer = std::make_shared( + clip_rsuperellipse, Clip::kAntiAlias); + clip_rect_layer->Add(mock1); + clip_rect_layer->Add(mock2); + + // ClipRectLayer will pass through compatibility from multiple + // non-overlapping compatible children + PrerollContext* context = preroll_context(); + clip_rect_layer->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, + LayerStateStack::kCallerCanApplyOpacity); + + int opacity_alpha = 0x7F; + DlPoint offset = DlPoint(10, 10); + auto opacity_layer = std::make_shared(opacity_alpha, offset); + opacity_layer->Add(clip_rect_layer); + opacity_layer->Preroll(context); + EXPECT_TRUE(opacity_layer->children_can_accept_opacity()); + + DisplayListBuilder expected_builder; + /* OpacityLayer::Paint() */ { + expected_builder.Save(); + { + expected_builder.Translate(offset.x, offset.y); + /* ClipRectLayer::Paint() */ { + expected_builder.Save(); + expected_builder.ClipRoundSuperellipse(clip_rsuperellipse, + DlClipOp::kIntersect, true); + /* child layer1 paint */ { + expected_builder.DrawPath(path1, DlPaint().setAlpha(opacity_alpha)); + } + /* child layer2 paint */ { + expected_builder.DrawPath(path2, DlPaint().setAlpha(opacity_alpha)); + } + expected_builder.Restore(); + } + } + expected_builder.Restore(); + } + + opacity_layer->Paint(display_list_paint_context()); + EXPECT_TRUE(DisplayListsEQ_Verbose(expected_builder.Build(), display_list())); +} + +TEST_F(ClipRSuperellipseLayerTest, OpacityInheritanceSaveLayerPainting) { + auto path1 = DlPath::MakeRectLTRB(10, 10, 30, 30); + auto mock1 = MockLayer::MakeOpacityCompatible(path1); + auto path2 = DlPath::MakeRectLTRB(20, 20, 40, 40); + auto mock2 = MockLayer::MakeOpacityCompatible(path2); + auto children_bounds = path1.GetBounds().Union(path2.GetBounds()); + DlRect clip_rect = DlRect::MakeWH(500, 500); + DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(clip_rect, 20, 20); + auto clip_rsuperellipse_layer = std::make_shared( + clip_rsuperellipse, Clip::kAntiAliasWithSaveLayer); + clip_rsuperellipse_layer->Add(mock1); + clip_rsuperellipse_layer->Add(mock2); + + // ClipRectLayer will pass through compatibility from multiple + // non-overlapping compatible children + PrerollContext* context = preroll_context(); + clip_rsuperellipse_layer->Preroll(context); + EXPECT_EQ(context->renderable_state_flags, Layer::kSaveLayerRenderFlags); + + int opacity_alpha = 0x7F; + DlPoint offset = DlPoint(10, 10); + auto opacity_layer = std::make_shared(opacity_alpha, offset); + opacity_layer->Add(clip_rsuperellipse_layer); + opacity_layer->Preroll(context); + EXPECT_TRUE(opacity_layer->children_can_accept_opacity()); + + DisplayListBuilder expected_builder; + /* OpacityLayer::Paint() */ { + expected_builder.Save(); + { + expected_builder.Translate(offset.x, offset.y); + /* ClipRectLayer::Paint() */ { + expected_builder.Save(); + expected_builder.ClipRoundSuperellipse(clip_rsuperellipse, + DlClipOp::kIntersect, true); + expected_builder.SaveLayer(children_bounds, + &DlPaint().setAlpha(opacity_alpha)); + /* child layer1 paint */ { + expected_builder.DrawPath(path1, DlPaint()); + } + /* child layer2 paint */ { // + expected_builder.DrawPath(path2, DlPaint()); + } + expected_builder.Restore(); + } + } + expected_builder.Restore(); + } + + opacity_layer->Paint(display_list_paint_context()); + EXPECT_TRUE(DisplayListsEQ_Verbose(expected_builder.Build(), display_list())); +} + +TEST_F(ClipRSuperellipseLayerTest, LayerCached) { + auto path1 = DlPath::MakeRectLTRB(10, 10, 30, 30); + DlPaint paint = DlPaint(); + auto mock1 = MockLayer::MakeOpacityCompatible(path1); + DlRect clip_rect = DlRect::MakeWH(500, 500); + DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(clip_rect, 20, 20); + auto layer = std::make_shared( + clip_rsuperellipse, Clip::kAntiAliasWithSaveLayer); + layer->Add(mock1); + + auto initial_transform = DlMatrix::MakeTranslation({50.0, 25.5}); + DlMatrix cache_ctm = initial_transform; + DisplayListBuilder cache_canvas; + cache_canvas.Transform(cache_ctm); + + use_mock_raster_cache(); + preroll_context()->state_stack.set_preroll_delegate(initial_transform); + + const auto* clip_cache_item = layer->raster_cache_item(); + + layer->Preroll(preroll_context()); + LayerTree::TryToRasterCache(cacheable_items(), &paint_context()); + + EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)0); + EXPECT_EQ(clip_cache_item->cache_state(), RasterCacheItem::CacheState::kNone); + + layer->Preroll(preroll_context()); + LayerTree::TryToRasterCache(cacheable_items(), &paint_context()); + EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)0); + EXPECT_EQ(clip_cache_item->cache_state(), RasterCacheItem::CacheState::kNone); + + layer->Preroll(preroll_context()); + LayerTree::TryToRasterCache(cacheable_items(), &paint_context()); + EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)1); + EXPECT_EQ(clip_cache_item->cache_state(), + RasterCacheItem::CacheState::kCurrent); + EXPECT_TRUE(raster_cache()->Draw(clip_cache_item->GetId().value(), + cache_canvas, &paint)); +} + +TEST_F(ClipRSuperellipseLayerTest, NoSaveLayerShouldNotCache) { + auto path1 = DlPath::MakeRectLTRB(10, 10, 30, 30); + + auto mock1 = MockLayer::MakeOpacityCompatible(path1); + DlRect clip_rect = DlRect::MakeWH(500, 500); + DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(clip_rect, 20, 20); + auto layer = std::make_shared(clip_rsuperellipse, + Clip::kAntiAlias); + layer->Add(mock1); + + auto initial_transform = DlMatrix::MakeTranslation({50.0, 25.5}); + + use_mock_raster_cache(); + preroll_context()->state_stack.set_preroll_delegate(initial_transform); + + const auto* clip_cache_item = layer->raster_cache_item(); + + layer->Preroll(preroll_context()); + LayerTree::TryToRasterCache(cacheable_items(), &paint_context()); + + EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)0); + EXPECT_EQ(clip_cache_item->cache_state(), RasterCacheItem::CacheState::kNone); + + layer->Preroll(preroll_context()); + LayerTree::TryToRasterCache(cacheable_items(), &paint_context()); + EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)0); + EXPECT_EQ(clip_cache_item->cache_state(), RasterCacheItem::CacheState::kNone); + + layer->Preroll(preroll_context()); + LayerTree::TryToRasterCache(cacheable_items(), &paint_context()); + EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)0); + EXPECT_EQ(clip_cache_item->cache_state(), RasterCacheItem::CacheState::kNone); +} + +TEST_F(ClipRSuperellipseLayerTest, EmptyClipDoesNotCullPlatformView) { + const DlPoint view_offset = DlPoint(0.0f, 0.0f); + const DlSize view_size = DlSize(8.0f, 8.0f); + const int64_t view_id = 42; + auto platform_view = + std::make_shared(view_offset, view_size, view_id); + + DlRoundSuperellipse clip_rsuperellipse = + DlRoundSuperellipse::MakeRectXY(DlRect(), 20, 20); + auto clip = std::make_shared(clip_rsuperellipse, + Clip::kAntiAlias); + clip->Add(platform_view); + + auto embedder = MockViewEmbedder(); + DisplayListBuilder fake_overlay_builder; + embedder.AddCanvas(&fake_overlay_builder); + preroll_context()->view_embedder = &embedder; + paint_context().view_embedder = &embedder; + + clip->Preroll(preroll_context()); + EXPECT_EQ(embedder.prerolled_views(), std::vector({view_id})); + + clip->Paint(paint_context()); + EXPECT_EQ(embedder.painted_views(), std::vector({view_id})); +} + +} // namespace testing +} // namespace flutter + +// NOLINTEND(bugprone-unchecked-optional-access) diff --git a/engine/src/flutter/flow/layers/layer_state_stack.cc b/engine/src/flutter/flow/layers/layer_state_stack.cc index d9bab44859c..f438c83473d 100644 --- a/engine/src/flutter/flow/layers/layer_state_stack.cc +++ b/engine/src/flutter/flow/layers/layer_state_stack.cc @@ -58,6 +58,9 @@ class DummyDelegate : public LayerStateStack::Delegate { void clipRect(const DlRect& rect, DlClipOp op, bool is_aa) override {} void clipRRect(const DlRoundRect& rrect, DlClipOp op, bool is_aa) override {} + void clipRSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp op, + bool is_aa) override {} void clipPath(const DlPath& path, DlClipOp op, bool is_aa) override {} private: @@ -121,6 +124,11 @@ class DlCanvasDelegate : public LayerStateStack::Delegate { void clipRRect(const DlRoundRect& rrect, DlClipOp op, bool is_aa) override { canvas_->ClipRoundRect(rrect, op, is_aa); } + void clipRSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp op, + bool is_aa) override { + canvas_->ClipRoundSuperellipse(rse, op, is_aa); + } void clipPath(const DlPath& path, DlClipOp op, bool is_aa) override { canvas_->ClipPath(path, op, is_aa); } @@ -176,6 +184,11 @@ class PrerollDelegate : public LayerStateStack::Delegate { void clipRRect(const DlRoundRect& rrect, DlClipOp op, bool is_aa) override { state().clipRRect(rrect, op, is_aa); } + void clipRSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp op, + bool is_aa) override { + state().clipRSuperellipse(rse, op, is_aa); + } void clipPath(const DlPath& path, DlClipOp op, bool is_aa) override { state().clipPath(path, op, is_aa); } @@ -446,6 +459,33 @@ class ClipRRectEntry : public LayerStateStack::StateEntry { FML_DISALLOW_COPY_ASSIGN_AND_MOVE(ClipRRectEntry); }; +class ClipRSuperellipseEntry : public LayerStateStack::StateEntry { + public: + ClipRSuperellipseEntry(const DlRoundSuperellipse& clip_rsuperellipse, + bool is_aa) + : clip_rsuperellipse_(clip_rsuperellipse), is_aa_(is_aa) {} + + void apply(LayerStateStack* stack) const override { + stack->delegate_->clipRSuperellipse(clip_rsuperellipse_, + DlClipOp::kIntersect, is_aa_); + } + void update_mutators(MutatorsStack* mutators_stack) const override { + // MutatorsStack doesn't support non-Skia classes, and therefore this method + // has to use approximate RRect, which might cause trouble for certain + // embedded apps. + // TODO(dkwingsmt): Make this method push a correct ClipRoundedSuperellipse + // mutator. + // https://github.com/flutter/flutter/issues/163716 + mutators_stack->PushClipRRect(ToApproximateSkRRect(clip_rsuperellipse_)); + } + + private: + const DlRoundSuperellipse clip_rsuperellipse_; + const bool is_aa_; + + FML_DISALLOW_COPY_ASSIGN_AND_MOVE(ClipRSuperellipseEntry); +}; + class ClipPathEntry : public LayerStateStack::StateEntry { public: ClipPathEntry(const DlPath& clip_path, bool is_aa) @@ -570,6 +610,13 @@ void MutatorContext::clipRRect(const DlRoundRect& rrect, bool is_aa) { layer_state_stack_->push_clip_rrect(rrect, is_aa); } +void MutatorContext::clipRSuperellipse(const DlRoundSuperellipse& rse, + bool is_aa) { + layer_state_stack_->maybe_save_layer_for_clip(save_needed_); + save_needed_ = false; + layer_state_stack_->push_clip_rsuperellipse(rse, is_aa); +} + void MutatorContext::clipPath(const DlPath& path, bool is_aa) { layer_state_stack_->maybe_save_layer_for_clip(save_needed_); save_needed_ = false; @@ -700,6 +747,13 @@ void LayerStateStack::push_clip_rrect(const DlRoundRect& rrect, bool is_aa) { apply_last_entry(); } +void LayerStateStack::push_clip_rsuperellipse(const DlRoundSuperellipse& rse, + bool is_aa) { + state_stack_.emplace_back( + std::make_unique(rse, is_aa)); + apply_last_entry(); +} + void LayerStateStack::push_clip_path(const DlPath& path, bool is_aa) { state_stack_.emplace_back(std::make_unique(path, is_aa)); apply_last_entry(); diff --git a/engine/src/flutter/flow/layers/layer_state_stack.h b/engine/src/flutter/flow/layers/layer_state_stack.h index d5796e886b8..01e826e7298 100644 --- a/engine/src/flutter/flow/layers/layer_state_stack.h +++ b/engine/src/flutter/flow/layers/layer_state_stack.h @@ -207,6 +207,7 @@ class LayerStateStack { void clipRect(const DlRect& rect, bool is_aa); void clipRRect(const DlRoundRect& rrect, bool is_aa); + void clipRSuperellipse(const DlRoundSuperellipse& rse, bool is_aa); void clipPath(const DlPath& path, bool is_aa); private: @@ -334,6 +335,7 @@ class LayerStateStack { void push_clip_rect(const DlRect& rect, bool is_aa); void push_clip_rrect(const DlRoundRect& rrect, bool is_aa); + void push_clip_rsuperellipse(const DlRoundSuperellipse& rse, bool is_aa); void push_clip_path(const DlPath& path, bool is_aa); // --------------------- @@ -408,6 +410,7 @@ class LayerStateStack { friend class ClipEntry; friend class ClipRectEntry; friend class ClipRRectEntry; + friend class ClipRSuperellipseEntry; friend class ClipPathEntry; class Delegate { @@ -447,6 +450,9 @@ class LayerStateStack { virtual void clipRRect(const DlRoundRect& rrect, DlClipOp op, bool is_aa) = 0; + virtual void clipRSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp op, + bool is_aa) = 0; virtual void clipPath(const DlPath& path, DlClipOp op, bool is_aa) = 0; }; friend class DummyDelegate; diff --git a/engine/src/flutter/impeller/display_list/canvas.cc b/engine/src/flutter/impeller/display_list/canvas.cc index aa7cbd29851..f3f96425f11 100644 --- a/engine/src/flutter/impeller/display_list/canvas.cc +++ b/engine/src/flutter/impeller/display_list/canvas.cc @@ -540,6 +540,29 @@ void Canvas::DrawRoundRect(const RoundRect& round_rect, const Paint& paint) { DrawPath(path, paint); } +void Canvas::DrawRoundSuperellipse(const RoundSuperellipse& rse, + const Paint& paint) { + if (paint.style == Paint::Style::kFill) { + // TODO(dkwingsmt): Investigate if RSE can use the `AttemptDrawBlurredRRect` + // optimization at some point, such as a large enough mask radius. + // https://github.com/flutter/flutter/issues/163893 + Entity entity; + entity.SetTransform(GetCurrentTransform()); + entity.SetBlendMode(paint.blend_mode); + + RoundSuperellipseGeometry geom(rse.GetBounds(), rse.GetRadii()); + AddRenderEntityWithFiltersToCurrentPass(entity, &geom, paint); + return; + } + + auto path = PathBuilder{} + .SetConvexity(Convexity::kConvex) + .AddRoundSuperellipse(rse) + .SetBounds(rse.GetBounds()) + .TakePath(); + DrawPath(path, paint); +} + void Canvas::DrawCircle(const Point& center, Scalar radius, const Paint& paint) { diff --git a/engine/src/flutter/impeller/display_list/canvas.h b/engine/src/flutter/impeller/display_list/canvas.h index fe3d3b1b7ae..08273caca03 100644 --- a/engine/src/flutter/impeller/display_list/canvas.h +++ b/engine/src/flutter/impeller/display_list/canvas.h @@ -201,6 +201,8 @@ class Canvas { void DrawRoundRect(const RoundRect& rect, const Paint& paint); + void DrawRoundSuperellipse(const RoundSuperellipse& rse, const Paint& paint); + void DrawCircle(const Point& center, Scalar radius, const Paint& paint); void DrawPoints(const Point points[], diff --git a/engine/src/flutter/impeller/display_list/dl_dispatcher.cc b/engine/src/flutter/impeller/display_list/dl_dispatcher.cc index 036b7e361d1..660737ca0f7 100644 --- a/engine/src/flutter/impeller/display_list/dl_dispatcher.cc +++ b/engine/src/flutter/impeller/display_list/dl_dispatcher.cc @@ -29,6 +29,7 @@ #include "impeller/entity/geometry/fill_path_geometry.h" #include "impeller/entity/geometry/rect_geometry.h" #include "impeller/entity/geometry/round_rect_geometry.h" +#include "impeller/entity/geometry/round_superellipse_geometry.h" #include "impeller/geometry/color.h" #include "impeller/geometry/path.h" #include "impeller/geometry/path_builder.h" @@ -468,6 +469,25 @@ void DlDispatcherBase::clipRoundRect(const DlRoundRect& rrect, } } +// |flutter::DlOpReceiver| +void DlDispatcherBase::clipRoundSuperellipse(const DlRoundSuperellipse& rse, + flutter::DlClipOp sk_op, + bool is_aa) { + AUTO_DEPTH_WATCHER(0u); + + auto clip_op = ToClipOperation(sk_op); + if (rse.IsRect()) { + RectGeometry geom(rse.GetBounds()); + GetCanvas().ClipGeometry(geom, clip_op, /*is_aa=*/is_aa); + } else if (rse.IsOval()) { + EllipseGeometry geom(rse.GetBounds()); + GetCanvas().ClipGeometry(geom, clip_op); + } else { + RoundSuperellipseGeometry geom(rse.GetBounds(), rse.GetRadii()); + GetCanvas().ClipGeometry(geom, clip_op); + } +} + // |flutter::DlOpReceiver| void DlDispatcherBase::clipPath(const DlPath& path, flutter::DlClipOp sk_op, @@ -603,6 +623,13 @@ void DlDispatcherBase::drawDiffRoundRect(const DlRoundRect& outer, GetCanvas().DrawPath(builder.TakePath(FillType::kOdd), paint_); } +// |flutter::DlOpReceiver| +void DlDispatcherBase::drawRoundSuperellipse(const DlRoundSuperellipse& rse) { + AUTO_DEPTH_WATCHER(1u); + + GetCanvas().DrawRoundSuperellipse(rse, paint_); +} + // |flutter::DlOpReceiver| void DlDispatcherBase::drawPath(const DlPath& path) { AUTO_DEPTH_WATCHER(1u); diff --git a/engine/src/flutter/impeller/display_list/dl_dispatcher.h b/engine/src/flutter/impeller/display_list/dl_dispatcher.h index f2dc195b445..4af1e339119 100644 --- a/engine/src/flutter/impeller/display_list/dl_dispatcher.h +++ b/engine/src/flutter/impeller/display_list/dl_dispatcher.h @@ -25,6 +25,7 @@ using DlPoint = flutter::DlPoint; using DlRect = flutter::DlRect; using DlIRect = flutter::DlIRect; using DlRoundRect = flutter::DlRoundRect; +using DlRoundSuperellipse = flutter::DlRoundSuperellipse; using DlPath = flutter::DlPath; class DlDispatcherBase : public flutter::DlOpReceiver { @@ -138,6 +139,11 @@ class DlDispatcherBase : public flutter::DlOpReceiver { flutter::DlClipOp clip_op, bool is_aa) override; + // |flutter::DlOpReceiver| + void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + flutter::DlClipOp clip_op, + bool is_aa) override; + // |flutter::DlOpReceiver| void clipPath(const DlPath& path, flutter::DlClipOp clip_op, @@ -174,6 +180,9 @@ class DlDispatcherBase : public flutter::DlOpReceiver { void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override; + // |flutter::DlOpReceiver| + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override; + // |flutter::DlOpReceiver| void drawPath(const DlPath& path) override; diff --git a/engine/src/flutter/impeller/entity/geometry/geometry.cc b/engine/src/flutter/impeller/entity/geometry/geometry.cc index 0f3185fce94..b259f1820be 100644 --- a/engine/src/flutter/impeller/entity/geometry/geometry.cc +++ b/engine/src/flutter/impeller/entity/geometry/geometry.cc @@ -15,6 +15,7 @@ #include "impeller/entity/geometry/line_geometry.h" #include "impeller/entity/geometry/rect_geometry.h" #include "impeller/entity/geometry/round_rect_geometry.h" +#include "impeller/entity/geometry/round_superellipse_geometry.h" #include "impeller/entity/geometry/stroke_path_geometry.h" #include "impeller/geometry/rect.h" @@ -110,6 +111,12 @@ std::unique_ptr Geometry::MakeRoundRect(const Rect& rect, return std::make_unique(rect, radii); } +std::unique_ptr Geometry::MakeRoundSuperellipse( + const Rect& rect, + Scalar corner_radius) { + return std::make_unique(rect, corner_radius); +} + bool Geometry::CoversArea(const Matrix& transform, const Rect& rect) const { return false; } diff --git a/engine/src/flutter/impeller/entity/geometry/geometry.h b/engine/src/flutter/impeller/entity/geometry/geometry.h index d5c504d79e8..257c44d3aeb 100644 --- a/engine/src/flutter/impeller/entity/geometry/geometry.h +++ b/engine/src/flutter/impeller/entity/geometry/geometry.h @@ -83,6 +83,9 @@ class Geometry { static std::unique_ptr MakeRoundRect(const Rect& rect, const Size& radii); + static std::unique_ptr MakeRoundSuperellipse(const Rect& rect, + Scalar corner_radius); + virtual GeometryResult GetPositionBuffer(const ContentContext& renderer, const Entity& entity, RenderPass& pass) const = 0; diff --git a/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h b/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h index 5df22cd29ee..9cb17474961 100644 --- a/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h +++ b/engine/src/flutter/impeller/entity/geometry/round_superellipse_geometry.h @@ -12,14 +12,12 @@ namespace impeller { /// Geometry class that can generate vertices for a rounded superellipse. /// -/// A superellipse is an ellipse-like shape that is defined by the parameters N, -/// alpha, and beta: -/// -/// 1 = |x / b| ^n + |y / a| ^n -/// -/// A rounded superellipse is a square-like superellipse (a=b) with its four -/// corners replaced by circular arcs. It replicates the `RoundedRectangle` -/// shape in SwiftUI with corner style `.continuous`. +/// A rounded superellipse is a shape similar to a typical rounded rectangle +/// (`RoundRect`), but with smoother transitions between the straight sides and +/// the rounded corners. It resembles the `RoundedRectangle` shape in SwiftUI +/// with the `.continuous` corner style. Technically, it is created by replacing +/// the four corners of a superellipse (also known as a Lamé curve) with +/// circular arcs. /// /// The `bounds` defines the position and size of the shape. The `corner_radius` /// corresponds to SwiftUI's `cornerRadius` parameter, which is close to, but diff --git a/engine/src/flutter/impeller/geometry/round_superellipse.cc b/engine/src/flutter/impeller/geometry/round_superellipse.cc index 8c74c35fc4a..b47533c84b4 100644 --- a/engine/src/flutter/impeller/geometry/round_superellipse.cc +++ b/engine/src/flutter/impeller/geometry/round_superellipse.cc @@ -4,6 +4,7 @@ #include "flutter/impeller/geometry/round_superellipse.h" +#include "flutter/impeller/geometry/round_rect.h" #include "flutter/impeller/geometry/round_superellipse_param.h" namespace impeller { @@ -30,4 +31,11 @@ RoundSuperellipse RoundSuperellipse::MakeRectRadii( return param.Contains(p); } +RoundRect RoundSuperellipse::ToApproximateRoundRect() const { + // Experiments have shown that using the same corner radii for the RRect + // provides an approximation that is close to optimal, as achieving a perfect + // match is not feasible. + return RoundRect::MakeRectRadii(GetBounds(), GetRadii()); +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/geometry/round_superellipse.h b/engine/src/flutter/impeller/geometry/round_superellipse.h index d18044b192f..12fe74a4130 100644 --- a/engine/src/flutter/impeller/geometry/round_superellipse.h +++ b/engine/src/flutter/impeller/geometry/round_superellipse.h @@ -12,6 +12,8 @@ namespace impeller { +struct RoundRect; + struct RoundSuperellipse { RoundSuperellipse() = default; @@ -124,6 +126,14 @@ struct RoundSuperellipse { return !(*this == r); } + // Approximates a rounded superellipse with a round rectangle to the + // best practical accuracy. + // + // This is used for Skia backends, which does not support rounded + // superellipses directly, so rendering rounded superellipses + // falls back to RRect. + [[nodiscard]] RoundRect ToApproximateRoundRect() const; + private: constexpr RoundSuperellipse(const Rect& bounds, const RoundingRadii& radii) : bounds_(bounds), radii_(radii) {} diff --git a/engine/src/flutter/lib/ui/BUILD.gn b/engine/src/flutter/lib/ui/BUILD.gn index 87d99beef86..e63038b9bf4 100644 --- a/engine/src/flutter/lib/ui/BUILD.gn +++ b/engine/src/flutter/lib/ui/BUILD.gn @@ -103,6 +103,8 @@ source_set("ui") { "painting/picture_recorder.h", "painting/rrect.cc", "painting/rrect.h", + "painting/rsuperellipse.cc", + "painting/rsuperellipse.h", "painting/shader.cc", "painting/shader.h", "painting/single_frame_codec.cc", diff --git a/engine/src/flutter/lib/ui/compositing.dart b/engine/src/flutter/lib/ui/compositing.dart index 196291448de..4701a154774 100644 --- a/engine/src/flutter/lib/ui/compositing.dart +++ b/engine/src/flutter/lib/ui/compositing.dart @@ -174,6 +174,15 @@ class ClipRRectEngineLayer extends _EngineLayerWrapper { ClipRRectEngineLayer._(super.nativeLayer) : super._(); } +/// An opaque handle to a clip rounded superellipse engine layer. +/// +/// Instances of this class are created by [SceneBuilder.pushClipRSuperellipse]. +/// +/// {@macro dart.ui.sceneBuilder.oldLayerCompatibility} +class ClipRSuperellipseEngineLayer extends _EngineLayerWrapper { + ClipRSuperellipseEngineLayer._(super.nativeLayer) : super._(); +} + /// An opaque handle to a clip path engine layer. /// /// Instances of this class are created by [SceneBuilder.pushClipPath]. @@ -325,6 +334,22 @@ abstract class SceneBuilder { ClipRRectEngineLayer? oldLayer, }); + /// Pushes a rounded-superellipse clip operation onto the operation stack. + /// + /// Rasterization outside the given rounded superellipse is discarded. + /// + /// {@macro dart.ui.sceneBuilder.oldLayer} + /// + /// {@macro dart.ui.sceneBuilder.oldLayerVsRetained} + /// + /// See [pop] for details about the operation stack, and [Clip] for different clip modes. + /// By default, the clip will be anti-aliased (clip = [Clip.antiAlias]). + ClipRSuperellipseEngineLayer pushClipRSuperellipse( + RSuperellipse rse, { + Clip clipBehavior = Clip.antiAlias, + ClipRSuperellipseEngineLayer? oldLayer, + }); + /// Pushes a path clip operation onto the operation stack. /// /// Rasterization outside the given path is discarded. @@ -720,6 +745,36 @@ base class _NativeSceneBuilder extends NativeFieldWrapperClass1 implements Scene EngineLayer? oldLayer, ); + @override + ClipRSuperellipseEngineLayer pushClipRSuperellipse( + RSuperellipse rse, { + Clip clipBehavior = Clip.antiAlias, + ClipRSuperellipseEngineLayer? oldLayer, + }) { + assert(clipBehavior != Clip.none); + assert(_debugCheckCanBeUsedAsOldLayer(oldLayer, 'pushClipRSuperellipse')); + final EngineLayer engineLayer = _NativeEngineLayer._(); + _pushClipRSuperellipse( + engineLayer, + rse._getValue32(), + clipBehavior.index, + oldLayer?._nativeLayer, + ); + final ClipRSuperellipseEngineLayer layer = ClipRSuperellipseEngineLayer._(engineLayer); + assert(_debugPushLayer(layer)); + return layer; + } + + @Native, Handle, Handle, Int32, Handle)>( + symbol: 'SceneBuilder::pushClipRSuperellipse', + ) + external void _pushClipRSuperellipse( + EngineLayer layer, + Float32List rrect, + int clipBehavior, + EngineLayer? oldLayer, + ); + @override ClipPathEngineLayer pushClipPath( Path path, { diff --git a/engine/src/flutter/lib/ui/compositing/scene_builder.cc b/engine/src/flutter/lib/ui/compositing/scene_builder.cc index 656c94f507b..0c165ec1b05 100644 --- a/engine/src/flutter/lib/ui/compositing/scene_builder.cc +++ b/engine/src/flutter/lib/ui/compositing/scene_builder.cc @@ -10,6 +10,7 @@ #include "flutter/flow/layers/clip_path_layer.h" #include "flutter/flow/layers/clip_rect_layer.h" #include "flutter/flow/layers/clip_rrect_layer.h" +#include "flutter/flow/layers/clip_rsuperellipse_layer.h" #include "flutter/flow/layers/color_filter_layer.h" #include "flutter/flow/layers/container_layer.h" #include "flutter/flow/layers/display_list_layer.h" @@ -101,6 +102,21 @@ void SceneBuilder::pushClipRRect(Dart_Handle layer_handle, } } +void SceneBuilder::pushClipRSuperellipse( + Dart_Handle layer_handle, + const RSuperellipse& rse, + int clip_behavior, + const fml::RefPtr& old_layer) { + auto layer = std::make_shared( + rse.rsuperellipse, static_cast(clip_behavior)); + PushLayer(layer); + EngineLayer::MakeRetained(layer_handle, layer); + + if (old_layer && old_layer->Layer()) { + layer->AssignOldLayer(old_layer->Layer().get()); + } +} + void SceneBuilder::pushClipPath(Dart_Handle layer_handle, const CanvasPath* path, int clip_behavior, diff --git a/engine/src/flutter/lib/ui/compositing/scene_builder.h b/engine/src/flutter/lib/ui/compositing/scene_builder.h index 717129a6fd2..55726665c50 100644 --- a/engine/src/flutter/lib/ui/compositing/scene_builder.h +++ b/engine/src/flutter/lib/ui/compositing/scene_builder.h @@ -17,6 +17,7 @@ #include "flutter/lib/ui/painting/path.h" #include "flutter/lib/ui/painting/picture.h" #include "flutter/lib/ui/painting/rrect.h" +#include "flutter/lib/ui/painting/rsuperellipse.h" #include "flutter/lib/ui/painting/shader.h" #include "third_party/tonic/typed_data/typed_list.h" @@ -59,6 +60,10 @@ class SceneBuilder : public RefCountedDartWrappable { const RRect& rrect, int clip_behavior, const fml::RefPtr& old_layer); + void pushClipRSuperellipse(Dart_Handle layer_handle, + const RSuperellipse& rse, + int clip_behavior, + const fml::RefPtr& old_layer); void pushClipPath(Dart_Handle layer_handle, const CanvasPath* path, int clip_behavior, diff --git a/engine/src/flutter/lib/ui/dart_ui.cc b/engine/src/flutter/lib/ui/dart_ui.cc index d8509e37e3a..a47bf094c42 100644 --- a/engine/src/flutter/lib/ui/dart_ui.cc +++ b/engine/src/flutter/lib/ui/dart_ui.cc @@ -138,11 +138,13 @@ typedef CanvasPath Path; V(Canvas, clipPath) \ V(Canvas, clipRect) \ V(Canvas, clipRRect) \ + V(Canvas, clipRSuperellipse) \ V(Canvas, drawArc) \ V(Canvas, drawAtlas) \ V(Canvas, drawCircle) \ V(Canvas, drawColor) \ V(Canvas, drawDRRect) \ + V(Canvas, drawRSuperellipse) \ V(Canvas, drawImage) \ V(Canvas, drawImageNine) \ V(Canvas, drawImageRect) \ @@ -288,8 +290,9 @@ typedef CanvasPath Path; V(SceneBuilder, pop) \ V(SceneBuilder, pushBackdropFilter) \ V(SceneBuilder, pushClipPath) \ - V(SceneBuilder, pushClipRRect) \ V(SceneBuilder, pushClipRect) \ + V(SceneBuilder, pushClipRRect) \ + V(SceneBuilder, pushClipRSuperellipse) \ V(SceneBuilder, pushColorFilter) \ V(SceneBuilder, pushImageFilter) \ V(SceneBuilder, pushOffset) \ diff --git a/engine/src/flutter/lib/ui/geometry.dart b/engine/src/flutter/lib/ui/geometry.dart index 5d9a7cb695a..ae30eace464 100644 --- a/engine/src/flutter/lib/ui/geometry.dart +++ b/engine/src/flutter/lib/ui/geometry.dart @@ -1093,162 +1093,21 @@ class Radius { } } -/// An immutable rounded rectangle with the custom radii for all four corners. -class RRect { - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and the same radii along its horizontal axis and its vertical axis. - /// - /// Will assert in debug mode if `radiusX` or `radiusY` are negative. - const RRect.fromLTRBXY( - double left, - double top, - double right, - double bottom, - double radiusX, - double radiusY, - ) : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: radiusX, - tlRadiusY: radiusY, - trRadiusX: radiusX, - trRadiusY: radiusY, - blRadiusX: radiusX, - blRadiusY: radiusY, - brRadiusX: radiusX, - brRadiusY: radiusY, - ); - - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and the same radius in each corner. - /// - /// Will assert in debug mode if the `radius` is negative in either x or y. - RRect.fromLTRBR(double left, double top, double right, double bottom, Radius radius) - : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: radius.x, - tlRadiusY: radius.y, - trRadiusX: radius.x, - trRadiusY: radius.y, - blRadiusX: radius.x, - blRadiusY: radius.y, - brRadiusX: radius.x, - brRadiusY: radius.y, - ); - - /// Construct a rounded rectangle from its bounding box and the same radii - /// along its horizontal axis and its vertical axis. - /// - /// Will assert in debug mode if `radiusX` or `radiusY` are negative. - RRect.fromRectXY(Rect rect, double radiusX, double radiusY) - : this._raw( - top: rect.top, - left: rect.left, - right: rect.right, - bottom: rect.bottom, - tlRadiusX: radiusX, - tlRadiusY: radiusY, - trRadiusX: radiusX, - trRadiusY: radiusY, - blRadiusX: radiusX, - blRadiusY: radiusY, - brRadiusX: radiusX, - brRadiusY: radiusY, - ); - - /// Construct a rounded rectangle from its bounding box and a radius that is - /// the same in each corner. - /// - /// Will assert in debug mode if the `radius` is negative in either x or y. - RRect.fromRectAndRadius(Rect rect, Radius radius) - : this._raw( - top: rect.top, - left: rect.left, - right: rect.right, - bottom: rect.bottom, - tlRadiusX: radius.x, - tlRadiusY: radius.y, - trRadiusX: radius.x, - trRadiusY: radius.y, - blRadiusX: radius.x, - blRadiusY: radius.y, - brRadiusX: radius.x, - brRadiusY: radius.y, - ); - - /// Construct a rounded rectangle from its left, top, right, and bottom edges, - /// and topLeft, topRight, bottomRight, and bottomLeft radii. - /// - /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will - /// assert in debug mode if any of the radii are negative in either x or y. - RRect.fromLTRBAndCorners( - double left, - double top, - double right, - double bottom, { - Radius topLeft = Radius.zero, - Radius topRight = Radius.zero, - Radius bottomRight = Radius.zero, - Radius bottomLeft = Radius.zero, - }) : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: topLeft.x, - tlRadiusY: topLeft.y, - trRadiusX: topRight.x, - trRadiusY: topRight.y, - blRadiusX: bottomLeft.x, - blRadiusY: bottomLeft.y, - brRadiusX: bottomRight.x, - brRadiusY: bottomRight.y, - ); - - /// Construct a rounded rectangle from its bounding box and topLeft, - /// topRight, bottomRight, and bottomLeft radii. - /// - /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will - /// assert in debug mode if any of the radii are negative in either x or y. - RRect.fromRectAndCorners( - Rect rect, { - Radius topLeft = Radius.zero, - Radius topRight = Radius.zero, - Radius bottomRight = Radius.zero, - Radius bottomLeft = Radius.zero, - }) : this._raw( - top: rect.top, - left: rect.left, - right: rect.right, - bottom: rect.bottom, - tlRadiusX: topLeft.x, - tlRadiusY: topLeft.y, - trRadiusX: topRight.x, - trRadiusY: topRight.y, - blRadiusX: bottomLeft.x, - blRadiusY: bottomLeft.y, - brRadiusX: bottomRight.x, - brRadiusY: bottomRight.y, - ); - - const RRect._raw({ - this.left = 0.0, - this.top = 0.0, - this.right = 0.0, - this.bottom = 0.0, - this.tlRadiusX = 0.0, - this.tlRadiusY = 0.0, - this.trRadiusX = 0.0, - this.trRadiusY = 0.0, - this.brRadiusX = 0.0, - this.brRadiusY = 0.0, - this.blRadiusX = 0.0, - this.blRadiusY = 0.0, +// The common base class for `RRect` and `RSuperellipse`. +abstract class _RRectLike> { + const _RRectLike({ + required this.left, + required this.top, + required this.right, + required this.bottom, + required this.tlRadiusX, + required this.tlRadiusY, + required this.trRadiusX, + required this.trRadiusY, + required this.brRadiusX, + required this.brRadiusY, + required this.blRadiusX, + required this.blRadiusY, }) : assert(tlRadiusX >= 0), assert(tlRadiusY >= 0), assert(trRadiusX >= 0), @@ -1258,6 +1117,25 @@ class RRect { assert(blRadiusX >= 0), assert(blRadiusY >= 0); + // Implemented by a subclass to return an object constructed with the given + // parameters. + // + // Used by various methods that construct an object of the same shape. + T _create({ + required double left, + required double top, + required double right, + required double bottom, + required double tlRadiusX, + required double tlRadiusY, + required double trRadiusX, + required double trRadiusY, + required double brRadiusX, + required double brRadiusY, + required double blRadiusX, + required double blRadiusY, + }); + Float32List _getValue32() { final Float32List result = Float32List(12); result[0] = left; @@ -1323,12 +1201,9 @@ class RRect { /// The bottom-left [Radius]. Radius get blRadius => Radius.elliptical(blRadiusX, blRadiusY); - /// A rounded rectangle with all the values set to zero. - static const RRect zero = RRect._raw(); - - /// Returns a new [RRect] translated by the given offset. - RRect shift(Offset offset) { - return RRect._raw( + /// Returns a clone translated by the given offset. + T shift(Offset offset) { + return _create( left: left + offset.dx, top: top + offset.dy, right: right + offset.dx, @@ -1344,10 +1219,10 @@ class RRect { ); } - /// Returns a new [RRect] with edges and radii moved outwards by the given + /// Returns a clone with edges and radii moved outwards by the given /// delta. - RRect inflate(double delta) { - return RRect._raw( + T inflate(double delta) { + return _create( left: left - delta, top: top - delta, right: right + delta, @@ -1363,8 +1238,8 @@ class RRect { ); } - /// Returns a new [RRect] with edges and radii moved inwards by the given delta. - RRect deflate(double delta) => inflate(-delta); + /// Returns a clone with edges and radii moved inwards by the given delta. + T deflate(double delta) => inflate(-delta); /// The distance between the left and right edges of this rectangle. double get width => right - left; @@ -1516,7 +1391,7 @@ class RRect { /// /// See the [Skia scaling implementation](https://github.com/google/skia/blob/main/src/core/SkRRect.cpp) /// for more details. - RRect scaleRadii() { + T scaleRadii() { double scale = 1.0; scale = _getMin(scale, blRadiusY, tlRadiusY, height); scale = _getMin(scale, tlRadiusX, trRadiusX, width); @@ -1525,7 +1400,7 @@ class RRect { assert(scale >= 0); if (scale < 1.0) { - return RRect._raw( + return _create( top: top, left: left, right: right, @@ -1541,7 +1416,7 @@ class RRect { ); } - return RRect._raw( + return _create( top: top, left: left, right: right, @@ -1557,127 +1432,40 @@ class RRect { ); } - /// Whether the point specified by the given offset (which is assumed to be - /// relative to the origin) lies inside the rounded rectangle. - /// - /// This method may allocate (and cache) a copy of the object with normalized - /// radii the first time it is called on a particular [RRect] instance. When - /// using this method, prefer to reuse existing [RRect]s rather than - /// recreating the object each time. - bool contains(Offset point) { - if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) { - return false; - } // outside bounding box - - final RRect scaled = scaleRadii(); - - double x; - double y; - double radiusX; - double radiusY; - // check whether point is in one of the rounded corner areas - // x, y -> translate to ellipse center - if (point.dx < left + scaled.tlRadiusX && point.dy < top + scaled.tlRadiusY) { - x = point.dx - left - scaled.tlRadiusX; - y = point.dy - top - scaled.tlRadiusY; - radiusX = scaled.tlRadiusX; - radiusY = scaled.tlRadiusY; - } else if (point.dx > right - scaled.trRadiusX && point.dy < top + scaled.trRadiusY) { - x = point.dx - right + scaled.trRadiusX; - y = point.dy - top - scaled.trRadiusY; - radiusX = scaled.trRadiusX; - radiusY = scaled.trRadiusY; - } else if (point.dx > right - scaled.brRadiusX && point.dy > bottom - scaled.brRadiusY) { - x = point.dx - right + scaled.brRadiusX; - y = point.dy - bottom + scaled.brRadiusY; - radiusX = scaled.brRadiusX; - radiusY = scaled.brRadiusY; - } else if (point.dx < left + scaled.blRadiusX && point.dy > bottom - scaled.blRadiusY) { - x = point.dx - left - scaled.blRadiusX; - y = point.dy - bottom + scaled.blRadiusY; - radiusX = scaled.blRadiusX; - radiusY = scaled.blRadiusY; - } else { - return true; // inside and not within the rounded corner area - } - - x = x / radiusX; - y = y / radiusY; - // check if the point is outside the unit circle - if (x * x + y * y > 1.0) { - return false; - } - return true; - } - - /// Linearly interpolate between two rounded rectangles. - /// - /// If either is null, this function substitutes [RRect.zero] instead. - /// - /// The `t` argument represents position on the timeline, with 0.0 meaning - /// that the interpolation has not started, returning `a` (or something - /// equivalent to `a`), 1.0 meaning that the interpolation has finished, - /// returning `b` (or something equivalent to `b`), and values in between - /// meaning that the interpolation is at the relevant point on the timeline - /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and - /// 1.0, so negative values and values greater than 1.0 are valid (and can - /// easily be generated by curves such as [Curves.elasticInOut]). - /// - /// Values for `t` are usually obtained from an [Animation], such as - /// an [AnimationController]. - static RRect? lerp(RRect? a, RRect? b, double t) { + // Linearly interpolate between this object and another of the same shape. + T _lerpTo(T? b, double t) { + assert(runtimeType == T); if (b == null) { - if (a == null) { - return null; - } else { - final double k = 1.0 - t; - return RRect._raw( - left: a.left * k, - top: a.top * k, - right: a.right * k, - bottom: a.bottom * k, - tlRadiusX: math.max(0, a.tlRadiusX * k), - tlRadiusY: math.max(0, a.tlRadiusY * k), - trRadiusX: math.max(0, a.trRadiusX * k), - trRadiusY: math.max(0, a.trRadiusY * k), - brRadiusX: math.max(0, a.brRadiusX * k), - brRadiusY: math.max(0, a.brRadiusY * k), - blRadiusX: math.max(0, a.blRadiusX * k), - blRadiusY: math.max(0, a.blRadiusY * k), - ); - } + final double k = 1.0 - t; + return _create( + left: left * k, + top: top * k, + right: right * k, + bottom: bottom * k, + tlRadiusX: math.max(0, tlRadiusX * k), + tlRadiusY: math.max(0, tlRadiusY * k), + trRadiusX: math.max(0, trRadiusX * k), + trRadiusY: math.max(0, trRadiusY * k), + brRadiusX: math.max(0, brRadiusX * k), + brRadiusY: math.max(0, brRadiusY * k), + blRadiusX: math.max(0, blRadiusX * k), + blRadiusY: math.max(0, blRadiusY * k), + ); } else { - if (a == null) { - return RRect._raw( - left: b.left * t, - top: b.top * t, - right: b.right * t, - bottom: b.bottom * t, - tlRadiusX: math.max(0, b.tlRadiusX * t), - tlRadiusY: math.max(0, b.tlRadiusY * t), - trRadiusX: math.max(0, b.trRadiusX * t), - trRadiusY: math.max(0, b.trRadiusY * t), - brRadiusX: math.max(0, b.brRadiusX * t), - brRadiusY: math.max(0, b.brRadiusY * t), - blRadiusX: math.max(0, b.blRadiusX * t), - blRadiusY: math.max(0, b.blRadiusY * t), - ); - } else { - return RRect._raw( - left: _lerpDouble(a.left, b.left, t), - top: _lerpDouble(a.top, b.top, t), - right: _lerpDouble(a.right, b.right, t), - bottom: _lerpDouble(a.bottom, b.bottom, t), - tlRadiusX: math.max(0, _lerpDouble(a.tlRadiusX, b.tlRadiusX, t)), - tlRadiusY: math.max(0, _lerpDouble(a.tlRadiusY, b.tlRadiusY, t)), - trRadiusX: math.max(0, _lerpDouble(a.trRadiusX, b.trRadiusX, t)), - trRadiusY: math.max(0, _lerpDouble(a.trRadiusY, b.trRadiusY, t)), - brRadiusX: math.max(0, _lerpDouble(a.brRadiusX, b.brRadiusX, t)), - brRadiusY: math.max(0, _lerpDouble(a.brRadiusY, b.brRadiusY, t)), - blRadiusX: math.max(0, _lerpDouble(a.blRadiusX, b.blRadiusX, t)), - blRadiusY: math.max(0, _lerpDouble(a.blRadiusY, b.blRadiusY, t)), - ); - } + return _create( + left: _lerpDouble(left, b.left, t), + top: _lerpDouble(top, b.top, t), + right: _lerpDouble(right, b.right, t), + bottom: _lerpDouble(bottom, b.bottom, t), + tlRadiusX: math.max(0, _lerpDouble(tlRadiusX, b.tlRadiusX, t)), + tlRadiusY: math.max(0, _lerpDouble(tlRadiusY, b.tlRadiusY, t)), + trRadiusX: math.max(0, _lerpDouble(trRadiusX, b.trRadiusX, t)), + trRadiusY: math.max(0, _lerpDouble(trRadiusY, b.trRadiusY, t)), + brRadiusX: math.max(0, _lerpDouble(brRadiusX, b.brRadiusX, t)), + brRadiusY: math.max(0, _lerpDouble(brRadiusY, b.brRadiusY, t)), + blRadiusX: math.max(0, _lerpDouble(blRadiusX, b.blRadiusX, t)), + blRadiusY: math.max(0, _lerpDouble(blRadiusY, b.blRadiusY, t)), + ); } } @@ -1689,7 +1477,7 @@ class RRect { if (runtimeType != other.runtimeType) { return false; } - return other is RRect && + return other is _RRectLike && other.left == left && other.top == top && other.right == right && @@ -1720,8 +1508,7 @@ class RRect { brRadiusY, ); - @override - String toString() { + String _toString({required String className}) { final String rect = '${left.toStringAsFixed(1)}, ' '${top.toStringAsFixed(1)}, ' @@ -1729,11 +1516,11 @@ class RRect { '${bottom.toStringAsFixed(1)}'; if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) { if (tlRadius.x == tlRadius.y) { - return 'RRect.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})'; + return '$className.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})'; } - return 'RRect.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})'; + return '$className.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})'; } - return 'RRect.fromLTRBAndCorners(' + return '$className.fromLTRBAndCorners(' '$rect, ' 'topLeft: $tlRadius, ' 'topRight: $trRadius, ' @@ -1743,6 +1530,516 @@ class RRect { } } +/// An immutable rounded rectangle with the custom radii for all four corners. +class RRect extends _RRectLike { + /// Construct a rounded rectangle from its left, top, right, and bottom edges, + /// and the same radii along its horizontal axis and its vertical axis. + /// + /// Will assert in debug mode if `radiusX` or `radiusY` are negative. + const RRect.fromLTRBXY( + double left, + double top, + double right, + double bottom, + double radiusX, + double radiusY, + ) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + ); + + /// Construct a rounded rectangle from its left, top, right, and bottom edges, + /// and the same radius in each corner. + /// + /// Will assert in debug mode if the `radius` is negative in either x or y. + RRect.fromLTRBR(double left, double top, double right, double bottom, Radius radius) + : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + ); + + /// Construct a rounded rectangle from its bounding box and the same radii + /// along its horizontal axis and its vertical axis. + /// + /// Will assert in debug mode if `radiusX` or `radiusY` are negative. + RRect.fromRectXY(Rect rect, double radiusX, double radiusY) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + ); + + /// Construct a rounded rectangle from its bounding box and a radius that is + /// the same in each corner. + /// + /// Will assert in debug mode if the `radius` is negative in either x or y. + RRect.fromRectAndRadius(Rect rect, Radius radius) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + ); + + /// Construct a rounded rectangle from its left, top, right, and bottom edges, + /// and topLeft, topRight, bottomRight, and bottomLeft radii. + /// + /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will + /// assert in debug mode if any of the radii are negative in either x or y. + RRect.fromLTRBAndCorners( + double left, + double top, + double right, + double bottom, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + ); + + /// Construct a rounded rectangle from its bounding box and topLeft, + /// topRight, bottomRight, and bottomLeft radii. + /// + /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will + /// assert in debug mode if any of the radii are negative in either x or y. + RRect.fromRectAndCorners( + Rect rect, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + ); + + const RRect._raw({ + super.left = 0.0, + super.top = 0.0, + super.right = 0.0, + super.bottom = 0.0, + super.tlRadiusX = 0.0, + super.tlRadiusY = 0.0, + super.trRadiusX = 0.0, + super.trRadiusY = 0.0, + super.brRadiusX = 0.0, + super.brRadiusY = 0.0, + super.blRadiusX = 0.0, + super.blRadiusY = 0.0, + }); + + @override + RRect _create({ + required double left, + required double top, + required double right, + required double bottom, + required double tlRadiusX, + required double tlRadiusY, + required double trRadiusX, + required double trRadiusY, + required double brRadiusX, + required double brRadiusY, + required double blRadiusX, + required double blRadiusY, + }) => RRect._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: tlRadiusX, + tlRadiusY: tlRadiusY, + trRadiusX: trRadiusX, + trRadiusY: trRadiusY, + blRadiusX: blRadiusX, + blRadiusY: blRadiusY, + brRadiusX: brRadiusX, + brRadiusY: brRadiusY, + ); + + /// A rounded rectangle with all the values set to zero. + static const RRect zero = RRect._raw(); + + /// Whether the point specified by the given offset (which is assumed to be + /// relative to the origin) lies inside the rounded rectangle. + /// + /// This method may allocate (and cache) a copy of the object with normalized + /// radii the first time it is called on a particular [RRect] instance. When + /// using this method, prefer to reuse existing [RRect]s rather than + /// recreating the object each time. + bool contains(Offset point) { + if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) { + return false; + } // outside bounding box + + final RRect scaled = scaleRadii(); + + double x; + double y; + double radiusX; + double radiusY; + // check whether point is in one of the rounded corner areas + // x, y -> translate to ellipse center + if (point.dx < left + scaled.tlRadiusX && point.dy < top + scaled.tlRadiusY) { + x = point.dx - left - scaled.tlRadiusX; + y = point.dy - top - scaled.tlRadiusY; + radiusX = scaled.tlRadiusX; + radiusY = scaled.tlRadiusY; + } else if (point.dx > right - scaled.trRadiusX && point.dy < top + scaled.trRadiusY) { + x = point.dx - right + scaled.trRadiusX; + y = point.dy - top - scaled.trRadiusY; + radiusX = scaled.trRadiusX; + radiusY = scaled.trRadiusY; + } else if (point.dx > right - scaled.brRadiusX && point.dy > bottom - scaled.brRadiusY) { + x = point.dx - right + scaled.brRadiusX; + y = point.dy - bottom + scaled.brRadiusY; + radiusX = scaled.brRadiusX; + radiusY = scaled.brRadiusY; + } else if (point.dx < left + scaled.blRadiusX && point.dy > bottom - scaled.blRadiusY) { + x = point.dx - left - scaled.blRadiusX; + y = point.dy - bottom + scaled.blRadiusY; + radiusX = scaled.blRadiusX; + radiusY = scaled.blRadiusY; + } else { + return true; // inside and not within the rounded corner area + } + + x = x / radiusX; + y = y / radiusY; + // check if the point is outside the unit circle + if (x * x + y * y > 1.0) { + return false; + } + return true; + } + + /// Linearly interpolate between two rounded rectangles. + /// + /// If either is null, this function substitutes [RRect.zero] instead. + /// + /// The `t` argument represents position on the timeline, with 0.0 meaning + /// that the interpolation has not started, returning `a` (or something + /// equivalent to `a`), 1.0 meaning that the interpolation has finished, + /// returning `b` (or something equivalent to `b`), and values in between + /// meaning that the interpolation is at the relevant point on the timeline + /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and + /// 1.0, so negative values and values greater than 1.0 are valid (and can + /// easily be generated by curves such as [Curves.elasticInOut]). + /// + /// Values for `t` are usually obtained from an [Animation], such as + /// an [AnimationController]. + static RRect? lerp(RRect? a, RRect? b, double t) { + if (a == null) { + if (b == null) { + return null; + } + return b._lerpTo(null, 1 - t); + } + return a._lerpTo(b, t); + } + + @override + String toString() { + return _toString(className: 'RRect'); + } +} + +/// An immutable rounded superellipse. +/// +/// A rounded superellipse is a shape similar to a typical rounded rectangle +/// ([RRect]), but with smoother transitions between the straight sides and the +/// rounded corners. It resembles the `RoundedRectangle` shape in SwiftUI with +/// the `.continuous` corner style. +/// +/// Technically, a canonical rounded superellipse, i.e. one with a uniform +/// corner radius ([RSuperellipse.fromRectAndRadius]), is created by replacing +/// the four corners of a superellipse (also known as a Lamé curve) with +/// circular arcs. A rounded superellipse with non-uniform radii is extended on it +/// by concatenating arc segments and transformation. +/// +/// The corner radius parameters used in this class corresponds to SwiftUI's +/// `cornerRadius` parameter, which is close to, but not exactly equals to, the +/// radius of the corner circles. +class RSuperellipse extends _RRectLike { + /// Construct a rounded rectangle from its left, top, right, and bottom edges, + /// and the same radii along its horizontal axis and its vertical axis. + /// + /// Will assert in debug mode if `radiusX` or `radiusY` are negative. + const RSuperellipse.fromLTRBXY( + double left, + double top, + double right, + double bottom, + double radiusX, + double radiusY, + ) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + ); + + /// Construct a rounded rectangle from its left, top, right, and bottom edges, + /// and the same radius in each corner. + /// + /// Will assert in debug mode if the `radius` is negative in either x or y. + RSuperellipse.fromLTRBR(double left, double top, double right, double bottom, Radius radius) + : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + ); + + /// Construct a rounded rectangle from its bounding box and the same radii + /// along its horizontal axis and its vertical axis. + /// + /// Will assert in debug mode if `radiusX` or `radiusY` are negative. + RSuperellipse.fromRectXY(Rect rect, double radiusX, double radiusY) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + ); + + /// Construct a rounded rectangle from its bounding box and a radius that is + /// the same in each corner. + /// + /// Will assert in debug mode if the `radius` is negative in either x or y. + RSuperellipse.fromRectAndRadius(Rect rect, Radius radius) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + ); + + /// Construct a rounded rectangle from its left, top, right, and bottom edges, + /// and topLeft, topRight, bottomRight, and bottomLeft radii. + /// + /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will + /// assert in debug mode if any of the radii are negative in either x or y. + RSuperellipse.fromLTRBAndCorners( + double left, + double top, + double right, + double bottom, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + ); + + /// Construct a rounded rectangle from its bounding box and topLeft, + /// topRight, bottomRight, and bottomLeft radii. + /// + /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will + /// assert in debug mode if any of the radii are negative in either x or y. + RSuperellipse.fromRectAndCorners( + Rect rect, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + ); + + const RSuperellipse._raw({ + super.left = 0.0, + super.top = 0.0, + super.right = 0.0, + super.bottom = 0.0, + super.tlRadiusX = 0.0, + super.tlRadiusY = 0.0, + super.trRadiusX = 0.0, + super.trRadiusY = 0.0, + super.brRadiusX = 0.0, + super.brRadiusY = 0.0, + super.blRadiusX = 0.0, + super.blRadiusY = 0.0, + }); + + @override + RSuperellipse _create({ + required double left, + required double top, + required double right, + required double bottom, + required double tlRadiusX, + required double tlRadiusY, + required double trRadiusX, + required double trRadiusY, + required double brRadiusX, + required double brRadiusY, + required double blRadiusX, + required double blRadiusY, + }) => RSuperellipse._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: tlRadiusX, + tlRadiusY: tlRadiusY, + trRadiusX: trRadiusX, + trRadiusY: trRadiusY, + blRadiusX: blRadiusX, + blRadiusY: blRadiusY, + brRadiusX: brRadiusX, + brRadiusY: brRadiusY, + ); + + /// A rounded rectangle with all the values set to zero. + static const RSuperellipse zero = RSuperellipse._raw(); + + /// Linearly interpolate between two rounded superellipses. + /// + /// If either is null, this function substitutes [RSuperellipse.zero] instead. + /// + /// The `t` argument represents position on the timeline, with 0.0 meaning + /// that the interpolation has not started, returning `a` (or something + /// equivalent to `a`), 1.0 meaning that the interpolation has finished, + /// returning `b` (or something equivalent to `b`), and values in between + /// meaning that the interpolation is at the relevant point on the timeline + /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and + /// 1.0, so negative values and values greater than 1.0 are valid (and can + /// easily be generated by curves such as [Curves.elasticInOut]). + /// + /// Values for `t` are usually obtained from an [Animation], such as + /// an [AnimationController]. + static RSuperellipse? lerp(RSuperellipse? a, RSuperellipse? b, double t) { + if (a == null) { + if (b == null) { + return null; + } + return b._lerpTo(null, 1 - t); + } + return a._lerpTo(b, t); + } + + @override + String toString() { + return _toString(className: 'RSuperellipse'); + } +} + /// A transform consisting of a translation, a rotation, and a uniform scale. /// /// Used by [Canvas.drawAtlas]. This is a more efficient way to represent these diff --git a/engine/src/flutter/lib/ui/painting.dart b/engine/src/flutter/lib/ui/painting.dart index 5905633eced..dea0c7885fa 100644 --- a/engine/src/flutter/lib/ui/painting.dart +++ b/engine/src/flutter/lib/ui/painting.dart @@ -32,6 +32,11 @@ bool _rrectIsValid(RRect rrect) { return true; } +bool _rseIsValid(RSuperellipse rse) { + assert(!rse.hasNaN, 'RSuperellipse argument contained a NaN value.'); + return true; +} + bool _offsetIsValid(Offset offset) { assert(!offset.dx.isNaN && !offset.dy.isNaN, 'Offset argument contained a NaN value.'); return true; @@ -5873,6 +5878,18 @@ abstract class Canvas { /// discussion of how to address that and some examples of using [clipRRect]. void clipRRect(RRect rrect, {bool doAntiAlias = true}); + /// Reduces the clip region to the intersection of the current clip and the + /// given rounded superellipse. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/clip_rsuperellipse.png) + /// + /// If [doAntiAlias] is true, then the clip will be anti-aliased. + /// + /// If multiple draw commands intersect with the clip boundary, this can result + /// in incorrect blending at the clip boundary. See [saveLayer] for a + /// discussion of how to address that and some examples of using [clipRSuperellipse]. + void clipRSuperellipse(RSuperellipse rse, {bool doAntiAlias = true}); + /// Reduces the clip region to the intersection of the current clip and the /// given [Path]. /// @@ -5996,6 +6013,13 @@ abstract class Canvas { /// This shape is almost but not quite entirely unlike an annulus. void drawDRRect(RRect outer, RRect inner, Paint paint); + /// Draws a rounded superellipse with the given [Paint]. The shape is filled, + /// and the value of the [Paint.style] is ignored for this call. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/canvas_rsuperellipse.png#gh-light-mode-only) + /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/canvas_rsuperellipse.png#gh-dark-mode-only) + void drawRSuperellipse(RSuperellipse rse, Paint paint); + /// Draws an axis-aligned oval that fills the given axis-aligned rectangle /// with the given [Paint]. Whether the oval is filled or stroked (or both) is /// controlled by [Paint.style]. @@ -6607,6 +6631,15 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas { @Native, Handle, Bool)>(symbol: 'Canvas::clipRRect') external void _clipRRect(Float32List rrect, bool doAntiAlias); + @override + void clipRSuperellipse(RSuperellipse rse, {bool doAntiAlias = true}) { + assert(_rseIsValid(rse)); + _clipRSuperellipse(rse._getValue32(), doAntiAlias); + } + + @Native, Handle, Bool)>(symbol: 'Canvas::clipRSuperellipse') + external void _clipRSuperellipse(Float32List rse, bool doAntiAlias); + @override void clipPath(Path path, {bool doAntiAlias = true}) { _clipPath(path as _NativePath, doAntiAlias); @@ -6717,6 +6750,19 @@ base class _NativeCanvas extends NativeFieldWrapperClass1 implements Canvas { ByteData paintData, ); + @override + void drawRSuperellipse(RSuperellipse rse, Paint paint) { + assert(_rseIsValid(rse)); + _drawRSuperellipse(rse._getValue32(), paint._objects, paint._data); + } + + @Native, Handle, Handle, Handle)>(symbol: 'Canvas::drawRSuperellipse') + external void _drawRSuperellipse( + Float32List rse, + List? paintObjects, + ByteData paintData, + ); + @override void drawOval(Rect rect, Paint paint) { assert(_rectIsValid(rect)); diff --git a/engine/src/flutter/lib/ui/painting/canvas.cc b/engine/src/flutter/lib/ui/painting/canvas.cc index cf75ff91906..db280f2ddd3 100644 --- a/engine/src/flutter/lib/ui/painting/canvas.cc +++ b/engine/src/flutter/lib/ui/painting/canvas.cc @@ -177,6 +177,13 @@ void Canvas::clipRRect(const RRect& rrect, bool doAntiAlias) { } } +void Canvas::clipRSuperellipse(const RSuperellipse& rse, bool doAntiAlias) { + if (display_list_builder_) { + builder()->ClipRoundSuperellipse(rse.rsuperellipse, DlClipOp::kIntersect, + doAntiAlias); + } +} + void Canvas::clipPath(const CanvasPath* path, bool doAntiAlias) { if (!path) { Dart_ThrowException( @@ -295,6 +302,19 @@ void Canvas::drawDRRect(const RRect& outer, } } +void Canvas::drawRSuperellipse(const RSuperellipse& rse, + Dart_Handle paint_objects, + Dart_Handle paint_data) { + Paint paint(paint_objects, paint_data); + + FML_DCHECK(paint.isNotNull()); + if (display_list_builder_) { + DlPaint dl_paint; + paint.paint(dl_paint, kDrawDRRectFlags, DlTileMode::kDecal); + builder()->DrawRoundSuperellipse(rse.rsuperellipse, dl_paint); + } +} + void Canvas::drawOval(double left, double top, double right, diff --git a/engine/src/flutter/lib/ui/painting/canvas.h b/engine/src/flutter/lib/ui/painting/canvas.h index f475168fe08..e0c885b0482 100644 --- a/engine/src/flutter/lib/ui/painting/canvas.h +++ b/engine/src/flutter/lib/ui/painting/canvas.h @@ -12,6 +12,7 @@ #include "flutter/lib/ui/painting/picture.h" #include "flutter/lib/ui/painting/picture_recorder.h" #include "flutter/lib/ui/painting/rrect.h" +#include "flutter/lib/ui/painting/rsuperellipse.h" #include "flutter/lib/ui/painting/vertices.h" #include "third_party/tonic/typed_data/typed_list.h" @@ -61,6 +62,7 @@ class Canvas : public RefCountedDartWrappable, DisplayListOpFlags { DlClipOp clipOp, bool doAntiAlias = true); void clipRRect(const RRect& rrect, bool doAntiAlias = true); + void clipRSuperellipse(const RSuperellipse& rse, bool doAntiAlias = true); void clipPath(const CanvasPath* path, bool doAntiAlias = true); void getDestinationClipBounds(Dart_Handle rect_handle); void getLocalClipBounds(Dart_Handle rect_handle); @@ -92,6 +94,10 @@ class Canvas : public RefCountedDartWrappable, DisplayListOpFlags { Dart_Handle paint_objects, Dart_Handle paint_data); + void drawRSuperellipse(const RSuperellipse& rse, + Dart_Handle paint_objects, + Dart_Handle paint_data); + void drawOval(double left, double top, double right, diff --git a/engine/src/flutter/lib/ui/painting/rsuperellipse.cc b/engine/src/flutter/lib/ui/painting/rsuperellipse.cc new file mode 100644 index 00000000000..6f1865b6339 --- /dev/null +++ b/engine/src/flutter/lib/ui/painting/rsuperellipse.cc @@ -0,0 +1,59 @@ +// 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. + +#include "flutter/lib/ui/painting/rsuperellipse.h" + +#include "flutter/fml/logging.h" +#include "third_party/tonic/logging/dart_error.h" +#include "third_party/tonic/typed_data/typed_list.h" + +using flutter::RSuperellipse; + +namespace tonic { + +// Construct an DlRoundSuperellipse from a Dart RSuperellipse object. +// The Dart RSuperellipse is a Float32List containing +// [left, top, right, bottom, xRadius, yRadius] +RSuperellipse DartConverter::FromDart( + Dart_Handle value) { + Float32List buffer(value); + + RSuperellipse result; + result.is_null = true; + if (buffer.data() == nullptr) { + return result; + } + + // The Flutter rect may be inverted (upside down, backward, or both) + // Historically, Skia would normalize such rects but we will do that + // manually below when we construct the Impeller RoundRect + flutter::DlRect raw_rect = + flutter::DlRect::MakeLTRB(buffer[0], buffer[1], buffer[2], buffer[3]); + + // Flutter has radii in TL,TR,BR,BL (clockwise) order, + // but Impeller uses TL,TR,BL,BR (zig-zag) order + impeller::RoundingRadii radii = { + .top_left = flutter::DlSize(buffer[4], buffer[5]), + .top_right = flutter::DlSize(buffer[6], buffer[7]), + .bottom_left = flutter::DlSize(buffer[10], buffer[11]), + .bottom_right = flutter::DlSize(buffer[8], buffer[9]), + }; + + result.rsuperellipse = flutter::DlRoundSuperellipse::MakeRectRadii( + raw_rect.GetPositive(), radii); + + result.is_null = false; + return result; +} + +RSuperellipse DartConverter::FromArguments( + Dart_NativeArguments args, + int index, + Dart_Handle& exception) { + Dart_Handle value = Dart_GetNativeArgument(args, index); + FML_DCHECK(!CheckAndHandleError(value)); + return FromDart(value); +} + +} // namespace tonic diff --git a/engine/src/flutter/lib/ui/painting/rsuperellipse.h b/engine/src/flutter/lib/ui/painting/rsuperellipse.h new file mode 100644 index 00000000000..6df8606338b --- /dev/null +++ b/engine/src/flutter/lib/ui/painting/rsuperellipse.h @@ -0,0 +1,45 @@ +// 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. + +#ifndef FLUTTER_LIB_UI_PAINTING_RSUPERELLIPSE_H_ +#define FLUTTER_LIB_UI_PAINTING_RSUPERELLIPSE_H_ + +#include "flutter/display_list/geometry/dl_geometry_types.h" +#include "third_party/dart/runtime/include/dart_api.h" +#include "third_party/tonic/converter/dart_converter.h" + +namespace flutter { + +class RSuperellipse { + public: + DlRoundSuperellipse rsuperellipse; + bool is_null; +}; + +} // namespace flutter + +namespace tonic { + +template <> +struct DartConverter { + using NativeType = flutter::RSuperellipse; + using FfiType = Dart_Handle; + static constexpr const char* kFfiRepresentation = "Handle"; + static constexpr const char* kDartRepresentation = "Object"; + static constexpr bool kAllowedInLeafCall = false; + + static NativeType FromDart(Dart_Handle handle); + static NativeType FromArguments(Dart_NativeArguments args, + int index, + Dart_Handle& exception); + + static NativeType FromFfi(FfiType val) { return FromDart(val); } + static const char* GetFfiRepresentation() { return kFfiRepresentation; } + static const char* GetDartRepresentation() { return kDartRepresentation; } + static bool AllowedInLeafCall() { return kAllowedInLeafCall; } +}; + +} // namespace tonic + +#endif // FLUTTER_LIB_UI_PAINTING_RSUPERELLIPSE_H_ diff --git a/engine/src/flutter/lib/web_ui/lib/canvas.dart b/engine/src/flutter/lib/web_ui/lib/canvas.dart index f9be002cdd5..0342ef3e06c 100644 --- a/engine/src/flutter/lib/web_ui/lib/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/canvas.dart @@ -68,6 +68,7 @@ abstract class Canvas { Float64List getTransform(); void clipRect(Rect rect, {ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true}); void clipRRect(RRect rrect, {bool doAntiAlias = true}); + void clipRSuperellipse(RSuperellipse rse, {bool doAntiAlias = true}); void clipPath(Path path, {bool doAntiAlias = true}); Rect getLocalClipBounds(); Rect getDestinationClipBounds(); @@ -76,6 +77,7 @@ abstract class Canvas { void drawPaint(Paint paint); void drawRect(Rect rect, Paint paint); void drawRRect(RRect rrect, Paint paint); + void drawRSuperellipse(RSuperellipse rse, Paint paint); void drawDRRect(RRect outer, RRect inner, Paint paint); void drawOval(Rect rect, Paint paint); void drawCircle(Offset c, double radius, Paint paint); diff --git a/engine/src/flutter/lib/web_ui/lib/compositing.dart b/engine/src/flutter/lib/web_ui/lib/compositing.dart index 27713aad31d..b751ba553dd 100644 --- a/engine/src/flutter/lib/web_ui/lib/compositing.dart +++ b/engine/src/flutter/lib/web_ui/lib/compositing.dart @@ -18,6 +18,8 @@ abstract class ClipRectEngineLayer implements EngineLayer {} abstract class ClipRRectEngineLayer implements EngineLayer {} +abstract class ClipRSuperellipseEngineLayer implements EngineLayer {} + abstract class ClipPathEngineLayer implements EngineLayer {} abstract class OpacityEngineLayer implements EngineLayer {} @@ -45,6 +47,11 @@ abstract class SceneBuilder { required Clip clipBehavior, ClipRRectEngineLayer? oldLayer, }); + ClipRSuperellipseEngineLayer pushClipRSuperellipse( + RSuperellipse rse, { + required Clip clipBehavior, + ClipRSuperellipseEngineLayer? oldLayer, + }); ClipPathEngineLayer pushClipPath( Path path, { Clip clipBehavior = Clip.antiAlias, diff --git a/engine/src/flutter/lib/web_ui/lib/geometry.dart b/engine/src/flutter/lib/web_ui/lib/geometry.dart index a9c152e9d7b..0cb88fe9d3b 100644 --- a/engine/src/flutter/lib/web_ui/lib/geometry.dart +++ b/engine/src/flutter/lib/web_ui/lib/geometry.dart @@ -405,156 +405,21 @@ class Radius { } } -class RRect { - const RRect.fromLTRBXY( - double left, - double top, - double right, - double bottom, - double radiusX, - double radiusY, - ) : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: radiusX, - tlRadiusY: radiusY, - trRadiusX: radiusX, - trRadiusY: radiusY, - blRadiusX: radiusX, - blRadiusY: radiusY, - brRadiusX: radiusX, - brRadiusY: radiusY, - uniformRadii: radiusX == radiusY, - ); - - RRect.fromLTRBR(double left, double top, double right, double bottom, Radius radius) - : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: radius.x, - tlRadiusY: radius.y, - trRadiusX: radius.x, - trRadiusY: radius.y, - blRadiusX: radius.x, - blRadiusY: radius.y, - brRadiusX: radius.x, - brRadiusY: radius.y, - uniformRadii: radius.x == radius.y, - ); - - RRect.fromRectXY(Rect rect, double radiusX, double radiusY) - : this._raw( - top: rect.top, - left: rect.left, - right: rect.right, - bottom: rect.bottom, - tlRadiusX: radiusX, - tlRadiusY: radiusY, - trRadiusX: radiusX, - trRadiusY: radiusY, - blRadiusX: radiusX, - blRadiusY: radiusY, - brRadiusX: radiusX, - brRadiusY: radiusY, - uniformRadii: radiusX == radiusY, - ); - - RRect.fromRectAndRadius(Rect rect, Radius radius) - : this._raw( - top: rect.top, - left: rect.left, - right: rect.right, - bottom: rect.bottom, - tlRadiusX: radius.x, - tlRadiusY: radius.y, - trRadiusX: radius.x, - trRadiusY: radius.y, - blRadiusX: radius.x, - blRadiusY: radius.y, - brRadiusX: radius.x, - brRadiusY: radius.y, - uniformRadii: radius.x == radius.y, - ); - - RRect.fromLTRBAndCorners( - double left, - double top, - double right, - double bottom, { - Radius topLeft = Radius.zero, - Radius topRight = Radius.zero, - Radius bottomRight = Radius.zero, - Radius bottomLeft = Radius.zero, - }) : this._raw( - top: top, - left: left, - right: right, - bottom: bottom, - tlRadiusX: topLeft.x, - tlRadiusY: topLeft.y, - trRadiusX: topRight.x, - trRadiusY: topRight.y, - blRadiusX: bottomLeft.x, - blRadiusY: bottomLeft.y, - brRadiusX: bottomRight.x, - brRadiusY: bottomRight.y, - uniformRadii: - topLeft.x == topLeft.y && - topLeft.x == topRight.x && - topLeft.x == topRight.y && - topLeft.x == bottomLeft.x && - topLeft.x == bottomLeft.y && - topLeft.x == bottomRight.x && - topLeft.x == bottomRight.y, - ); - - RRect.fromRectAndCorners( - Rect rect, { - Radius topLeft = Radius.zero, - Radius topRight = Radius.zero, - Radius bottomRight = Radius.zero, - Radius bottomLeft = Radius.zero, - }) : this._raw( - top: rect.top, - left: rect.left, - right: rect.right, - bottom: rect.bottom, - tlRadiusX: topLeft.x, - tlRadiusY: topLeft.y, - trRadiusX: topRight.x, - trRadiusY: topRight.y, - blRadiusX: bottomLeft.x, - blRadiusY: bottomLeft.y, - brRadiusX: bottomRight.x, - brRadiusY: bottomRight.y, - uniformRadii: - topLeft.x == topLeft.y && - topLeft.x == topRight.x && - topLeft.x == topRight.y && - topLeft.x == bottomLeft.x && - topLeft.x == bottomLeft.y && - topLeft.x == bottomRight.x && - topLeft.x == bottomRight.y, - ); - - const RRect._raw({ - this.left = 0.0, - this.top = 0.0, - this.right = 0.0, - this.bottom = 0.0, - this.tlRadiusX = 0.0, - this.tlRadiusY = 0.0, - this.trRadiusX = 0.0, - this.trRadiusY = 0.0, - this.brRadiusX = 0.0, - this.brRadiusY = 0.0, - this.blRadiusX = 0.0, - this.blRadiusY = 0.0, - bool uniformRadii = false, +abstract class _RRectLike> { + const _RRectLike({ + required this.left, + required this.top, + required this.right, + required this.bottom, + required this.tlRadiusX, + required this.tlRadiusY, + required this.trRadiusX, + required this.trRadiusY, + required this.brRadiusX, + required this.brRadiusY, + required this.blRadiusX, + required this.blRadiusY, + required bool uniformRadii, }) : assert(tlRadiusX >= 0), assert(tlRadiusY >= 0), assert(trRadiusX >= 0), @@ -565,6 +430,21 @@ class RRect { assert(blRadiusY >= 0), webOnlyUniformRadii = uniformRadii; + T _create({ + required double left, + required double top, + required double right, + required double bottom, + required double tlRadiusX, + required double tlRadiusY, + required double trRadiusX, + required double trRadiusY, + required double brRadiusX, + required double brRadiusY, + required double blRadiusX, + required double blRadiusY, + }); + final double left; final double top; final double right; @@ -583,10 +463,9 @@ class RRect { // webOnly final bool webOnlyUniformRadii; Radius get blRadius => Radius.elliptical(blRadiusX, blRadiusY); - static const RRect zero = RRect._raw(); - RRect shift(Offset offset) { - return RRect._raw( + T shift(Offset offset) { + return _create( left: left + offset.dx, top: top + offset.dy, right: right + offset.dx, @@ -602,8 +481,8 @@ class RRect { ); } - RRect inflate(double delta) { - return RRect._raw( + T inflate(double delta) { + return _create( left: left - delta, top: top - delta, right: right + delta, @@ -619,7 +498,7 @@ class RRect { ); } - RRect deflate(double delta) => inflate(-delta); + T deflate(double delta) => inflate(-delta); double get width => right - left; double get height => bottom - top; Rect get outerRect => Rect.fromLTRB(left, top, right, bottom); @@ -716,7 +595,7 @@ class RRect { return min; } - RRect scaleRadii() { + T scaleRadii() { double scale = 1.0; final double absWidth = width.abs(); final double absHeight = height.abs(); @@ -726,7 +605,7 @@ class RRect { scale = _getMin(scale, brRadiusX, blRadiusX, absWidth); if (scale < 1.0) { - return RRect._raw( + return _create( top: top, left: left, right: right, @@ -742,7 +621,7 @@ class RRect { ); } - return RRect._raw( + return _create( top: top, left: left, right: right, @@ -758,105 +637,40 @@ class RRect { ); } - bool contains(Offset point) { - if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) { - return false; - } // outside bounding box - - final RRect scaled = scaleRadii(); - - double x; - double y; - double radiusX; - double radiusY; - // check whether point is in one of the rounded corner areas - // x, y -> translate to ellipse center - if (point.dx < left + scaled.tlRadiusX && point.dy < top + scaled.tlRadiusY) { - x = point.dx - left - scaled.tlRadiusX; - y = point.dy - top - scaled.tlRadiusY; - radiusX = scaled.tlRadiusX; - radiusY = scaled.tlRadiusY; - } else if (point.dx > right - scaled.trRadiusX && point.dy < top + scaled.trRadiusY) { - x = point.dx - right + scaled.trRadiusX; - y = point.dy - top - scaled.trRadiusY; - radiusX = scaled.trRadiusX; - radiusY = scaled.trRadiusY; - } else if (point.dx > right - scaled.brRadiusX && point.dy > bottom - scaled.brRadiusY) { - x = point.dx - right + scaled.brRadiusX; - y = point.dy - bottom + scaled.brRadiusY; - radiusX = scaled.brRadiusX; - radiusY = scaled.brRadiusY; - } else if (point.dx < left + scaled.blRadiusX && point.dy > bottom - scaled.blRadiusY) { - x = point.dx - left - scaled.blRadiusX; - y = point.dy - bottom + scaled.blRadiusY; - radiusX = scaled.blRadiusX; - radiusY = scaled.blRadiusY; - } else { - return true; // inside and not within the rounded corner area - } - - x = x / radiusX; - y = y / radiusY; - // check if the point is outside the unit circle - if (x * x + y * y > 1.0) { - return false; - } - return true; - } - - static RRect? lerp(RRect? a, RRect? b, double t) { + // Linearly interpolate between this object and another of the same shape. + T _lerpTo(T? b, double t) { + assert(runtimeType == T); if (b == null) { - if (a == null) { - return null; - } else { - final double k = 1.0 - t; - return RRect._raw( - left: a.left * k, - top: a.top * k, - right: a.right * k, - bottom: a.bottom * k, - tlRadiusX: math.max(0, a.tlRadiusX * k), - tlRadiusY: math.max(0, a.tlRadiusY * k), - trRadiusX: math.max(0, a.trRadiusX * k), - trRadiusY: math.max(0, a.trRadiusY * k), - brRadiusX: math.max(0, a.brRadiusX * k), - brRadiusY: math.max(0, a.brRadiusY * k), - blRadiusX: math.max(0, a.blRadiusX * k), - blRadiusY: math.max(0, a.blRadiusY * k), - ); - } + final double k = 1.0 - t; + return _create( + left: left * k, + top: top * k, + right: right * k, + bottom: bottom * k, + tlRadiusX: math.max(0, tlRadiusX * k), + tlRadiusY: math.max(0, tlRadiusY * k), + trRadiusX: math.max(0, trRadiusX * k), + trRadiusY: math.max(0, trRadiusY * k), + brRadiusX: math.max(0, brRadiusX * k), + brRadiusY: math.max(0, brRadiusY * k), + blRadiusX: math.max(0, blRadiusX * k), + blRadiusY: math.max(0, blRadiusY * k), + ); } else { - if (a == null) { - return RRect._raw( - left: b.left * t, - top: b.top * t, - right: b.right * t, - bottom: b.bottom * t, - tlRadiusX: math.max(0, b.tlRadiusX * t), - tlRadiusY: math.max(0, b.tlRadiusY * t), - trRadiusX: math.max(0, b.trRadiusX * t), - trRadiusY: math.max(0, b.trRadiusY * t), - brRadiusX: math.max(0, b.brRadiusX * t), - brRadiusY: math.max(0, b.brRadiusY * t), - blRadiusX: math.max(0, b.blRadiusX * t), - blRadiusY: math.max(0, b.blRadiusY * t), - ); - } else { - return RRect._raw( - left: _lerpDouble(a.left, b.left, t), - top: _lerpDouble(a.top, b.top, t), - right: _lerpDouble(a.right, b.right, t), - bottom: _lerpDouble(a.bottom, b.bottom, t), - tlRadiusX: math.max(0, _lerpDouble(a.tlRadiusX, b.tlRadiusX, t)), - tlRadiusY: math.max(0, _lerpDouble(a.tlRadiusY, b.tlRadiusY, t)), - trRadiusX: math.max(0, _lerpDouble(a.trRadiusX, b.trRadiusX, t)), - trRadiusY: math.max(0, _lerpDouble(a.trRadiusY, b.trRadiusY, t)), - brRadiusX: math.max(0, _lerpDouble(a.brRadiusX, b.brRadiusX, t)), - brRadiusY: math.max(0, _lerpDouble(a.brRadiusY, b.brRadiusY, t)), - blRadiusX: math.max(0, _lerpDouble(a.blRadiusX, b.blRadiusX, t)), - blRadiusY: math.max(0, _lerpDouble(a.blRadiusY, b.blRadiusY, t)), - ); - } + return _create( + left: _lerpDouble(left, b.left, t), + top: _lerpDouble(top, b.top, t), + right: _lerpDouble(right, b.right, t), + bottom: _lerpDouble(bottom, b.bottom, t), + tlRadiusX: math.max(0, _lerpDouble(tlRadiusX, b.tlRadiusX, t)), + tlRadiusY: math.max(0, _lerpDouble(tlRadiusY, b.tlRadiusY, t)), + trRadiusX: math.max(0, _lerpDouble(trRadiusX, b.trRadiusX, t)), + trRadiusY: math.max(0, _lerpDouble(trRadiusY, b.trRadiusY, t)), + brRadiusX: math.max(0, _lerpDouble(brRadiusX, b.brRadiusX, t)), + brRadiusY: math.max(0, _lerpDouble(brRadiusY, b.brRadiusY, t)), + blRadiusX: math.max(0, _lerpDouble(blRadiusX, b.blRadiusX, t)), + blRadiusY: math.max(0, _lerpDouble(blRadiusY, b.blRadiusY, t)), + ); } } @@ -868,7 +682,7 @@ class RRect { if (runtimeType != other.runtimeType) { return false; } - return other is RRect && + return other is _RRectLike && other.left == left && other.top == top && other.right == right && @@ -899,8 +713,7 @@ class RRect { brRadiusY, ); - @override - String toString() { + String _toString({required String className}) { final String rect = '${left.toStringAsFixed(1)}, ' '${top.toStringAsFixed(1)}, ' @@ -908,11 +721,11 @@ class RRect { '${bottom.toStringAsFixed(1)}'; if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) { if (tlRadius.x == tlRadius.y) { - return 'RRect.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})'; + return '$className.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})'; } - return 'RRect.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})'; + return '$className.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})'; } - return 'RRect.fromLTRBAndCorners(' + return '$className.fromLTRBAndCorners(' '$rect, ' 'topLeft: $tlRadius, ' 'topRight: $trRadius, ' @@ -921,6 +734,475 @@ class RRect { ')'; } } + +class RRect extends _RRectLike { + const RRect.fromLTRBXY( + double left, + double top, + double right, + double bottom, + double radiusX, + double radiusY, + ) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + uniformRadii: radiusX == radiusY, + ); + + RRect.fromLTRBR(double left, double top, double right, double bottom, Radius radius) + : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + uniformRadii: radius.x == radius.y, + ); + + RRect.fromRectXY(Rect rect, double radiusX, double radiusY) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + uniformRadii: radiusX == radiusY, + ); + + RRect.fromRectAndRadius(Rect rect, Radius radius) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + uniformRadii: radius.x == radius.y, + ); + + RRect.fromLTRBAndCorners( + double left, + double top, + double right, + double bottom, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + uniformRadii: + topLeft.x == topLeft.y && + topLeft.x == topRight.x && + topLeft.x == topRight.y && + topLeft.x == bottomLeft.x && + topLeft.x == bottomLeft.y && + topLeft.x == bottomRight.x && + topLeft.x == bottomRight.y, + ); + + RRect.fromRectAndCorners( + Rect rect, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + uniformRadii: + topLeft.x == topLeft.y && + topLeft.x == topRight.x && + topLeft.x == topRight.y && + topLeft.x == bottomLeft.x && + topLeft.x == bottomLeft.y && + topLeft.x == bottomRight.x && + topLeft.x == bottomRight.y, + ); + + const RRect._raw({ + super.left = 0.0, + super.top = 0.0, + super.right = 0.0, + super.bottom = 0.0, + super.tlRadiusX = 0.0, + super.tlRadiusY = 0.0, + super.trRadiusX = 0.0, + super.trRadiusY = 0.0, + super.brRadiusX = 0.0, + super.brRadiusY = 0.0, + super.blRadiusX = 0.0, + super.blRadiusY = 0.0, + super.uniformRadii = false, + }); + + @override + RRect _create({ + required double left, + required double top, + required double right, + required double bottom, + required double tlRadiusX, + required double tlRadiusY, + required double trRadiusX, + required double trRadiusY, + required double brRadiusX, + required double brRadiusY, + required double blRadiusX, + required double blRadiusY, + }) => RRect._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: tlRadiusX, + tlRadiusY: tlRadiusY, + trRadiusX: trRadiusX, + trRadiusY: trRadiusY, + blRadiusX: blRadiusX, + blRadiusY: blRadiusY, + brRadiusX: brRadiusX, + brRadiusY: brRadiusY, + ); + + static const RRect zero = RRect._raw(); + + bool contains(Offset point) { + if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) { + return false; + } // outside bounding box + + final RRect scaled = scaleRadii(); + + double x; + double y; + double radiusX; + double radiusY; + // check whether point is in one of the rounded corner areas + // x, y -> translate to ellipse center + if (point.dx < left + scaled.tlRadiusX && point.dy < top + scaled.tlRadiusY) { + x = point.dx - left - scaled.tlRadiusX; + y = point.dy - top - scaled.tlRadiusY; + radiusX = scaled.tlRadiusX; + radiusY = scaled.tlRadiusY; + } else if (point.dx > right - scaled.trRadiusX && point.dy < top + scaled.trRadiusY) { + x = point.dx - right + scaled.trRadiusX; + y = point.dy - top - scaled.trRadiusY; + radiusX = scaled.trRadiusX; + radiusY = scaled.trRadiusY; + } else if (point.dx > right - scaled.brRadiusX && point.dy > bottom - scaled.brRadiusY) { + x = point.dx - right + scaled.brRadiusX; + y = point.dy - bottom + scaled.brRadiusY; + radiusX = scaled.brRadiusX; + radiusY = scaled.brRadiusY; + } else if (point.dx < left + scaled.blRadiusX && point.dy > bottom - scaled.blRadiusY) { + x = point.dx - left - scaled.blRadiusX; + y = point.dy - bottom + scaled.blRadiusY; + radiusX = scaled.blRadiusX; + radiusY = scaled.blRadiusY; + } else { + return true; // inside and not within the rounded corner area + } + + x = x / radiusX; + y = y / radiusY; + // check if the point is outside the unit circle + if (x * x + y * y > 1.0) { + return false; + } + return true; + } + + static RRect? lerp(RRect? a, RRect? b, double t) { + if (a == null) { + if (b == null) { + return null; + } + return b._lerpTo(null, 1 - t); + } + return a._lerpTo(b, t); + } + + @override + String toString() { + return _toString(className: 'RRect'); + } +} + +class RSuperellipse extends _RRectLike { + const RSuperellipse.fromLTRBXY( + double left, + double top, + double right, + double bottom, + double radiusX, + double radiusY, + ) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + uniformRadii: radiusX == radiusY, + ); + + RSuperellipse.fromLTRBR(double left, double top, double right, double bottom, Radius radius) + : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + uniformRadii: radius.x == radius.y, + ); + + RSuperellipse.fromRectXY(Rect rect, double radiusX, double radiusY) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radiusX, + tlRadiusY: radiusY, + trRadiusX: radiusX, + trRadiusY: radiusY, + blRadiusX: radiusX, + blRadiusY: radiusY, + brRadiusX: radiusX, + brRadiusY: radiusY, + uniformRadii: radiusX == radiusY, + ); + + RSuperellipse.fromRectAndRadius(Rect rect, Radius radius) + : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: radius.x, + tlRadiusY: radius.y, + trRadiusX: radius.x, + trRadiusY: radius.y, + blRadiusX: radius.x, + blRadiusY: radius.y, + brRadiusX: radius.x, + brRadiusY: radius.y, + uniformRadii: radius.x == radius.y, + ); + + RSuperellipse.fromLTRBAndCorners( + double left, + double top, + double right, + double bottom, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + uniformRadii: + topLeft.x == topLeft.y && + topLeft.x == topRight.x && + topLeft.x == topRight.y && + topLeft.x == bottomLeft.x && + topLeft.x == bottomLeft.y && + topLeft.x == bottomRight.x && + topLeft.x == bottomRight.y, + ); + + RSuperellipse.fromRectAndCorners( + Rect rect, { + Radius topLeft = Radius.zero, + Radius topRight = Radius.zero, + Radius bottomRight = Radius.zero, + Radius bottomLeft = Radius.zero, + }) : this._raw( + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + tlRadiusX: topLeft.x, + tlRadiusY: topLeft.y, + trRadiusX: topRight.x, + trRadiusY: topRight.y, + blRadiusX: bottomLeft.x, + blRadiusY: bottomLeft.y, + brRadiusX: bottomRight.x, + brRadiusY: bottomRight.y, + uniformRadii: + topLeft.x == topLeft.y && + topLeft.x == topRight.x && + topLeft.x == topRight.y && + topLeft.x == bottomLeft.x && + topLeft.x == bottomLeft.y && + topLeft.x == bottomRight.x && + topLeft.x == bottomRight.y, + ); + + const RSuperellipse._raw({ + super.left = 0.0, + super.top = 0.0, + super.right = 0.0, + super.bottom = 0.0, + super.tlRadiusX = 0.0, + super.tlRadiusY = 0.0, + super.trRadiusX = 0.0, + super.trRadiusY = 0.0, + super.brRadiusX = 0.0, + super.brRadiusY = 0.0, + super.blRadiusX = 0.0, + super.blRadiusY = 0.0, + super.uniformRadii = false, + }); + + @override + RSuperellipse _create({ + required double left, + required double top, + required double right, + required double bottom, + required double tlRadiusX, + required double tlRadiusY, + required double trRadiusX, + required double trRadiusY, + required double brRadiusX, + required double brRadiusY, + required double blRadiusX, + required double blRadiusY, + }) => RSuperellipse._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: tlRadiusX, + tlRadiusY: tlRadiusY, + trRadiusX: trRadiusX, + trRadiusY: trRadiusY, + blRadiusX: blRadiusX, + blRadiusY: blRadiusY, + brRadiusX: brRadiusX, + brRadiusY: brRadiusY, + ); + + // Approximates a rounded superellipse with a round rectangle to the + // best practical accuracy. + // + // This workaround is needed until the rounded superellipse is implemented on + // Web. https://github.com/flutter/flutter/issues/163718 + RRect toApproximateRRect() { + // Experiments have shown that using the same corner radii for the RRect + // provides an approximation that is close to optimal, as achieving a perfect + // match is not feasible. + return RRect._raw( + top: top, + left: left, + right: right, + bottom: bottom, + tlRadiusX: tlRadiusX, + tlRadiusY: tlRadiusY, + trRadiusX: trRadiusX, + trRadiusY: trRadiusY, + blRadiusX: blRadiusX, + blRadiusY: blRadiusY, + brRadiusX: brRadiusX, + brRadiusY: brRadiusY, + ); + } + + static const RSuperellipse zero = RSuperellipse._raw(); + + static RSuperellipse? lerp(RSuperellipse? a, RSuperellipse? b, double t) { + if (a == null) { + if (b == null) { + return null; + } + return b._lerpTo(null, 1 - t); + } + return a._lerpTo(b, t); + } + + @override + String toString() { + return _toString(className: 'RSuperellipse'); + } +} // Modeled after Skia's SkRSXform. class RSTransform { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart index 1c25e7f5cac..ba40eaa0839 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -520,6 +520,17 @@ class CanvasPool extends _SaveStackTracking { } } + @override + void clipRSuperellipse(ui.RSuperellipse rse) { + // TODO(dkwingsmt): Properly implement clipRSuperellipse on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + final ui.RRect rrect = rse.toApproximateRRect(); + super.clipRSuperellipse(rse); + if (_canvas != null) { + _clipRRect(context, rrect); + } + } + void _clipRRect(DomCanvasRenderingContext2D ctx, ui.RRect rrect) { final ui.Path path = ui.Path()..addRRect(rrect); _runPath(ctx, path as SurfacePath); @@ -1244,6 +1255,14 @@ class _SaveStackTracking { clipStack!.add(SaveClipEntry.rrect(rrect, _currentTransform.clone())); } + /// Adds a round rectangle to clipping stack. + @mustCallSuper + void clipRSuperellipse(ui.RSuperellipse rse) { + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + clipRRect(rse.toApproximateRRect()); + } + /// Adds a path to clipping stack. @mustCallSuper void clipPath(ui.Path path) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 36d05d1c3da..474a59c916f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -52,6 +52,12 @@ class CkCanvas { skCanvas.clipRRect(toSkRRect(rrect), _clipOpIntersect, doAntiAlias); } + void clipRSuperellipse(ui.RSuperellipse rse, bool doAntiAlias) { + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + skCanvas.clipRRect(toSkRRect(rse.toApproximateRRect()), _clipOpIntersect, doAntiAlias); + } + void clipRect(ui.Rect rect, ui.ClipOp clipOp, bool doAntiAlias) { skCanvas.clipRect(toSkRect(rect), toSkClipOp(clipOp), doAntiAlias); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart index d283d3d11ab..c1ae75100a2 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart @@ -130,6 +130,14 @@ class CanvasKitCanvas implements ui.Canvas { _canvas.clipRRect(rrect, doAntiAlias); } + @override + void clipRSuperellipse(ui.RSuperellipse rse, {bool doAntiAlias = true}) { + assert(rsuperellipseIsValid(rse)); + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + _clipRRect(rse.toApproximateRRect(), doAntiAlias); + } + @override void clipPath(ui.Path path, {bool doAntiAlias = true}) { _canvas.clipPath(path as CkPath, doAntiAlias); @@ -199,6 +207,14 @@ class CanvasKitCanvas implements ui.Canvas { _canvas.drawRRect(rrect, paint as CkPaint); } + @override + void drawRSuperellipse(ui.RSuperellipse rse, ui.Paint paint) { + assert(rsuperellipseIsValid(rse)); + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + _drawRRect(rse.toApproximateRRect(), paint); + } + @override void drawDRRect(ui.RRect outer, ui.RRect inner, ui.Paint paint) { assert(rrectIsValid(outer)); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart index a331bfb30b6..b2fedd63c91 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart @@ -116,6 +116,22 @@ class ClipRRectEngineLayer extends ContainerLayer implements ui.ClipRRectEngineL } } +/// A layer that clips its child layers by a given [RRect]. +class ClipRSuperellipseEngineLayer extends ContainerLayer + implements ui.ClipRSuperellipseEngineLayer { + ClipRSuperellipseEngineLayer(this.clipRSuperellipse, this.clipBehavior) + : assert(clipBehavior != ui.Clip.none); + + /// The rounded superellipse used to clip child layers. + final ui.RSuperellipse clipRSuperellipse; + final ui.Clip? clipBehavior; + + @override + void accept(LayerVisitor visitor) { + visitor.visitClipRSuperellipse(this); + } +} + /// A layer that paints its children with the given opacity. class OpacityEngineLayer extends ContainerLayer implements ui.OpacityEngineLayer { OpacityEngineLayer(this.alpha, this.offset); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart index 78c52c340fa..dde50386547 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart @@ -126,6 +126,15 @@ class LayerSceneBuilder implements ui.SceneBuilder { return pushLayer(ClipRRectEngineLayer(rrect, clipBehavior)); } + @override + ClipRSuperellipseEngineLayer pushClipRSuperellipse( + ui.RSuperellipse rse, { + ui.Clip? clipBehavior, + ui.EngineLayer? oldLayer, + }) { + return pushLayer(ClipRSuperellipseEngineLayer(rse, clipBehavior)); + } + @override ClipRectEngineLayer pushClipRect( ui.Rect rect, { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart index 9516b466d5d..7b1dcd80868 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart @@ -13,6 +13,7 @@ abstract class LayerVisitor { void visitClipPath(ClipPathEngineLayer clipPath); void visitClipRect(ClipRectEngineLayer clipRect); void visitClipRRect(ClipRRectEngineLayer clipRRect); + void visitClipRSuperellipse(ClipRSuperellipseEngineLayer clipRSuperellipse); void visitOpacity(OpacityEngineLayer opacity); void visitTransform(TransformEngineLayer transform); void visitOffset(OffsetEngineLayer offset); @@ -109,6 +110,20 @@ class PrerollVisitor extends LayerVisitor { mutatorsStack.pop(); } + @override + void visitClipRSuperellipse(ClipRSuperellipseEngineLayer clipRSuperellipse) { + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + mutatorsStack.pushClipRRect(clipRSuperellipse.clipRSuperellipse.toApproximateRRect()); + final ui.Rect childPaintBounds = prerollChildren(clipRSuperellipse); + if (childPaintBounds.overlaps(clipRSuperellipse.clipRSuperellipse.outerRect)) { + clipRSuperellipse.paintBounds = childPaintBounds.intersect( + clipRSuperellipse.clipRSuperellipse.outerRect, + ); + } + mutatorsStack.pop(); + } + @override void visitClipRect(ClipRectEngineLayer clipRect) { mutatorsStack.pushClipRect(clipRect.clipRect); @@ -310,6 +325,25 @@ class MeasureVisitor extends LayerVisitor { measuringCanvas.restore(); } + @override + void visitClipRSuperellipse(ClipRSuperellipseEngineLayer clipRSuperellipse) { + assert(clipRSuperellipse.needsPainting); + + measuringCanvas.save(); + measuringCanvas.clipRSuperellipse( + clipRSuperellipse.clipRSuperellipse, + clipRSuperellipse.clipBehavior != ui.Clip.hardEdge, + ); + if (clipRSuperellipse.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + measuringCanvas.saveLayer(clipRSuperellipse.paintBounds, null); + } + measureChildren(clipRSuperellipse); + if (clipRSuperellipse.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + measuringCanvas.restore(); + } + measuringCanvas.restore(); + } + @override void visitOpacity(OpacityEngineLayer opacity) { assert(opacity.needsPainting); @@ -532,6 +566,27 @@ class PaintVisitor extends LayerVisitor { nWayCanvas.restore(); } + @override + void visitClipRSuperellipse(ClipRSuperellipseEngineLayer clipRSuperellipse) { + assert(clipRSuperellipse.needsPainting); + + nWayCanvas.save(); + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + nWayCanvas.clipRRect( + clipRSuperellipse.clipRSuperellipse.toApproximateRRect(), + clipRSuperellipse.clipBehavior != ui.Clip.hardEdge, + ); + if (clipRSuperellipse.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.saveLayer(clipRSuperellipse.paintBounds, null); + } + paintChildren(clipRSuperellipse); + if (clipRSuperellipse.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + nWayCanvas.restore(); + } + nWayCanvas.restore(); + } + @override void visitOpacity(OpacityEngineLayer opacity) { assert(opacity.needsPainting); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/engine_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/engine_canvas.dart index f49bc3a156e..0966f664e48 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -47,6 +47,8 @@ abstract class EngineCanvas { void clipRRect(ui.RRect rrect); + void clipRSuperellipse(ui.RSuperellipse rse); + void clipPath(ui.Path path); void drawColor(ui.Color color, ui.BlendMode blendMode); @@ -234,6 +236,17 @@ mixin SaveStackTracking on EngineCanvas { _clipStack!.add(SaveClipEntry.rrect(rrect, _currentTransform.clone())); } + /// Adds a round superellipse to clipping stack. + /// + /// Classes that override this method must call `super.clipRSuperellipse()`. + @override + void clipRSuperellipse(ui.RSuperellipse rse) { + _clipStack ??= []; + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + _clipStack!.add(SaveClipEntry.rrect(rse.toApproximateRRect(), _currentTransform.clone())); + } + /// Adds a path to clipping stack. /// /// Classes that override this method must call `super.clipPath()`. diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart index a80195d0536..cb965175b1c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart @@ -343,6 +343,13 @@ class BitmapCanvas extends EngineCanvas { _canvasPool.clipRRect(rrect); } + @override + void clipRSuperellipse(ui.RSuperellipse rse) { + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + _canvasPool.clipRRect(rse.toApproximateRRect()); + } + @override void clipPath(ui.Path path) { _canvasPool.clipPath(path); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/canvas.dart index 4925d41e8da..ce82542eb31 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/canvas.dart @@ -123,6 +123,14 @@ class SurfaceCanvas implements ui.Canvas { _canvas.clipRRect(rrect); } + @override + void clipRSuperellipse(ui.RSuperellipse rse, {bool doAntiAlias = true}) { + assert(rsuperellipseIsValid(rse)); + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + _clipRRect(rse.toApproximateRRect(), doAntiAlias); + } + @override void clipPath(ui.Path path, {bool doAntiAlias = true}) { _clipPath(path, doAntiAlias); @@ -220,6 +228,14 @@ class SurfaceCanvas implements ui.Canvas { _canvas.drawDRRect(outer, inner, paint as SurfacePaint); } + @override + void drawRSuperellipse(ui.RSuperellipse rse, ui.Paint paint) { + assert(rsuperellipseIsValid(rse)); + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + _drawRRect(rse.toApproximateRRect(), paint); + } + @override void drawOval(ui.Rect rect, ui.Paint paint) { assert(rectIsValid(rect)); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart index b75461003c3..e77944caaf5 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/clip.dart @@ -186,6 +186,71 @@ class PersistedClipRRect extends PersistedContainerSurface bool get isClipping => true; } +/// A surface that creates a rounded superellipse clip. +/// +/// Implemented by falling back to RRect. +class PersistedClipRSuperellipse extends PersistedContainerSurface + with _DomClip + implements ui.ClipRSuperellipseEngineLayer { + PersistedClipRSuperellipse(ui.EngineLayer? oldLayer, this.rse, this.clipBehavior) + : super(oldLayer as PersistedSurface?); + + final ui.RSuperellipse rse; + // TODO(yjbanov): can this be controlled in the browser? + final ui.Clip? clipBehavior; + + @override + void recomputeTransformAndClip() { + transform = parent!.transform; + if (clipBehavior != ui.Clip.none) { + localClipBounds = rse.outerRect; + } else { + localClipBounds = null; + } + projectedClip = null; + } + + @override + DomElement createElement() { + // Fall back to rrect. + return super.createElement()..setAttribute('clip-type', 'rrect'); + } + + @override + void apply() { + final DomCSSStyleDeclaration style = rootElement!.style; + style + ..left = '${rse.left}px' + ..top = '${rse.top}px' + ..width = '${rse.width}px' + ..height = '${rse.height}px' + ..borderTopLeftRadius = '${rse.tlRadiusX}px' + ..borderTopRightRadius = '${rse.trRadiusX}px' + ..borderBottomRightRadius = '${rse.brRadiusX}px' + ..borderBottomLeftRadius = '${rse.blRadiusX}px'; + applyOverflow(rootElement!, clipBehavior); + + // Translate the child container in the opposite direction to compensate for + // the shift in the coordinate system introduced by the translation of the + // rootElement. Clipping in Flutter has no effect on the coordinate system. + childContainer!.style + ..left = '${-rse.left}px' + ..top = '${-rse.top}px'; + } + + @override + void update(PersistedClipRSuperellipse oldSurface) { + super.update(oldSurface); + if (rse != oldSurface.rse || clipBehavior != oldSurface.clipBehavior) { + localClipBounds = null; + apply(); + } + } + + @override + bool get isClipping => true; +} + /// A surface that clips it's children. class PersistedClipPath extends PersistedContainerSurface implements ui.ClipPathEngineLayer { PersistedClipPath(PersistedClipPath? super.oldLayer, this.clipPath, this.clipBehavior); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/dom_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/dom_canvas.dart index 5cd6d5d63ed..80761d369e4 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/dom_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/dom_canvas.dart @@ -44,6 +44,11 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { throw UnimplementedError(); } + @override + void clipRSuperellipse(ui.RSuperellipse rse, {bool doAntiAlias = true}) { + throw UnimplementedError(); + } + @override void clipPath(ui.Path path) { throw UnimplementedError(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart index b2c24c8293c..fa5ca2fcfbf 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -147,6 +147,22 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { return _pushSurface(PersistedClipRRect(oldLayer, rrect, clipBehavior)); } + /// Pushes a rounded-superellipse clip operation onto the operation stack. + /// + /// Rasterization outside the given rounded rectangle is discarded. + /// + /// See [pop] for details about the operation stack. + @override + ui.ClipRSuperellipseEngineLayer pushClipRSuperellipse( + ui.RSuperellipse rse, { + ui.Clip? clipBehavior, + ui.ClipRSuperellipseEngineLayer? oldLayer, + }) { + return _pushSurface( + PersistedClipRSuperellipse(oldLayer, rse, clipBehavior), + ); + } + /// Pushes a path clip operation onto the operation stack. /// /// Rasterization outside the given path is discarded. diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart index ae86ab7e3d5..788794e08bf 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart @@ -226,6 +226,56 @@ class ClipRRectOperation implements LayerOperation { String toString() => 'ClipRRectOperation(rrect: $rrect, clip: $clip)'; } +class ClipRSuperellipseLayer with PictureEngineLayer implements ui.ClipRSuperellipseEngineLayer { + ClipRSuperellipseLayer(this.operation); + + @override + final ClipRSuperellipseOperation operation; + + @override + ClipRSuperellipseLayer emptyClone() => ClipRSuperellipseLayer(operation); +} + +class ClipRSuperellipseOperation implements LayerOperation { + const ClipRSuperellipseOperation(this.rse, this.clip); + + final ui.RSuperellipse rse; + final ui.Clip clip; + + @override + ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rse.outerRect); + + @override + void pre(SceneCanvas canvas) { + canvas.save(); + canvas.clipRSuperellipse(rse, doAntiAlias: clip != ui.Clip.hardEdge); + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.saveLayer(rse.outerRect, ui.Paint()); + } + } + + @override + void post(SceneCanvas canvas) { + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.restore(); + } + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() { + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + return PlatformViewStyling(clip: PlatformViewRRectClip(rse.toApproximateRRect())); + } + + @override + bool get affectsBackdrop => false; + + @override + String toString() => 'ClipRSuperellipseOperation(rse: $rse, clip: $clip)'; +} + class ColorFilterLayer with PictureEngineLayer implements ui.ColorFilterEngineLayer { ColorFilterLayer(this.operation); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart index 61ab18b73f8..e7c74477180 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart @@ -385,6 +385,17 @@ class EngineSceneBuilder implements ui.SceneBuilder { ui.ClipRRectEngineLayer? oldLayer, }) => pushLayer(ClipRRectLayer(ClipRRectOperation(rrect, clipBehavior))); + @override + ui.ClipRSuperellipseEngineLayer pushClipRSuperellipse( + ui.RSuperellipse rse, { + required ui.Clip clipBehavior, + ui.ClipRSuperellipseEngineLayer? oldLayer, + }) { + return pushLayer( + ClipRSuperellipseLayer(ClipRSuperellipseOperation(rse, clipBehavior)), + ); + } + @override ui.ClipRectEngineLayer pushClipRect( ui.Rect rect, { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart index d643821c576..fd63b4d5611 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart @@ -134,6 +134,13 @@ class SkwasmCanvas implements SceneCanvas { }); } + @override + void clipRSuperellipse(ui.RSuperellipse rse, {bool doAntiAlias = true}) { + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + clipRRect(rse.toApproximateRRect(), doAntiAlias: doAntiAlias); + } + @override void clipPath(ui.Path path, {bool doAntiAlias = true}) { path as SkwasmPath; @@ -176,6 +183,13 @@ class SkwasmCanvas implements SceneCanvas { paintDispose(paintHandle); } + @override + void drawRSuperellipse(ui.RSuperellipse rse, ui.Paint paint) { + // TODO(dkwingsmt): Properly implement clipRSE on Web instead of falling + // back to RRect. https://github.com/flutter/flutter/issues/163718 + drawRRect(rse.toApproximateRRect(), paint); + } + @override void drawDRRect(ui.RRect outer, ui.RRect inner, ui.Paint paint) { final paintHandle = (paint as SkwasmPaint).toRawPaint(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/validators.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/validators.dart index d6ee7df6501..ba1b72a244f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/validators.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/validators.dart @@ -22,6 +22,14 @@ bool rrectIsValid(ui.RRect rrect) { return true; } +bool rsuperellipseIsValid(ui.RSuperellipse rse) { + assert( + !(rse.left.isNaN || rse.right.isNaN || rse.top.isNaN || rse.bottom.isNaN), + 'RSuperellipse argument contained a NaN value.', + ); + return true; +} + bool offsetIsValid(ui.Offset offset) { assert(!offset.dx.isNaN && !offset.dy.isNaN, 'Offset argument contained a NaN value.'); return true; diff --git a/engine/src/flutter/lib/web_ui/test/common/mock_engine_canvas.dart b/engine/src/flutter/lib/web_ui/test/common/mock_engine_canvas.dart index 434d3e7c008..39f5ae370ae 100644 --- a/engine/src/flutter/lib/web_ui/test/common/mock_engine_canvas.dart +++ b/engine/src/flutter/lib/web_ui/test/common/mock_engine_canvas.dart @@ -92,6 +92,11 @@ class MockEngineCanvas implements EngineCanvas { _called('clipRRect', arguments: rrect); } + @override + void clipRSuperellipse(RSuperellipse rse) { + _called('clipRSuperellipse', arguments: rse); + } + @override void clipPath(Path path) { _called('clipPath', arguments: path); diff --git a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart index bb145d7cae1..d17b02b5cef 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart @@ -89,6 +89,9 @@ class StubSceneCanvas implements SceneCanvas { @override void clipRRect(ui.RRect rrect, {bool doAntiAlias = true}) {} + @override + void clipRSuperellipse(ui.RSuperellipse rse, {bool doAntiAlias = true}) {} + @override void clipRect(ui.Rect rect, {ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true}) {} @@ -151,6 +154,9 @@ class StubSceneCanvas implements SceneCanvas { @override void drawRRect(ui.RRect rrect, ui.Paint paint) {} + @override + void drawRSuperellipse(ui.RSuperellipse rse, ui.Paint paint) {} + @override void drawRawAtlas( ui.Image atlas, diff --git a/engine/src/flutter/shell/common/dl_op_spy.cc b/engine/src/flutter/shell/common/dl_op_spy.cc index c0ad22186f7..2b0426a1a07 100644 --- a/engine/src/flutter/shell/common/dl_op_spy.cc +++ b/engine/src/flutter/shell/common/dl_op_spy.cc @@ -65,6 +65,9 @@ void DlOpSpy::drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) { did_draw_ |= will_draw_; } +void DlOpSpy::drawRoundSuperellipse(const DlRoundSuperellipse& rse) { + did_draw_ |= will_draw_; +} void DlOpSpy::drawPath(const DlPath& path) { did_draw_ |= will_draw_; } diff --git a/engine/src/flutter/shell/common/dl_op_spy.h b/engine/src/flutter/shell/common/dl_op_spy.h index 3e689b11489..b6ebae2ef42 100644 --- a/engine/src/flutter/shell/common/dl_op_spy.h +++ b/engine/src/flutter/shell/common/dl_op_spy.h @@ -57,6 +57,7 @@ class DlOpSpy final : public virtual DlOpReceiver, void drawRoundRect(const DlRoundRect& rrect) override; void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override; + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override; void drawPath(const DlPath& path) override; void drawArc(const DlRect& oval_bounds, DlScalar start_degrees, diff --git a/engine/src/flutter/testing/display_list_testing.cc b/engine/src/flutter/testing/display_list_testing.cc index ae45ef92175..1fa32453874 100644 --- a/engine/src/flutter/testing/display_list_testing.cc +++ b/engine/src/flutter/testing/display_list_testing.cc @@ -207,8 +207,8 @@ extern std::ostream& operator<<(std::ostream& os, const DlPath& path) { std::ostream& operator<<(std::ostream& os, const flutter::DlClipOp& op) { switch (op) { - case flutter::DlClipOp::kDifference: return os << "ClipOp::kDifference"; - case flutter::DlClipOp::kIntersect: return os << "ClipOp::kIntersect"; + case flutter::DlClipOp::kDifference: return os << "DlClipOp::kDifference"; + case flutter::DlClipOp::kIntersect: return os << "DlClipOp::kIntersect"; } } @@ -784,6 +784,15 @@ void DisplayListStreamDispatcher::clipRoundRect(const DlRoundRect& rrect, << "isaa: " << is_aa << ");" << std::endl; } +void DisplayListStreamDispatcher::clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) { + startl() << "clipRoundSuperellipse(" + << rse << ", " + << clip_op << ", " + << "isaa: " << is_aa + << ");" << std::endl; +} void DisplayListStreamDispatcher::clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) { startl() << "clipPath(" @@ -835,6 +844,9 @@ void DisplayListStreamDispatcher::drawDiffRoundRect(const DlRoundRect& outer, startl() << "drawDRRect(outer: " << outer << ", " << std::endl; startl() << " inner: " << inner << ");" << std::endl; } +void DisplayListStreamDispatcher::drawRoundSuperellipse(const DlRoundSuperellipse& rse) { + startl() << "drawRSuperellipse(" << rse << ");" << std::endl; +} void DisplayListStreamDispatcher::drawPath(const DlPath& path) { startl() << "drawPath(" << path << ");" << std::endl; } diff --git a/engine/src/flutter/testing/display_list_testing.h b/engine/src/flutter/testing/display_list_testing.h index 515b6b25d95..18ff7a94c50 100644 --- a/engine/src/flutter/testing/display_list_testing.h +++ b/engine/src/flutter/testing/display_list_testing.h @@ -135,6 +135,9 @@ class DisplayListStreamDispatcher final : public DlOpReceiver { void clipRoundRect(const DlRoundRect& rrect, DlClipOp clip_op, bool is_aa) override; + void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) override; void clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) override; void drawColor(DlColor color, DlBlendMode mode) override; @@ -150,6 +153,7 @@ class DisplayListStreamDispatcher final : public DlOpReceiver { void drawRoundRect(const DlRoundRect& rrect) override; void drawDiffRoundRect(const DlRoundRect& outer, const DlRoundRect& inner) override; + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override; void drawPath(const DlPath& path) override; void drawArc(const DlRect& oval_bounds, DlScalar start_degrees, @@ -382,6 +386,18 @@ class DisplayListGeneralReceiver : public DlOpReceiver { break; } } + void clipRoundSuperellipse(const DlRoundSuperellipse& rse, + DlClipOp clip_op, + bool is_aa) override { + switch (clip_op) { + case DlClipOp::kIntersect: + RecordByType(DisplayListOpType::kClipIntersectRoundSuperellipse); + break; + case DlClipOp::kDifference: + RecordByType(DisplayListOpType::kClipDifferenceRoundSuperellipse); + break; + } + } void clipPath(const DlPath& path, DlClipOp clip_op, bool is_aa) override { switch (clip_op) { case DlClipOp::kIntersect: @@ -435,6 +451,9 @@ class DisplayListGeneralReceiver : public DlOpReceiver { const DlRoundRect& inner) override { RecordByType(DisplayListOpType::kDrawDiffRoundRect); } + void drawRoundSuperellipse(const DlRoundSuperellipse& rse) override { + RecordByType(DisplayListOpType::kDrawRoundSuperellipse); + } void drawPath(const DlPath& path) override { RecordByType(DisplayListOpType::kDrawPath); }