[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].

<!-- Links -->
[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
This commit is contained in:
Tong Mu 2025-02-21 15:58:26 -08:00 committed by GitHub
parent 7d23780c7f
commit 5b7a3c8b20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 3038 additions and 697 deletions

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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()) {

View File

@ -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,

View File

@ -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:

View File

@ -97,10 +97,12 @@ namespace flutter {
V(ClipIntersectRect) \
V(ClipIntersectOval) \
V(ClipIntersectRoundRect) \
V(ClipIntersectRoundSuperellipse) \
V(ClipIntersectPath) \
V(ClipDifferenceRect) \
V(ClipDifferenceOval) \
V(ClipDifferenceRoundRect) \
V(ClipDifferenceRoundSuperellipse) \
V(ClipDifferencePath) \
\
V(DrawPaint) \
@ -113,6 +115,7 @@ namespace flutter {
V(DrawCircle) \
V(DrawRoundRect) \
V(DrawDiffRoundRect) \
V(DrawRoundSuperellipse) \
V(DrawArc) \
V(DrawPath) \
\

View File

@ -4512,7 +4512,7 @@ TEST_F(DisplayListTest, DrawDisplayListForwardsBackdropFlag) {
#define CLIP_EXPECTOR(name) ClipExpector name(__FILE__, __LINE__)
struct ClipExpectation {
std::variant<DlRect, DlRoundRect, DlPath> shape;
std::variant<DlRect, DlRoundRect, DlRoundSuperellipse, DlPath> 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);
}

View File

@ -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<ClipIntersectRoundSuperellipseOp>(0, rse, is_aa);
break;
case DlClipOp::kDifference:
Push<ClipDifferenceRoundSuperellipseOp>(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<DrawRoundSuperellipseOp>(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);

View File

@ -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,

View File

@ -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,

View File

@ -323,6 +323,10 @@ class DisplayListOpFlags : DisplayListFlags {
kBasePaintFlags | //
kBaseStrokeOrFillFlags //
};
static constexpr DisplayListAttributeFlags kDrawRSuperellipseFlags{
kBasePaintFlags | //
kBaseStrokeOrFillFlags //
};
static constexpr DisplayListAttributeFlags kDrawPathFlags{
kBasePaintFlags | //
kBaseStrokeOrFillFlags | //

View File

@ -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,

View File

@ -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

View File

@ -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], //

View File

@ -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

View File

@ -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));

View File

@ -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,

View File

@ -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());

View File

@ -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,

View File

@ -546,6 +546,34 @@ std::vector<DisplayListInvocationGroup> 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<DisplayListInvocationGroup> 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,

View File

@ -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 =

View File

@ -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<DlRect, 2> 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;

View File

@ -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:

View File

@ -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,

View File

@ -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",

View File

@ -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

View File

@ -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<DlRoundSuperellipse> {
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_

View File

@ -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<ClipRSuperellipseLayer>(layer_rsuperellipse,
Clip::kNone),
"clip_behavior != Clip::kNone");
}
TEST_F(ClipRSuperellipseLayerTest, PaintingEmptyLayerDies) {
const DlRoundSuperellipse layer_rsuperellipse = DlRoundSuperellipse();
auto layer = std::make_shared<ClipRSuperellipseLayer>(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<ClipRSuperellipseLayer>(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<MockLayer>(child_path, child_paint);
auto layer = std::make_shared<ClipRSuperellipseLayer>(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<MockLayer>(child_path, child_paint);
auto layer = std::make_shared<ClipRSuperellipseLayer>(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<MockLayer>(child_path, child_paint);
auto layer = std::make_shared<ClipRSuperellipseLayer>(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<MockLayer>(child_path, child_paint);
auto layer = std::make_shared<ClipRSuperellipseLayer>(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<Layer>& 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<ClipRSuperellipseLayer>(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<MockLayer> nochild;
auto reader = std::make_shared<MockLayer>(path, paint);
reader->set_fake_reads_surface(true);
auto nonreader = std::make_shared<MockLayer>(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<ClipRSuperellipseLayer>(
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<ClipRSuperellipseLayer>(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<ClipRSuperellipseLayer>(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<ClipRSuperellipseLayer>(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<ClipRSuperellipseLayer>(
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<OpacityLayer>(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<ClipRSuperellipseLayer>(
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<OpacityLayer>(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<ClipRSuperellipseLayer>(
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<ClipRSuperellipseLayer>(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<PlatformViewLayer>(view_offset, view_size, view_id);
DlRoundSuperellipse clip_rsuperellipse =
DlRoundSuperellipse::MakeRectXY(DlRect(), 20, 20);
auto clip = std::make_shared<ClipRSuperellipseLayer>(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<int64_t>({view_id}));
clip->Paint(paint_context());
EXPECT_EQ(embedder.painted_views(), std::vector<int64_t>({view_id}));
}
} // namespace testing
} // namespace flutter
// NOLINTEND(bugprone-unchecked-optional-access)

View File

@ -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<ClipRSuperellipseEntry>(rse, is_aa));
apply_last_entry();
}
void LayerStateStack::push_clip_path(const DlPath& path, bool is_aa) {
state_stack_.emplace_back(std::make_unique<ClipPathEntry>(path, is_aa));
apply_last_entry();

View File

@ -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;

View File

@ -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) {

View File

@ -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[],

View File

@ -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);

View File

@ -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;

View File

@ -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> Geometry::MakeRoundRect(const Rect& rect,
return std::make_unique<RoundRectGeometry>(rect, radii);
}
std::unique_ptr<Geometry> Geometry::MakeRoundSuperellipse(
const Rect& rect,
Scalar corner_radius) {
return std::make_unique<RoundSuperellipseGeometry>(rect, corner_radius);
}
bool Geometry::CoversArea(const Matrix& transform, const Rect& rect) const {
return false;
}

View File

@ -83,6 +83,9 @@ class Geometry {
static std::unique_ptr<Geometry> MakeRoundRect(const Rect& rect,
const Size& radii);
static std::unique_ptr<Geometry> MakeRoundSuperellipse(const Rect& rect,
Scalar corner_radius);
virtual GeometryResult GetPositionBuffer(const ContentContext& renderer,
const Entity& entity,
RenderPass& pass) const = 0;

View File

@ -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

View File

@ -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

View File

@ -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) {}

View File

@ -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",

View File

@ -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<Void Function(Pointer<Void>, Handle, Handle, Int32, Handle)>(
symbol: 'SceneBuilder::pushClipRSuperellipse',
)
external void _pushClipRSuperellipse(
EngineLayer layer,
Float32List rrect,
int clipBehavior,
EngineLayer? oldLayer,
);
@override
ClipPathEngineLayer pushClipPath(
Path path, {

View File

@ -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<EngineLayer>& old_layer) {
auto layer = std::make_shared<flutter::ClipRSuperellipseLayer>(
rse.rsuperellipse, static_cast<flutter::Clip>(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,

View File

@ -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<SceneBuilder> {
const RRect& rrect,
int clip_behavior,
const fml::RefPtr<EngineLayer>& old_layer);
void pushClipRSuperellipse(Dart_Handle layer_handle,
const RSuperellipse& rse,
int clip_behavior,
const fml::RefPtr<EngineLayer>& old_layer);
void pushClipPath(Dart_Handle layer_handle,
const CanvasPath* path,
int clip_behavior,

View File

@ -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) \

View File

@ -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<T extends _RRectLike<T>> {
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,6 +1432,294 @@ class RRect {
);
}
// Linearly interpolate between this object and another of the same shape.
T _lerpTo(T? b, double t) {
assert(runtimeType == T);
if (b == null) {
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 {
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)),
);
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is _RRectLike &&
other.left == left &&
other.top == top &&
other.right == right &&
other.bottom == bottom &&
other.tlRadiusX == tlRadiusX &&
other.tlRadiusY == tlRadiusY &&
other.trRadiusX == trRadiusX &&
other.trRadiusY == trRadiusY &&
other.blRadiusX == blRadiusX &&
other.blRadiusY == blRadiusY &&
other.brRadiusX == brRadiusX &&
other.brRadiusY == brRadiusY;
}
@override
int get hashCode => Object.hash(
left,
top,
right,
bottom,
tlRadiusX,
tlRadiusY,
trRadiusX,
trRadiusY,
blRadiusX,
blRadiusY,
brRadiusX,
brRadiusY,
);
String _toString({required String className}) {
final String rect =
'${left.toStringAsFixed(1)}, '
'${top.toStringAsFixed(1)}, '
'${right.toStringAsFixed(1)}, '
'${bottom.toStringAsFixed(1)}';
if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) {
if (tlRadius.x == tlRadius.y) {
return '$className.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})';
}
return '$className.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})';
}
return '$className.fromLTRBAndCorners('
'$rect, '
'topLeft: $tlRadius, '
'topRight: $trRadius, '
'bottomRight: $brRadius, '
'bottomLeft: $blRadius'
')';
}
}
/// An immutable rounded rectangle with the custom radii for all four corners.
class RRect extends _RRectLike<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({
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.
///
@ -1626,120 +1789,254 @@ class RRect {
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static RRect? lerp(RRect? a, RRect? b, double t) {
if (a == null) {
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),
);
}
} 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 b._lerpTo(null, 1 - t);
}
return a._lerpTo(b, t);
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is RRect &&
other.left == left &&
other.top == top &&
other.right == right &&
other.bottom == bottom &&
other.tlRadiusX == tlRadiusX &&
other.tlRadiusY == tlRadiusY &&
other.trRadiusX == trRadiusX &&
other.trRadiusY == trRadiusY &&
other.blRadiusX == blRadiusX &&
other.blRadiusY == blRadiusY &&
other.brRadiusX == brRadiusX &&
other.brRadiusY == brRadiusY;
}
@override
int get hashCode => Object.hash(
left,
top,
right,
bottom,
tlRadiusX,
tlRadiusY,
trRadiusX,
trRadiusY,
blRadiusX,
blRadiusY,
brRadiusX,
brRadiusY,
);
@override
String toString() {
final String rect =
'${left.toStringAsFixed(1)}, '
'${top.toStringAsFixed(1)}, '
'${right.toStringAsFixed(1)}, '
'${bottom.toStringAsFixed(1)}';
if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) {
if (tlRadius.x == tlRadius.y) {
return 'RRect.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})';
return _toString(className: 'RRect');
}
return 'RRect.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})';
}
return 'RRect.fromLTRBAndCorners('
'$rect, '
'topLeft: $tlRadius, '
'topRight: $trRadius, '
'bottomRight: $brRadius, '
'bottomLeft: $blRadius'
')';
/// 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<RSuperellipse> {
/// 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<double>], 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');
}
}

View File

@ -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<Void Function(Pointer<Void>, 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<Void Function(Pointer<Void>, 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<Void Function(Pointer<Void>, Handle, Handle, Handle)>(symbol: 'Canvas::drawRSuperellipse')
external void _drawRSuperellipse(
Float32List rse,
List<Object?>? paintObjects,
ByteData paintData,
);
@override
void drawOval(Rect rect, Paint paint) {
assert(_rectIsValid(rect));

View File

@ -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,

View File

@ -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<Canvas>, 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<Canvas>, 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,

View File

@ -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<flutter::RSuperellipse>::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<flutter::RSuperellipse>::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

View File

@ -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<flutter::RSuperellipse> {
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_

View File

@ -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);

View File

@ -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,

View File

@ -405,7 +405,337 @@ class Radius {
}
}
class RRect {
abstract class _RRectLike<T extends _RRectLike<T>> {
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),
assert(trRadiusY >= 0),
assert(brRadiusX >= 0),
assert(brRadiusY >= 0),
assert(blRadiusX >= 0),
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;
final double bottom;
final double tlRadiusX;
final double tlRadiusY;
Radius get tlRadius => Radius.elliptical(tlRadiusX, tlRadiusY);
final double trRadiusX;
final double trRadiusY;
Radius get trRadius => Radius.elliptical(trRadiusX, trRadiusY);
final double brRadiusX;
final double brRadiusY;
Radius get brRadius => Radius.elliptical(brRadiusX, brRadiusY);
final double blRadiusX;
final double blRadiusY;
// webOnly
final bool webOnlyUniformRadii;
Radius get blRadius => Radius.elliptical(blRadiusX, blRadiusY);
T shift(Offset offset) {
return _create(
left: left + offset.dx,
top: top + offset.dy,
right: right + offset.dx,
bottom: bottom + offset.dy,
tlRadiusX: tlRadiusX,
tlRadiusY: tlRadiusY,
trRadiusX: trRadiusX,
trRadiusY: trRadiusY,
blRadiusX: blRadiusX,
blRadiusY: blRadiusY,
brRadiusX: brRadiusX,
brRadiusY: brRadiusY,
);
}
T inflate(double delta) {
return _create(
left: left - delta,
top: top - delta,
right: right + delta,
bottom: bottom + delta,
tlRadiusX: math.max(0, tlRadiusX + delta),
tlRadiusY: math.max(0, tlRadiusY + delta),
trRadiusX: math.max(0, trRadiusX + delta),
trRadiusY: math.max(0, trRadiusY + delta),
blRadiusX: math.max(0, blRadiusX + delta),
blRadiusY: math.max(0, blRadiusY + delta),
brRadiusX: math.max(0, brRadiusX + delta),
brRadiusY: math.max(0, brRadiusY + 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);
Rect get safeInnerRect {
const double kInsetFactor = 0.29289321881; // 1-cos(pi/4)
final double leftRadius = math.max(blRadiusX, tlRadiusX);
final double topRadius = math.max(tlRadiusY, trRadiusY);
final double rightRadius = math.max(trRadiusX, brRadiusX);
final double bottomRadius = math.max(brRadiusY, blRadiusY);
return Rect.fromLTRB(
left + leftRadius * kInsetFactor,
top + topRadius * kInsetFactor,
right - rightRadius * kInsetFactor,
bottom - bottomRadius * kInsetFactor,
);
}
Rect get middleRect {
final double leftRadius = math.max(blRadiusX, tlRadiusX);
final double topRadius = math.max(tlRadiusY, trRadiusY);
final double rightRadius = math.max(trRadiusX, brRadiusX);
final double bottomRadius = math.max(brRadiusY, blRadiusY);
return Rect.fromLTRB(
left + leftRadius,
top + topRadius,
right - rightRadius,
bottom - bottomRadius,
);
}
Rect get wideMiddleRect {
final double topRadius = math.max(tlRadiusY, trRadiusY);
final double bottomRadius = math.max(brRadiusY, blRadiusY);
return Rect.fromLTRB(left, top + topRadius, right, bottom - bottomRadius);
}
Rect get tallMiddleRect {
final double leftRadius = math.max(blRadiusX, tlRadiusX);
final double rightRadius = math.max(trRadiusX, brRadiusX);
return Rect.fromLTRB(left + leftRadius, top, right - rightRadius, bottom);
}
bool get isEmpty => left >= right || top >= bottom;
bool get isFinite => left.isFinite && top.isFinite && right.isFinite && bottom.isFinite;
bool get isRect {
return (tlRadiusX == 0.0 || tlRadiusY == 0.0) &&
(trRadiusX == 0.0 || trRadiusY == 0.0) &&
(blRadiusX == 0.0 || blRadiusY == 0.0) &&
(brRadiusX == 0.0 || brRadiusY == 0.0);
}
bool get isStadium {
return tlRadius == trRadius &&
trRadius == brRadius &&
brRadius == blRadius &&
(width <= 2.0 * tlRadiusX || height <= 2.0 * tlRadiusY);
}
bool get isEllipse {
return tlRadius == trRadius &&
trRadius == brRadius &&
brRadius == blRadius &&
width <= 2.0 * tlRadiusX &&
height <= 2.0 * tlRadiusY;
}
bool get isCircle => width == height && isEllipse;
double get shortestSide => math.min(width.abs(), height.abs());
double get longestSide => math.max(width.abs(), height.abs());
bool get hasNaN =>
left.isNaN ||
top.isNaN ||
right.isNaN ||
bottom.isNaN ||
trRadiusX.isNaN ||
trRadiusY.isNaN ||
tlRadiusX.isNaN ||
tlRadiusY.isNaN ||
brRadiusX.isNaN ||
brRadiusY.isNaN ||
blRadiusX.isNaN ||
blRadiusY.isNaN;
Offset get center => Offset(left + width / 2.0, top + height / 2.0);
// Returns the minimum between min and scale to which radius1 and radius2
// should be scaled with in order not to exceed the limit.
double _getMin(double min, double radius1, double radius2, double limit) {
final double sum = radius1 + radius2;
if (sum > limit && sum != 0.0) {
return math.min(min, limit / sum);
}
return min;
}
T scaleRadii() {
double scale = 1.0;
final double absWidth = width.abs();
final double absHeight = height.abs();
scale = _getMin(scale, blRadiusY, tlRadiusY, absHeight);
scale = _getMin(scale, tlRadiusX, trRadiusX, absWidth);
scale = _getMin(scale, trRadiusY, brRadiusY, absHeight);
scale = _getMin(scale, brRadiusX, blRadiusX, absWidth);
if (scale < 1.0) {
return _create(
top: top,
left: left,
right: right,
bottom: bottom,
tlRadiusX: tlRadiusX * scale,
tlRadiusY: tlRadiusY * scale,
trRadiusX: trRadiusX * scale,
trRadiusY: trRadiusY * scale,
blRadiusX: blRadiusX * scale,
blRadiusY: blRadiusY * scale,
brRadiusX: brRadiusX * scale,
brRadiusY: brRadiusY * scale,
);
}
return _create(
top: top,
left: left,
right: right,
bottom: bottom,
tlRadiusX: tlRadiusX,
tlRadiusY: tlRadiusY,
trRadiusX: trRadiusX,
trRadiusY: trRadiusY,
blRadiusX: blRadiusX,
blRadiusY: blRadiusY,
brRadiusX: brRadiusX,
brRadiusY: brRadiusY,
);
}
// Linearly interpolate between this object and another of the same shape.
T _lerpTo(T? b, double t) {
assert(runtimeType == T);
if (b == null) {
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 {
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)),
);
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is _RRectLike &&
other.left == left &&
other.top == top &&
other.right == right &&
other.bottom == bottom &&
other.tlRadiusX == tlRadiusX &&
other.tlRadiusY == tlRadiusY &&
other.trRadiusX == trRadiusX &&
other.trRadiusY == trRadiusY &&
other.blRadiusX == blRadiusX &&
other.blRadiusY == blRadiusY &&
other.brRadiusX == brRadiusX &&
other.brRadiusY == brRadiusY;
}
@override
int get hashCode => Object.hash(
left,
top,
right,
bottom,
tlRadiusX,
tlRadiusY,
trRadiusX,
trRadiusY,
blRadiusX,
blRadiusY,
brRadiusX,
brRadiusY,
);
String _toString({required String className}) {
final String rect =
'${left.toStringAsFixed(1)}, '
'${top.toStringAsFixed(1)}, '
'${right.toStringAsFixed(1)}, '
'${bottom.toStringAsFixed(1)}';
if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) {
if (tlRadius.x == tlRadius.y) {
return '$className.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})';
}
return '$className.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})';
}
return '$className.fromLTRBAndCorners('
'$rect, '
'topLeft: $tlRadius, '
'topRight: $trRadius, '
'bottomRight: $brRadius, '
'bottomLeft: $blRadius'
')';
}
}
class RRect extends _RRectLike<RRect> {
const RRect.fromLTRBXY(
double left,
double top,
@ -542,222 +872,52 @@ class RRect {
);
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,
}) : assert(tlRadiusX >= 0),
assert(tlRadiusY >= 0),
assert(trRadiusX >= 0),
assert(trRadiusY >= 0),
assert(brRadiusX >= 0),
assert(brRadiusY >= 0),
assert(blRadiusX >= 0),
assert(blRadiusY >= 0),
webOnlyUniformRadii = uniformRadii;
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,
);
final double left;
final double top;
final double right;
final double bottom;
final double tlRadiusX;
final double tlRadiusY;
Radius get tlRadius => Radius.elliptical(tlRadiusX, tlRadiusY);
final double trRadiusX;
final double trRadiusY;
Radius get trRadius => Radius.elliptical(trRadiusX, trRadiusY);
final double brRadiusX;
final double brRadiusY;
Radius get brRadius => Radius.elliptical(brRadiusX, brRadiusY);
final double blRadiusX;
final double blRadiusY;
// webOnly
final bool webOnlyUniformRadii;
Radius get blRadius => Radius.elliptical(blRadiusX, blRadiusY);
static const RRect zero = RRect._raw();
RRect shift(Offset offset) {
return RRect._raw(
left: left + offset.dx,
top: top + offset.dy,
right: right + offset.dx,
bottom: bottom + offset.dy,
tlRadiusX: tlRadiusX,
tlRadiusY: tlRadiusY,
trRadiusX: trRadiusX,
trRadiusY: trRadiusY,
blRadiusX: blRadiusX,
blRadiusY: blRadiusY,
brRadiusX: brRadiusX,
brRadiusY: brRadiusY,
);
}
RRect inflate(double delta) {
return RRect._raw(
left: left - delta,
top: top - delta,
right: right + delta,
bottom: bottom + delta,
tlRadiusX: math.max(0, tlRadiusX + delta),
tlRadiusY: math.max(0, tlRadiusY + delta),
trRadiusX: math.max(0, trRadiusX + delta),
trRadiusY: math.max(0, trRadiusY + delta),
blRadiusX: math.max(0, blRadiusX + delta),
blRadiusY: math.max(0, blRadiusY + delta),
brRadiusX: math.max(0, brRadiusX + delta),
brRadiusY: math.max(0, brRadiusY + delta),
);
}
RRect deflate(double delta) => inflate(-delta);
double get width => right - left;
double get height => bottom - top;
Rect get outerRect => Rect.fromLTRB(left, top, right, bottom);
Rect get safeInnerRect {
const double kInsetFactor = 0.29289321881; // 1-cos(pi/4)
final double leftRadius = math.max(blRadiusX, tlRadiusX);
final double topRadius = math.max(tlRadiusY, trRadiusY);
final double rightRadius = math.max(trRadiusX, brRadiusX);
final double bottomRadius = math.max(brRadiusY, blRadiusY);
return Rect.fromLTRB(
left + leftRadius * kInsetFactor,
top + topRadius * kInsetFactor,
right - rightRadius * kInsetFactor,
bottom - bottomRadius * kInsetFactor,
);
}
Rect get middleRect {
final double leftRadius = math.max(blRadiusX, tlRadiusX);
final double topRadius = math.max(tlRadiusY, trRadiusY);
final double rightRadius = math.max(trRadiusX, brRadiusX);
final double bottomRadius = math.max(brRadiusY, blRadiusY);
return Rect.fromLTRB(
left + leftRadius,
top + topRadius,
right - rightRadius,
bottom - bottomRadius,
);
}
Rect get wideMiddleRect {
final double topRadius = math.max(tlRadiusY, trRadiusY);
final double bottomRadius = math.max(brRadiusY, blRadiusY);
return Rect.fromLTRB(left, top + topRadius, right, bottom - bottomRadius);
}
Rect get tallMiddleRect {
final double leftRadius = math.max(blRadiusX, tlRadiusX);
final double rightRadius = math.max(trRadiusX, brRadiusX);
return Rect.fromLTRB(left + leftRadius, top, right - rightRadius, bottom);
}
bool get isEmpty => left >= right || top >= bottom;
bool get isFinite => left.isFinite && top.isFinite && right.isFinite && bottom.isFinite;
bool get isRect {
return (tlRadiusX == 0.0 || tlRadiusY == 0.0) &&
(trRadiusX == 0.0 || trRadiusY == 0.0) &&
(blRadiusX == 0.0 || blRadiusY == 0.0) &&
(brRadiusX == 0.0 || brRadiusY == 0.0);
}
bool get isStadium {
return tlRadius == trRadius &&
trRadius == brRadius &&
brRadius == blRadius &&
(width <= 2.0 * tlRadiusX || height <= 2.0 * tlRadiusY);
}
bool get isEllipse {
return tlRadius == trRadius &&
trRadius == brRadius &&
brRadius == blRadius &&
width <= 2.0 * tlRadiusX &&
height <= 2.0 * tlRadiusY;
}
bool get isCircle => width == height && isEllipse;
double get shortestSide => math.min(width.abs(), height.abs());
double get longestSide => math.max(width.abs(), height.abs());
bool get hasNaN =>
left.isNaN ||
top.isNaN ||
right.isNaN ||
bottom.isNaN ||
trRadiusX.isNaN ||
trRadiusY.isNaN ||
tlRadiusX.isNaN ||
tlRadiusY.isNaN ||
brRadiusX.isNaN ||
brRadiusY.isNaN ||
blRadiusX.isNaN ||
blRadiusY.isNaN;
Offset get center => Offset(left + width / 2.0, top + height / 2.0);
// Returns the minimum between min and scale to which radius1 and radius2
// should be scaled with in order not to exceed the limit.
double _getMin(double min, double radius1, double radius2, double limit) {
final double sum = radius1 + radius2;
if (sum > limit && sum != 0.0) {
return math.min(min, limit / sum);
}
return min;
}
RRect scaleRadii() {
double scale = 1.0;
final double absWidth = width.abs();
final double absHeight = height.abs();
scale = _getMin(scale, blRadiusY, tlRadiusY, absHeight);
scale = _getMin(scale, tlRadiusX, trRadiusX, absWidth);
scale = _getMin(scale, trRadiusY, brRadiusY, absHeight);
scale = _getMin(scale, brRadiusX, blRadiusX, absWidth);
if (scale < 1.0) {
return RRect._raw(
top: top,
left: left,
right: right,
bottom: bottom,
tlRadiusX: tlRadiusX * scale,
tlRadiusY: tlRadiusY * scale,
trRadiusX: trRadiusX * scale,
trRadiusY: trRadiusY * scale,
blRadiusX: blRadiusX * scale,
blRadiusY: blRadiusY * scale,
brRadiusX: brRadiusX * scale,
brRadiusY: brRadiusY * scale,
);
}
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,
);
}
bool contains(Offset point) {
if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) {
return false;
@ -805,120 +965,242 @@ class RRect {
}
static RRect? lerp(RRect? a, RRect? b, double t) {
if (a == null) {
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),
);
}
} 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 b._lerpTo(null, 1 - t);
}
return a._lerpTo(b, t);
}
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is RRect &&
other.left == left &&
other.top == top &&
other.right == right &&
other.bottom == bottom &&
other.tlRadiusX == tlRadiusX &&
other.tlRadiusY == tlRadiusY &&
other.trRadiusX == trRadiusX &&
other.trRadiusY == trRadiusY &&
other.blRadiusX == blRadiusX &&
other.blRadiusY == blRadiusY &&
other.brRadiusX == brRadiusX &&
other.brRadiusY == brRadiusY;
}
@override
int get hashCode => Object.hash(
left,
top,
right,
bottom,
tlRadiusX,
tlRadiusY,
trRadiusX,
trRadiusY,
blRadiusX,
blRadiusY,
brRadiusX,
brRadiusY,
);
@override
String toString() {
final String rect =
'${left.toStringAsFixed(1)}, '
'${top.toStringAsFixed(1)}, '
'${right.toStringAsFixed(1)}, '
'${bottom.toStringAsFixed(1)}';
if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) {
if (tlRadius.x == tlRadius.y) {
return 'RRect.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})';
return _toString(className: 'RRect');
}
return 'RRect.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})';
}
return 'RRect.fromLTRBAndCorners('
'$rect, '
'topLeft: $tlRadius, '
'topRight: $trRadius, '
'bottomRight: $brRadius, '
'bottomLeft: $blRadius'
')';
class RSuperellipse extends _RRectLike<RSuperellipse> {
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.

View File

@ -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) {

View File

@ -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);
}

View File

@ -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));

View File

@ -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);

View File

@ -126,6 +126,15 @@ class LayerSceneBuilder implements ui.SceneBuilder {
return pushLayer<ClipRRectEngineLayer>(ClipRRectEngineLayer(rrect, clipBehavior));
}
@override
ClipRSuperellipseEngineLayer pushClipRSuperellipse(
ui.RSuperellipse rse, {
ui.Clip? clipBehavior,
ui.EngineLayer? oldLayer,
}) {
return pushLayer<ClipRSuperellipseEngineLayer>(ClipRSuperellipseEngineLayer(rse, clipBehavior));
}
@override
ClipRectEngineLayer pushClipRect(
ui.Rect rect, {

View File

@ -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);

View File

@ -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 ??= <SaveClipEntry>[];
// 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()`.

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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();

View File

@ -147,6 +147,22 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
return _pushSurface<PersistedClipRRect>(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>(
PersistedClipRSuperellipse(oldLayer, rse, clipBehavior),
);
}
/// Pushes a path clip operation onto the operation stack.
///
/// Rasterization outside the given path is discarded.

View File

@ -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);

View File

@ -385,6 +385,17 @@ class EngineSceneBuilder implements ui.SceneBuilder {
ui.ClipRRectEngineLayer? oldLayer,
}) => pushLayer<ClipRRectLayer>(ClipRRectLayer(ClipRRectOperation(rrect, clipBehavior)));
@override
ui.ClipRSuperellipseEngineLayer pushClipRSuperellipse(
ui.RSuperellipse rse, {
required ui.Clip clipBehavior,
ui.ClipRSuperellipseEngineLayer? oldLayer,
}) {
return pushLayer<ClipRSuperellipseLayer>(
ClipRSuperellipseLayer(ClipRSuperellipseOperation(rse, clipBehavior)),
);
}
@override
ui.ClipRectEngineLayer pushClipRect(
ui.Rect rect, {

View File

@ -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();

View File

@ -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;

View File

@ -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);

View File

@ -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,

View File

@ -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_;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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);
}