aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexey Zerkin <[email protected]>2025-02-26 12:34:54 +0200
committerAlexey Zerkin <[email protected]>2025-09-17 07:09:27 +0300
commitb300f909d71708202ad2e31872926359ecc167d0 (patch)
tree99c3529f83508d7e539d1ad553eda53c475997fb
parentc86476db1fa845b9414e2a83191ed93c1c5a7936 (diff)
Add EllipseShape to QtQuick.Shapes
[ChangeLog][QtQuick.Shapes.DesignHelpers] Added EllipseShape. Fixes: QDS-14729 Fixes: QDS-15302 Change-Id: I0df4d6da0eb3a7cab210210fa3c695f0fe29a412 Reviewed-by: Jan Arve Sæther <[email protected]>
-rw-r--r--src/quick/doc/images/path-ellipseshape.pngbin0 -> 2678 bytes
-rw-r--r--src/quickshapes/designhelpers/CMakeLists.txt3
-rw-r--r--src/quickshapes/designhelpers/doc/snippets/ellipseshape.qml23
-rw-r--r--src/quickshapes/designhelpers/qquickellipseshape.cpp999
-rw-r--r--src/quickshapes/designhelpers/qquickellipseshape_p.h144
-rw-r--r--src/quickshapes/designhelpers/qquickellipseshape_p_p.h149
-rw-r--r--tests/auto/quickshapes/designhelpers/CMakeLists.txt1
-rw-r--r--tests/auto/quickshapes/designhelpers/qquickellipseshape/CMakeLists.txt44
-rw-r--r--tests/auto/quickshapes/designhelpers/qquickellipseshape/data/default.qml12
-rw-r--r--tests/auto/quickshapes/designhelpers/qquickellipseshape/data/ellipseshape1.qml5
-rw-r--r--tests/auto/quickshapes/designhelpers/qquickellipseshape/tst_qquickellipseshape.cpp117
-rw-r--r--tests/baseline/scenegraph/data/designhelpers/designhelpers_ellipseshape.qml223
12 files changed, 1720 insertions, 0 deletions
diff --git a/src/quick/doc/images/path-ellipseshape.png b/src/quick/doc/images/path-ellipseshape.png
new file mode 100644
index 0000000000..17448dd0b8
--- /dev/null
+++ b/src/quick/doc/images/path-ellipseshape.png
Binary files differ
diff --git a/src/quickshapes/designhelpers/CMakeLists.txt b/src/quickshapes/designhelpers/CMakeLists.txt
index 72d0fd2d2d..8f23b53381 100644
--- a/src/quickshapes/designhelpers/CMakeLists.txt
+++ b/src/quickshapes/designhelpers/CMakeLists.txt
@@ -10,6 +10,9 @@ qt_internal_add_qml_module(QuickShapesDesignHelpersPrivate
QtQuick/auto
INTERNAL_MODULE
SOURCES
+ qquickellipseshape.cpp
+ qquickellipseshape_p.h
+ qquickellipseshape_p_p.h
qquickrectangleshape.cpp
qquickrectangleshape_p.h
qquickrectangleshape_p_p.h
diff --git a/src/quickshapes/designhelpers/doc/snippets/ellipseshape.qml b/src/quickshapes/designhelpers/doc/snippets/ellipseshape.qml
new file mode 100644
index 0000000000..d1edda2110
--- /dev/null
+++ b/src/quickshapes/designhelpers/doc/snippets/ellipseshape.qml
@@ -0,0 +1,23 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Shapes
+
+Window {
+ visible: true
+ flags: Qt.FramelessWindowHint
+ width: 100
+ height: 100
+
+//! [ellipseShape]
+ EllipseShape {
+ id: ellipseShape
+ anchors.fill: parent
+ width: 90
+ height: 90
+ startAngle: 0
+ sweepAngle: 270
+ }
+//! [ellipseShape]
+}
diff --git a/src/quickshapes/designhelpers/qquickellipseshape.cpp b/src/quickshapes/designhelpers/qquickellipseshape.cpp
new file mode 100644
index 0000000000..9cad9502c0
--- /dev/null
+++ b/src/quickshapes/designhelpers/qquickellipseshape.cpp
@@ -0,0 +1,999 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qquickellipseshape_p.h"
+#include "qquickellipseshape_p_p.h"
+#include <algorithm>
+#include <QtMath>
+
+QT_BEGIN_NAMESPACE
+
+namespace {
+inline bool is_equal(qreal a, qreal b, qreal epsilon = DBL_EPSILON)
+{
+ return std::fabs(a - b) <= epsilon;
+}
+
+inline qreal arc_angle(qreal angle)
+{
+ return angle - 90;
+}
+
+inline QVector2D arc_point(QVector2D center, QVector2D radius, qreal angle)
+{
+ return QVector2D(center.x() + radius.x() * qCos(qDegreesToRadians(angle)),
+ center.y() + radius.y() * qSin(qDegreesToRadians(angle)));
+}
+
+// counter-clockwise
+inline QVector2D tangent_ccw(QVector2D radius, qreal angle)
+{
+ return QVector2D(-radius.x() * qSin(qDegreesToRadians(angle)),
+ radius.y() * qCos(qDegreesToRadians(angle)));
+}
+
+inline qreal cross(QVector2D a, QVector2D b)
+{
+ return a.x() * b.y() - a.y() * b.x();
+}
+
+// cross product of two vectors defined by points A->B x C->D
+inline qreal cross(QVector2D a, QVector2D b, QVector2D c, QVector2D d)
+{
+ return cross(b - a, d - c);
+}
+
+qreal angle_between_vectors(QVector2D a, QVector2D b)
+{
+ const QVector2D uA = a.normalized();
+ const QVector2D uB = b.normalized();
+ const qreal angle = qAtan2(cross(uA, uB), QVector2D::dotProduct(uA, uB));
+ if (std::fabs(angle) < FLT_EPSILON)
+ return 0.0f;
+ return angle;
+}
+
+// the intersection point can be calculated as C + T * (D - C) or A + S * (B - A)
+bool lines_intersect(QVector2D a, QVector2D b, QVector2D c, QVector2D d, qreal *s, qreal *t)
+{
+ // lines undefined
+ if ((a.x() == b.x() && a.y() == b.y()) || (c.x() == d.x() && c.y() == d.y()))
+ return false;
+
+ const qreal denom = cross(a, b, c, d);
+
+ // lines are parallel or overlap
+ if (denom == 0)
+ return false;
+
+ if (s != nullptr)
+ *s = cross(c, d, c, a) / denom;
+ if (t != nullptr)
+ *t = cross(a, b, c, a) / denom;
+
+ return true;
+}
+} // namespace
+
+QQuickEllipseShapePrivate::QQuickEllipseShapePrivate() = default;
+
+QQuickEllipseShapePrivate::~QQuickEllipseShapePrivate() = default;
+
+void QQuickEllipseShapePrivate::addLine(QVector2D point)
+{
+ auto line = new QQuickPathLine(path);
+ line->setX(point.x());
+ line->setY(point.y());
+ QQuickPathPrivate::get(path)->appendPathElement(line);
+}
+
+void QQuickEllipseShapePrivate::addArc(QVector2D point, QVector2D arcRadius,
+ QQuickPathArc::ArcDirection dir, bool largeArc)
+{
+ auto arc = new QQuickPathArc(path);
+ arc->setX(point.x());
+ arc->setY(point.y());
+ arc->setRadiusX(arcRadius.x());
+ arc->setRadiusY(arcRadius.y());
+ arc->setDirection(dir);
+ arc->setUseLargeArc(largeArc);
+ QQuickPathPrivate::get(path)->appendPathElement(arc);
+}
+
+qreal QQuickEllipseShapePrivate::getBorderOffset() const
+{
+ if (QQuickEllipseShape::BorderMode::Middle == borderMode)
+ return 0;
+ else if (QQuickEllipseShape::BorderMode::Outside == borderMode)
+ return -path->strokeWidth() * 0.5;
+ return path->strokeWidth() * 0.5f; // inside
+}
+
+void QQuickEllipseShapePrivate::roundCenter(QVector2D center, QVector2D ellipseRadius)
+{
+ const qreal endAngle = arc_angle(startAngle + sweepAngle);
+ const QVector2D endPoint = arc_point(center, ellipseRadius, endAngle);
+ const qreal beginAngle = arc_angle(startAngle);
+ const QVector2D beginPoint = arc_point(center, ellipseRadius, beginAngle);
+
+ const QVector2D AB = endPoint - center;
+ const QVector2D AC = beginPoint - center;
+
+ const qreal a = angle_between_vectors(AB, AC);
+ const qreal halfAngle = std::fabs(a) * 0.5f;
+
+ const qreal maxCornerRadius = (std::min(AB.length(), AC.length()) * 0.5f) * qTan(halfAngle);
+ const qreal corner_radius = std::min(cornerRadius, maxCornerRadius);
+
+ // calculate B and C based on the corner radius
+ const qreal edgeOffset = corner_radius / qTan(halfAngle);
+ const QVector2D B = center + (AB.normalized() * edgeOffset);
+ const QVector2D C = center + (AC.normalized() * edgeOffset);
+
+ // update
+ auto &rc = roundedCorners[RoundedCornerIndex::Center];
+ rc.A = center;
+ rc.B = B;
+ rc.C = C;
+ rc.alpha = a;
+ rc.radius = corner_radius;
+}
+
+void QQuickEllipseShapePrivate::roundBeginEnd(QVector2D center, QVector2D ellipseRadius)
+{
+ qreal deg = 0.0f;
+ const qreal endAngle = startAngle + sweepAngle;
+ bool e_outer = false, b_outer = false, e_inner = false, b_inner = false;
+ while (deg < 45.0f) {
+ deg += 1.0f;
+ if (e_outer && b_outer && e_inner && b_inner)
+ break;
+ if (!b_outer)
+ b_outer = roundOuter(center, ellipseRadius, deg, startAngle, endAngle,
+ RoundedCornerIndex::OuterBegin);
+ if (!e_outer)
+ e_outer = roundOuter(center, ellipseRadius, deg, endAngle, startAngle,
+ RoundedCornerIndex::OuterEnd);
+ if (!e_inner)
+ e_inner = roundInner(center, ellipseRadius, deg, endAngle, startAngle,
+ RoundedCornerIndex::InnerEnd);
+ if (!b_inner)
+ b_inner = roundInner(center, ellipseRadius, deg, startAngle, endAngle,
+ RoundedCornerIndex::InnerBegin);
+ }
+}
+
+bool QQuickEllipseShapePrivate::roundOuter(QVector2D center, QVector2D ellipseRadius, qreal deg,
+ qreal arcAngle1, qreal arcAngle2,
+ RoundedCornerIndex index)
+{
+ bool done = false;
+
+ const qreal arcAngle = arc_angle(arcAngle1);
+ const QVector2D arcPoint = arc_point(center, ellipseRadius, arcAngle);
+
+ const qreal angle = arcAngle1 > arcAngle2 ? arcAngle - deg : arcAngle + deg;
+
+ const QVector2D B = arc_point(center, ellipseRadius, angle); // point on arc
+
+ // calculate tangent vector
+ const QVector2D uV = tangent_ccw(ellipseRadius, angle).normalized();
+ const QVector2D b1 = B + uV;
+
+ qreal s = 0, t = 0;
+ bool res = lines_intersect(center, arcPoint, B, b1, &s, &t);
+ if (res) {
+ const QVector2D A = center + s * (arcPoint - center);
+
+ const QVector2D AB = B - A;
+ const QVector2D AC = center - A;
+
+ const qreal a = angle_between_vectors(AB, AC);
+ const qreal halfAngle = std::fabs(a) * 0.5;
+ const qreal edgeOffset = AB.length();
+
+ const qreal corner_radius = edgeOffset * qTan(halfAngle);
+
+ // constrain by sweep
+ const qreal sweep = std::fabs(arcAngle2 - arcAngle1) * 0.5;
+ const qreal degMax = std::min(sweep, 45.0);
+
+ const QVector2D C = A + AC.normalized() * edgeOffset;
+
+ const qreal ptoc = (arcPoint - C).length();
+ qreal edge = 0;
+ if (innerArcRatio > 0) {
+ const QVector2D ellipseInnerRadius(innerArcRatio * ellipseRadius.x(),
+ innerArcRatio * ellipseRadius.y());
+ const QVector2D innerArcPoint =
+ arc_point(center, ellipseInnerRadius, arcAngle); // point on the inner arc
+ edge = (arcPoint - innerArcPoint).length();
+ } else {
+ edge = (arcPoint - center).length();
+ }
+
+ const qreal diff = std::fabs(corner_radius - cornerRadius); // closest to target radius
+
+ auto &rc = roundedCorners[index];
+
+ bool canUpdate = diff < rc.diff && !(corner_radius > cornerRadius) && !(deg > degMax)
+ && !(rc.radius > corner_radius) && !(ptoc > edge * 0.5f);
+
+ // 1st loop or if constraints are met
+ if (rc.radius == 0 || canUpdate)
+ rc.update(diff, A, B, C, a, corner_radius);
+
+ done =
+ // corner radius is bigger or equal to the target radius
+ corner_radius > cornerRadius
+ || is_equal(corner_radius, cornerRadius, 0.01f)
+ // angle used to define point B is bigger than available sweep
+ || deg > degMax
+ || is_equal(deg, degMax, 0.01f)
+ // the corner radius starts to decline (from previously calculated)
+ || (rc.radius != 0 && rc.radius > corner_radius)
+ // point C is beyond the half of the ellipse edge
+ // (closer to the ellipse center or inner arc point)
+ || (ptoc > edge * 0.5f);
+ }
+
+ return done;
+}
+
+bool QQuickEllipseShapePrivate::roundInner(QVector2D center, QVector2D ellipseRadius, qreal deg,
+ qreal arcAngle1, qreal arcAngle2,
+ RoundedCornerIndex index)
+{
+ // make rounding corner bigger and produces smoother result
+ const qreal smoothFactor = 1.5;
+
+ deg *= smoothFactor;
+
+ bool done = false;
+
+ const QVector2D ellipseInnerRadius(innerArcRatio * ellipseRadius.x(),
+ innerArcRatio * ellipseRadius.y());
+
+ const qreal arcAngle = arc_angle(arcAngle1);
+ const QVector2D innerArcPoint =
+ arc_point(center, ellipseInnerRadius, arcAngle); // point on the inner arc
+
+ const qreal angle = arcAngle1 > arcAngle2 ? arcAngle - deg : arcAngle + deg;
+
+ const QVector2D B = arc_point(center, ellipseInnerRadius, angle); // point on arc
+
+ // calculate tangent vector
+ const QVector2D uV = tangent_ccw(ellipseInnerRadius, angle).normalized();
+ const QVector2D b1 = B + uV;
+
+ qreal s = 0, t = 0;
+ bool res = lines_intersect(center, innerArcPoint, B, b1, &s, &t);
+
+ if (res) {
+ // hit point
+ const QVector2D A = center + s * (innerArcPoint - center); // point on edge
+
+ const auto arcPoint = arc_point(center, ellipseRadius, arcAngle);
+
+ const QVector2D AB = B - A;
+ const QVector2D AC = A - innerArcPoint;
+
+ const qreal a = angle_between_vectors(AB, AC);
+ const qreal halfAngle = std::fabs(a) * 0.5;
+ const qreal edgeOffset = AB.length();
+
+ const qreal corner_radius = edgeOffset * qTan(halfAngle);
+
+ // constrain by sweep
+ const qreal sweep = std::fabs(arcAngle2 - arcAngle1) * 0.5;
+ const qreal degMax = std::min(sweep, 45.0);
+
+ const QVector2D C = A + AC.normalized() * edgeOffset;
+
+ const qreal ptoc = (innerArcPoint - C).length();
+ const qreal edge = (innerArcPoint - arcPoint).length();
+
+ const qreal diff =
+ std::fabs(corner_radius - cornerRadius * smoothFactor); // closest to target radius
+
+ auto &rc = roundedCorners[index];
+
+ bool canUpdate = diff < rc.diff && !(corner_radius > cornerRadius * smoothFactor)
+ && !(deg > degMax) && !(rc.radius > corner_radius) && !(ptoc > edge * 0.5f);
+
+ // 1st loop or if constraints are met
+ if (rc.radius == 0 || canUpdate)
+ rc.update(diff, A, B, C, a, corner_radius);
+
+ done =
+ // corner radius is bigger or equal to the target radius
+ corner_radius > cornerRadius * smoothFactor
+ || is_equal(corner_radius, cornerRadius * smoothFactor, 0.01f)
+ // angle used to define point B is bigger than available sweep
+ || deg > degMax
+ || is_equal(deg, degMax, 0.01f)
+ // the corner radius starts to decline (from previously calculated)
+ || (rc.radius != 0 && rc.radius > corner_radius)
+ // point C is beyond the half of the ellipse edge
+ // (closer to the inner arc end point)
+ || (ptoc > edge * 0.5f);
+ }
+
+ return done;
+}
+
+void QQuickEllipseShapePrivate::drawCenterCorner()
+{
+ auto &rc = roundedCorners[RoundedCornerIndex::Center];
+ path->setStartX(rc.B.x());
+ path->setStartY(rc.B.y());
+
+ addArc(rc.C, QVector2D(rc.radius, rc.radius),
+ rc.alpha < 0 ? QQuickPathArc::Clockwise : QQuickPathArc::Counterclockwise);
+}
+
+void QQuickEllipseShapePrivate::drawInnerEndCorner()
+{
+ auto &rc = roundedCorners[RoundedCornerIndex::InnerEnd];
+
+ addLine(rc.C);
+
+ addArc(rc.B, QVector2D(rc.radius, rc.radius),
+ rc.alpha > 0 ? QQuickPathArc::Clockwise : QQuickPathArc::Counterclockwise);
+}
+
+void QQuickEllipseShapePrivate::drawInnerBeginCorner()
+{
+ auto &rc = roundedCorners[RoundedCornerIndex::InnerBegin];
+ path->setStartX(rc.B.x());
+ path->setStartY(rc.B.y());
+
+ addArc(rc.C, QVector2D(rc.radius, rc.radius),
+ rc.alpha < 0 ? QQuickPathArc::Clockwise : QQuickPathArc::Counterclockwise);
+}
+
+void QQuickEllipseShapePrivate::drawOuterBeginCorner()
+{
+ auto &rc = roundedCorners[RoundedCornerIndex::OuterBegin];
+
+ addLine(rc.C);
+
+ addArc(rc.B, QVector2D(rc.radius, rc.radius),
+ rc.alpha > 0 ? QQuickPathArc::Clockwise : QQuickPathArc::Counterclockwise);
+}
+
+void QQuickEllipseShapePrivate::drawOuterArcRounded(QVector2D center, QVector2D ellipseRadius)
+{
+ // split outer arc in two parts to avoid issues of the large arc
+ const qreal endAngle = startAngle + sweepAngle;
+
+ const qreal angle = startAngle > endAngle
+ ? arc_angle(startAngle - std::fabs(sweepAngle * 0.5f))
+ : arc_angle(startAngle + std::fabs(sweepAngle * 0.5f));
+ const auto point = arc_point(center, ellipseRadius, angle); // mid point of the arc
+
+ // from begin to mid point
+ addArc(point, ellipseRadius,
+ sweepAngle > 0.0f ? QQuickPathArc::Clockwise : QQuickPathArc::Counterclockwise);
+
+ auto &rc = roundedCorners[RoundedCornerIndex::OuterEnd];
+
+ // from mid point to end rounded corner
+ addArc(rc.B, ellipseRadius,
+ sweepAngle > 0.0f ? QQuickPathArc::Clockwise : QQuickPathArc::Counterclockwise);
+
+ // rounded corner
+ addArc(rc.C, QVector2D(rc.radius, rc.radius),
+ rc.alpha < 0 ? QQuickPathArc::Clockwise : QQuickPathArc::Counterclockwise);
+}
+
+void QQuickEllipseShapePrivate::drawInnerArcRounded(QVector2D center, QVector2D ellipseRadius)
+{
+ // split inner arc in two parts to avoid issues of the large arc
+ const qreal endAngle = startAngle + sweepAngle;
+
+ const QVector2D ellipseInnerRadius(innerArcRatio * ellipseRadius.x(),
+ innerArcRatio * ellipseRadius.y());
+
+ const qreal angle = endAngle > startAngle ? arc_angle(endAngle - std::fabs(sweepAngle * 0.5f))
+ : arc_angle(endAngle + std::fabs(sweepAngle * 0.5f));
+ const auto point = arc_point(center, ellipseInnerRadius, angle); // mid point of the arc
+
+ // from end to mid point
+ addArc(point, ellipseInnerRadius,
+ sweepAngle > 0.0f ? QQuickPathArc::Counterclockwise : QQuickPathArc::Clockwise);
+
+ // from mid point to begin rounded corner
+ auto &rc = roundedCorners[RoundedCornerIndex::InnerBegin];
+ addArc(rc.B, ellipseInnerRadius,
+ sweepAngle > 0.0f ? QQuickPathArc::Counterclockwise : QQuickPathArc::Clockwise);
+}
+
+void QQuickEllipseShapePrivate::drawOuterArc(QVector2D center, QVector2D ellipseRadius)
+{
+ const qreal beginAngle = arc_angle(startAngle);
+ const qreal endAngle = arc_angle(startAngle + sweepAngle);
+
+ const qreal alpha = std::clamp(std::fabs(sweepAngle), 0.0, 359.9);
+ bool isFull = (alpha <= 0.0f || alpha >= 359.0f);
+
+ // QQuickPathArc has some weird behavior when it starts and ends at the same point
+ // leave some gap between the start and the end points in order to avoid it
+ const auto beginPoint = arc_point(center, ellipseRadius, isFull ? 0 : beginAngle);
+ const auto endPoint = arc_point(center, ellipseRadius, isFull ? 359.9 : endAngle);
+
+ path->setStartX(beginPoint.x());
+ path->setStartY(beginPoint.y());
+
+ addArc(endPoint, ellipseRadius,
+ isFull ? QQuickPathArc::Clockwise
+ : (sweepAngle > 0.0f ? QQuickPathArc::Clockwise
+ : QQuickPathArc::Counterclockwise),
+ isFull ? true : alpha > 180.0f);
+}
+
+void QQuickEllipseShapePrivate::drawFullInnerArc(QVector2D center, QVector2D ellipseRadius)
+{
+ const qreal beginAngle = arc_angle(startAngle);
+
+ auto arc = new QQuickPathAngleArc(path);
+ arc->setCenterX(center.x());
+ arc->setCenterY(center.y());
+ arc->setStartAngle(beginAngle);
+ arc->setRadiusX(innerArcRatio * ellipseRadius.x());
+ arc->setRadiusY(innerArcRatio * ellipseRadius.y());
+ arc->setSweepAngle(sweepAngle);
+ QQuickPathPrivate::get(path)->appendPathElement(arc);
+}
+
+void QQuickEllipseShapePrivate::drawWithInnerRadius(QVector2D center, QVector2D ellipseRadius)
+{
+ drawInnerBeginCorner(); // path starts at the begin rounded corner on the inner arc
+
+ drawOuterBeginCorner(); // path continues to the begin rounded corner on the outer arc
+
+ // outer arc connecting begin and end rounded corners
+ drawOuterArcRounded(center, ellipseRadius);
+
+ // path continues to the end rounded corner on the inner arc
+ drawInnerEndCorner();
+
+ // inner arc connecting end and begin rounded corners
+ drawInnerArcRounded(center, ellipseRadius);
+}
+
+void QQuickEllipseShapePrivate::drawWithoutInnerRadius(QVector2D center, QVector2D ellipseRadius)
+{
+ drawCenterCorner(); // path starts at rounded corner of ellipse center
+
+ drawOuterBeginCorner(); // path continues to the begin rounded corner on the outer arc
+
+ // outer arc connecting begin and end rounded corners
+ drawOuterArcRounded(center, ellipseRadius);
+
+ // path ends at the ellipse's center rounded corner
+ const auto &rc = roundedCorners[RoundedCornerIndex::Center];
+ addLine(rc.B);
+}
+
+void QQuickEllipseShapePrivate::updatePath()
+{
+ const qreal borderOffset = getBorderOffset();
+ const QVector2D center =
+ QVector2D(width.valueBypassingBindings(), height.valueBypassingBindings()) * 0.5f;
+ const QVector2D ellipseRadius = center - QVector2D(borderOffset, borderOffset);
+
+ QQuickPathPrivate::get(path)->clearPathElements(QQuickPathPrivate::DeleteElementPolicy::Delete);
+
+ const qreal alpha = std::clamp(std::fabs(sweepAngle), 0.0, 359.9);
+ const bool isFull = alpha >= 359.0;
+
+ if (qFuzzyCompare(alpha, 0))
+ return;
+
+ // just an arc
+ if (qFuzzyCompare(innerArcRatio, 1) || (hideLine && qFuzzyCompare(innerArcRatio, 0))) {
+ drawOuterArc(center, ellipseRadius);
+ return;
+ }
+
+ roundedCorners.reset(); // cleanup old results
+
+ if (innerArcRatio != 0 && isFull) {
+ // this is a donut
+ drawOuterArc(center, ellipseRadius);
+ drawFullInnerArc(center, ellipseRadius);
+ } else if (innerArcRatio != 0 && !isFull) {
+ // this is an outlined arc
+ roundBeginEnd(center, ellipseRadius);
+ drawWithInnerRadius(center, ellipseRadius);
+ } else if (!isFull) {
+ // this is a pie
+ roundCenter(center, ellipseRadius);
+ roundBeginEnd(center, ellipseRadius);
+ drawWithoutInnerRadius(center, ellipseRadius);
+ } else {
+ drawOuterArc(center, ellipseRadius);
+ }
+}
+
+/*!
+ \qmltype EllipseShape
+ \inqmlmodule QtQuick.Shapes.DesignHelpers
+ \brief A shape component that can render an ellipse, an arc, or a pie slice.
+ \since QtQuick 6.10
+
+ The EllipseShape item paints an ellipse, which can be customized to appear
+ as a full ellipse, an arc, or a filled pie slice. Its appearance is
+ controlled by the \l startAngle and \l sweepAngle properties.
+
+ \section1 Basic Ellipse
+ By default, the item renders a full ellipse. The interior is filled with the
+ \l fillColor, and the outline is drawn according to the \l strokeColor, \l
+ strokeWidth, and \l strokeStyle properties.
+
+ \section1 Arc and Pie Slices
+ To create an arc or a pie slice, set the \l startAngle (0-360 degrees) and
+ \l sweepAngle (0-360 degrees) to define the segment of the ellipse to draw.
+
+ \b {Arc Mode}: To create a simple arc (just the outline), set the \l
+ fillColor to \c "transparent". The arc's line style can be customized with
+ \l dashPattern and \l dashOffset.
+
+ \b {Pie Mode}: To create a filled pie slice (a segment connected to the
+ center), simply set the \l fillColor. The outline of the slice will also be
+ stroked.
+
+ \b {Donut Mode}: To create a donut ring (a hollow ellipse), set the
+ \l innerArcRatio to a value between 0.0 and 1.0. This defines the ratio of
+ the inner ellipse's radius to the outer ellipse's radius.
+
+ The area inside the stroke is painted using either a solid fill color,
+ specified using the \l fillColor property, or a gradient, defined using one
+ of the \l ShapeGradient subtypes and set using the \l fillGradient
+ property. If both a color and a gradient are specified, the gradient is
+ used.
+
+ An optional border can be added to an ellipse with its own color and
+ thickness by setting the \l strokeColor and \l strokeWidth properties.
+ Setting the color to \c transparent creates a border without a fill color.
+
+ Ellipse can be drawn with rounded corners using the \l cornerRadius
+ property. The default value of the \l cornerRadius is 10 degrees.
+
+ EllipseShape's default value for \l preferredRendererType is
+ \c Shape.CurveRenderer.
+
+ \section1 Example Usage
+
+ \snippet ellipseshape.qml ellipseShape
+
+ \image path-ellipseshape.png
+*/
+QQuickEllipseShape::QQuickEllipseShape(QQuickItem *parent)
+ : QQuickShape(*(new QQuickEllipseShapePrivate), parent)
+{
+ Q_D(QQuickEllipseShape);
+
+ setPreferredRendererType(CurveRenderer);
+
+ setWidth(100);
+ setHeight(100);
+
+ d->path = new QQuickShapePath(this);
+ d->path->setParent(this);
+ d->path->setAsynchronous(true);
+ d->path->setStrokeWidth(4);
+ d->path->setStrokeColor(QColorConstants::Black);
+
+ d->sp.append(d->path);
+ d->path->setParent(this);
+ d->extra.value().resourcesList.append(d->path);
+}
+
+QQuickEllipseShape::~QQuickEllipseShape() = default;
+
+bool QQuickEllipseShape::hideLine() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->hideLine;
+}
+
+void QQuickEllipseShape::setHideLine(bool hideLine)
+{
+ Q_D(QQuickEllipseShape);
+ if (d->hideLine == hideLine)
+ return;
+ d->hideLine = hideLine;
+ d->updatePath();
+ emit hideLineChanged();
+}
+
+/*!
+ \qmlproperty real QtQuick.Shapes.DesignHelpers::EllipseShape::sweepAngle
+
+ The angular extent in degrees to be drawn from the \l startAngle.
+
+ If set to positive value, the arc is drawn in clockwise direction.
+ If set to negative value, the arc is drawn in counter-clockwise direction.
+
+ The default value is \c 360.
+*/
+qreal QQuickEllipseShape::sweepAngle() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->sweepAngle;
+}
+
+void QQuickEllipseShape::setSweepAngle(qreal sweepAngle)
+{
+ Q_D(QQuickEllipseShape);
+ if (qFuzzyCompare(d->sweepAngle, sweepAngle))
+ return;
+ d->sweepAngle = sweepAngle;
+ d->updatePath();
+ emit sweepAngleChanged();
+}
+
+/*!
+ \qmlproperty real QtQuick.Shapes.DesignHelpers::EllipseShape::startAngle
+
+ The property defines the starting angle in degrees from which to begin
+ drawing the ellipse.
+
+ 0 degrees points to the top. Angle increases in clockwise direction.
+
+ The default value is \c 0.
+*/
+qreal QQuickEllipseShape::startAngle() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->startAngle;
+}
+
+void QQuickEllipseShape::setStartAngle(qreal startAngle)
+{
+ Q_D(QQuickEllipseShape);
+ if (qFuzzyCompare(d->startAngle, startAngle))
+ return;
+ d->startAngle = startAngle;
+ d->updatePath();
+ emit startAngleChanged();
+}
+
+/*!
+ \include shapepath.qdocinc {dashOffset-property} {QtQuick.Shapes.DesignHelpers::EllipseShape}
+*/
+
+qreal QQuickEllipseShape::dashOffset() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->dashOffset();
+}
+
+void QQuickEllipseShape::setDashOffset(qreal offset)
+{
+ Q_D(QQuickEllipseShape);
+ if (qFuzzyCompare(d->path->dashOffset(), offset))
+ return;
+ d->path->setDashOffset(offset);
+ d->updatePath();
+ emit dashOffsetChanged();
+}
+
+/*!
+ \qmlproperty real QtQuick.Shapes.DesignHelpers::EllipseShape::cornerRadius
+
+ Controls the rounding of corners where the radial lines meet the elliptical
+ arcs. For pie segments, this rounds the connection to the outer arc. For
+ donut segments, this also rounds the connections to both inner and outer arcs.
+
+ The default value is \c 10.
+*/
+qreal QQuickEllipseShape::cornerRadius() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->cornerRadius;
+}
+
+void QQuickEllipseShape::setCornerRadius(qreal cornerRadius)
+{
+ Q_D(QQuickEllipseShape);
+ if (qFuzzyCompare(d->cornerRadius, cornerRadius))
+ return;
+ d->cornerRadius = cornerRadius;
+ d->updatePath();
+ emit cornerRadiusChanged();
+}
+
+/*!
+ \qmlproperty real QtQuick.Shapes.DesignHelpers::EllipseShape::innerArcRatio
+
+ This property defines the ratio between the inner and outer arcs.
+
+ Value range is between 0.0 and 1.0. Setting the value to 0.0 will cause
+ the inner arc to collapse toward the center, drawing a solid filled
+ ellipse. Setting the value to 1.0 makes the inner arc the same size as the
+ outer ellipse, resulting in just an arc. Values between 0.0 and 1.0 create
+ hollow elliptical rings.
+
+ The default value is \c 0.
+*/
+qreal QQuickEllipseShape::innerArcRatio() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->innerArcRatio;
+}
+
+void QQuickEllipseShape::setInnerArcRatio(qreal innerArcRatio)
+{
+ Q_D(QQuickEllipseShape);
+ if (qFuzzyCompare(d->innerArcRatio, innerArcRatio))
+ return;
+ d->innerArcRatio = innerArcRatio;
+ d->updatePath();
+ emit innerArcRatioChanged();
+}
+
+/*!
+ \qmlproperty real QtQuick.Shapes.DesignHelpers::EllipseShape::strokeWidth
+
+ This property holds the stroke width.
+
+ When set to a negative value, no stroking occurs.
+
+ The default value is \c 1.
+*/
+
+qreal QQuickEllipseShape::strokeWidth() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->strokeWidth();
+}
+
+void QQuickEllipseShape::setStrokeWidth(qreal width)
+{
+ Q_D(QQuickEllipseShape);
+ if (qFuzzyCompare(d->path->strokeWidth(), width))
+ return;
+ d->path->setStrokeWidth(width);
+ d->updatePath();
+ emit strokeWidthChanged();
+}
+
+/*!
+ \qmlproperty color QtQuick.Shapes.DesignHelpers::EllipseShape::fillColor
+
+ This property holds the fill color.
+
+ When set to \c transparent, no filling occurs.
+
+ The default value is \c "white".
+
+ \note If either \l fillGradient is set to something other than \c null, it
+ will be used instead of \c fillColor.
+*/
+
+QColor QQuickEllipseShape::fillColor() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->fillColor();
+}
+
+void QQuickEllipseShape::setFillColor(const QColor &color)
+{
+ Q_D(QQuickEllipseShape);
+ d->path->setFillColor(color);
+ d->updatePath();
+ emit fillColorChanged();
+}
+
+/*!
+ \qmlproperty color QtQuick.Shapes.DesignHelpers::EllipseShape::strokeColor
+
+ This property holds the stroking color.
+
+ When set to \c transparent, no stroking occurs.
+
+ The default value is \c "black".
+*/
+
+QColor QQuickEllipseShape::strokeColor() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->strokeColor();
+}
+
+void QQuickEllipseShape::setStrokeColor(const QColor &color)
+{
+ Q_D(QQuickEllipseShape);
+ d->path->setStrokeColor(color);
+ d->updatePath();
+ emit strokeColorChanged();
+}
+
+/*!
+ \include shapepath.qdocinc {capStyle-property} {QtQuick.Shapes.DesignHelpers::EllipseShape}
+*/
+
+QQuickShapePath::CapStyle QQuickEllipseShape::capStyle() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->capStyle();
+}
+
+void QQuickEllipseShape::setCapStyle(QQuickShapePath::CapStyle style)
+{
+ Q_D(QQuickEllipseShape);
+ if (d->path->capStyle() == style)
+ return;
+ d->path->setCapStyle(style);
+ d->updatePath();
+ emit capStyleChanged();
+}
+
+/*!
+ \include shapepath.qdocinc {joinStyle-property} {QtQuick.Shapes.DesignHelpers::EllipseShape}
+*/
+
+QQuickShapePath::JoinStyle QQuickEllipseShape::joinStyle() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->joinStyle();
+}
+
+void QQuickEllipseShape::setJoinStyle(QQuickShapePath::JoinStyle style)
+{
+ Q_D(QQuickEllipseShape);
+ if (d->path->joinStyle() == style)
+ return;
+ d->path->setJoinStyle(style);
+ d->updatePath();
+ emit joinStyleChanged();
+}
+
+/*!
+ \include shapepath.qdocinc {strokeStyle-property} {QtQuick.Shapes.DesignHelpers::EllipseShape}
+*/
+
+QQuickShapePath::StrokeStyle QQuickEllipseShape::strokeStyle() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->strokeStyle();
+}
+
+void QQuickEllipseShape::setStrokeStyle(QQuickShapePath::StrokeStyle style)
+{
+ Q_D(QQuickEllipseShape);
+ if (d->path->strokeStyle() == style)
+ return;
+ d->path->setStrokeStyle(style);
+ d->updatePath();
+ emit strokeStyleChanged();
+}
+
+/*!
+ \include shapepath.qdocinc {fillRule-property} {QtQuick.Shapes.DesignHelpers::EllipseShape}
+*/
+
+QQuickShapePath::FillRule QQuickEllipseShape::fillRule() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->fillRule();
+}
+
+void QQuickEllipseShape::setFillRule(QQuickShapePath::FillRule fillRule)
+{
+ Q_D(QQuickEllipseShape);
+ if (d->path->fillRule() == fillRule)
+ return;
+ d->path->setFillRule(fillRule);
+ d->updatePath();
+ emit fillRuleChanged();
+}
+
+/*!
+ \include shapepath.qdocinc {dashPattern-property} {QtQuick.Shapes.DesignHelpers::EllipseShape}
+*/
+
+QVector<qreal> QQuickEllipseShape::dashPattern() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->dashPattern();
+}
+
+void QQuickEllipseShape::setDashPattern(const QVector<qreal> &array)
+{
+ Q_D(QQuickEllipseShape);
+ d->path->setDashPattern(array);
+ d->updatePath();
+ emit dashPatternChanged();
+}
+
+/*!
+ \qmlproperty ShapeGradient QtQuick.Shapes.DesignHelpers::EllipseShape::fillGradient
+
+ The fillGradient of the ellipse fill color.
+
+ By default, no fillGradient is enabled and the value is null. In this case, the
+ fill uses a solid color based on the value of \l fillColor.
+
+ When set, \l fillColor is ignored and filling is done using one of the
+ \l ShapeGradient subtypes.
+
+ \note The \l Gradient type cannot be used here. Rather, prefer using one of
+ the advanced subtypes, like \l LinearGradient.
+*/
+QQuickShapeGradient *QQuickEllipseShape::fillGradient() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->path->fillGradient();
+}
+
+void QQuickEllipseShape::setFillGradient(QQuickShapeGradient *fillGradient)
+{
+ Q_D(QQuickEllipseShape);
+ d->path->setFillGradient(fillGradient);
+ d->updatePath();
+ emit gradientChanged();
+}
+
+void QQuickEllipseShape::resetFillGradient()
+{
+ setFillGradient(nullptr);
+}
+
+/*!
+ \qmlproperty enumeration QtQuick.Shapes.DesignHelpers::EllipseShape::borderMode
+
+ The \l borderMode property determines where the border is drawn along the
+ edge of the ellipse.
+
+ \value EllipseShape.Inside
+ The border is drawn along the inside edge of the item and does not
+ affect the item width.
+
+ This is the default value.
+ \value EllipseShape.Middle
+ The border is drawn over the edge of the item and does not
+ affect the item width.
+ \value EllipseShape.Outside
+ The border is drawn along the outside edge of the item and increases
+ the item width by the value of \l strokeWidth.
+
+ \sa strokeWidth
+*/
+QQuickEllipseShape::BorderMode QQuickEllipseShape::borderMode() const
+{
+ Q_D(const QQuickEllipseShape);
+ return d->borderMode;
+}
+
+void QQuickEllipseShape::setBorderMode(BorderMode borderMode)
+{
+ Q_D(QQuickEllipseShape);
+ if (borderMode == d->borderMode)
+ return;
+ d->borderMode = borderMode;
+ d->updatePath();
+ emit borderModeChanged();
+}
+
+void QQuickEllipseShape::resetBorderMode()
+{
+ setBorderMode(BorderMode::Inside);
+}
+
+void QQuickEllipseShape::itemChange(ItemChange change, const ItemChangeData &value)
+{
+ Q_D(QQuickEllipseShape);
+
+ if (d->path)
+ d->updatePath();
+
+ QQuickItem::itemChange(change, value);
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qquickellipseshape_p.cpp"
diff --git a/src/quickshapes/designhelpers/qquickellipseshape_p.h b/src/quickshapes/designhelpers/qquickellipseshape_p.h
new file mode 100644
index 0000000000..0e11a79cb7
--- /dev/null
+++ b/src/quickshapes/designhelpers/qquickellipseshape_p.h
@@ -0,0 +1,144 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKELLIPSESHAPE_P_H
+#define QQUICKELLIPSESHAPE_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtQuickShapes/private/qquickshape_p.h>
+#include <QtQuickShapesDesignHelpers/qtquickshapesdesignhelpersexports.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQuickEllipseShapePrivate;
+
+class Q_QUICKSHAPESDESIGNHELPERS_EXPORT QQuickEllipseShape : public QQuickShape
+{
+public:
+ Q_OBJECT
+ Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged FINAL)
+ Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged FINAL)
+ Q_PROPERTY(qreal dashOffset READ dashOffset WRITE setDashOffset NOTIFY dashOffsetChanged FINAL)
+ Q_PROPERTY(qreal innerArcRatio READ innerArcRatio WRITE setInnerArcRatio NOTIFY
+ innerArcRatioChanged FINAL)
+ Q_PROPERTY(qreal cornerRadius READ cornerRadius WRITE setCornerRadius NOTIFY cornerRadiusChanged
+ FINAL)
+ Q_PROPERTY(
+ qreal strokeWidth READ strokeWidth WRITE setStrokeWidth NOTIFY strokeWidthChanged FINAL)
+ Q_PROPERTY(bool hideLine READ hideLine WRITE setHideLine NOTIFY hideLineChanged FINAL)
+ Q_PROPERTY(QColor fillColor READ fillColor WRITE setFillColor NOTIFY fillColorChanged FINAL)
+ Q_PROPERTY(QColor strokeColor READ strokeColor WRITE setStrokeColor NOTIFY strokeColorChanged
+ FINAL)
+ Q_PROPERTY(QQuickShapePath::CapStyle capStyle READ capStyle WRITE setCapStyle NOTIFY
+ capStyleChanged FINAL)
+ Q_PROPERTY(QQuickShapePath::JoinStyle joinStyle READ joinStyle WRITE setJoinStyle NOTIFY
+ joinStyleChanged FINAL)
+ Q_PROPERTY(QQuickShapePath::StrokeStyle strokeStyle READ strokeStyle WRITE setStrokeStyle NOTIFY
+ strokeStyleChanged FINAL)
+ Q_PROPERTY(QQuickShapePath::FillRule fillRule READ fillRule WRITE setFillRule NOTIFY
+ fillRuleChanged FINAL)
+ Q_PROPERTY(QVector<qreal> dashPattern READ dashPattern WRITE setDashPattern NOTIFY
+ dashPatternChanged FINAL)
+ Q_PROPERTY(QQuickShapeGradient *fillGradient READ fillGradient WRITE setFillGradient NOTIFY
+ gradientChanged RESET resetFillGradient FINAL)
+ Q_PROPERTY(BorderMode borderMode READ borderMode WRITE setBorderMode NOTIFY borderModeChanged
+ RESET resetBorderMode FINAL)
+
+ QML_NAMED_ELEMENT(EllipseShape)
+ QML_ADDED_IN_VERSION(6, 10)
+
+public:
+ QQuickEllipseShape(QQuickItem *parent = nullptr);
+ ~QQuickEllipseShape() override;
+
+ qreal sweepAngle() const;
+ void setSweepAngle(qreal sweepAngle);
+
+ qreal startAngle() const;
+ void setStartAngle(qreal startAngle);
+
+ qreal dashOffset() const;
+ void setDashOffset(qreal offset);
+
+ qreal innerArcRatio() const;
+ void setInnerArcRatio(qreal innerArcRatio);
+
+ qreal cornerRadius() const;
+ void setCornerRadius(qreal cornerRadius);
+
+ qreal strokeWidth() const;
+ void setStrokeWidth(qreal width);
+
+ bool hideLine() const;
+ void setHideLine(bool hideLine);
+
+ QColor fillColor() const;
+ void setFillColor(const QColor &color);
+
+ QColor strokeColor() const;
+ void setStrokeColor(const QColor &color);
+
+ QQuickShapePath::CapStyle capStyle() const;
+ void setCapStyle(QQuickShapePath::CapStyle style);
+
+ QQuickShapePath::JoinStyle joinStyle() const;
+ void setJoinStyle(QQuickShapePath::JoinStyle style);
+
+ QQuickShapePath::StrokeStyle strokeStyle() const;
+ void setStrokeStyle(QQuickShapePath::StrokeStyle style);
+
+ QQuickShapePath::FillRule fillRule() const;
+ void setFillRule(QQuickShapePath::FillRule fillRule);
+
+ QVector<qreal> dashPattern() const;
+ void setDashPattern(const QVector<qreal> &array);
+
+ QQuickShapeGradient *fillGradient() const;
+ void setFillGradient(QQuickShapeGradient *fillGradient);
+ void resetFillGradient();
+
+ enum class BorderMode { Inside, Middle, Outside };
+ Q_ENUM(BorderMode)
+ BorderMode borderMode() const;
+ void setBorderMode(BorderMode borderMode);
+ void resetBorderMode();
+
+Q_SIGNALS:
+ void innerArcRatioChanged();
+ void cornerRadiusChanged();
+ void hideLineChanged();
+ void startAngleChanged();
+ void sweepAngleChanged();
+ void strokeColorChanged();
+ void strokeWidthChanged();
+ void fillColorChanged();
+ void joinStyleChanged();
+ void capStyleChanged();
+ void fillRuleChanged();
+ void strokeStyleChanged();
+ void dashOffsetChanged();
+ void dashPatternChanged();
+ void gradientChanged();
+ void borderModeChanged();
+
+protected:
+ void itemChange(ItemChange change, const ItemChangeData &value) override;
+
+private:
+ Q_DISABLE_COPY(QQuickEllipseShape)
+ Q_DECLARE_PRIVATE(QQuickEllipseShape)
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKELLIPSE1SHAPE_P_H
diff --git a/src/quickshapes/designhelpers/qquickellipseshape_p_p.h b/src/quickshapes/designhelpers/qquickellipseshape_p_p.h
new file mode 100644
index 0000000000..f9a9b8bbc9
--- /dev/null
+++ b/src/quickshapes/designhelpers/qquickellipseshape_p_p.h
@@ -0,0 +1,149 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QQUICKELLIPSESHAPE_P_P_H
+#define QQUICKELLIPSESHAPE_P_P_H
+
+#include "qquickellipseshape_p.h"
+#include <QtQml/private/qqmlpropertyutils_p.h>
+#include <QtQuickShapes/private/qquickshape_p_p.h>
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+QT_BEGIN_NAMESPACE
+
+class Q_QUICKSHAPESDESIGNHELPERS_EXPORT QQuickEllipseShapePrivate : public QQuickShapePrivate
+{
+ Q_DECLARE_PUBLIC(QQuickEllipseShape)
+
+public:
+ QQuickEllipseShapePrivate();
+ ~QQuickEllipseShapePrivate() override;
+
+ struct RoundedCorner
+ {
+ void update(qreal diff, QVector2D a, QVector2D b, QVector2D c, qreal alpha, qreal radius)
+ {
+ A = a;
+ B = b;
+ C = c;
+ this->alpha = alpha;
+ this->radius = radius;
+ this->diff = diff;
+ }
+
+ void reset()
+ {
+ A = QVector2D();
+ B = QVector2D();
+ C = QVector2D();
+ alpha = 0;
+ radius = 0;
+ diff = 0;
+ }
+
+ // A - a point, where the rounding corner is located
+ QVector2D A;
+ // B - tangent point of the rounded corner arc, which is located on the ellipse's arc
+ // for the rounded corner at the ellipse's center this is a point on the edge defined by the
+ // end angle
+ // for the rounded corner at the begin angle this is a point on the outer arc of the ellipse
+ QVector2D B;
+ // C - tangent point of the rounded corner arc, which is located on the ellipse's edge
+ // for the rounded corner at the ellipse's center this is a point on the edge defined by the
+ // begin angle
+ QVector2D C;
+ qreal alpha = 0; // angle between AB and AC
+ qreal radius = 0; // rounded corner radius
+ // not a rounded corner data, but a helper used to compare
+ // currently calculated radius to the previously calculated radius
+ qreal diff = 0;
+ };
+
+ enum class RoundedCornerIndex { Center, InnerEnd, OuterEnd, InnerBegin, OuterBegin };
+
+ // helper, to avoid typing static_cast<int>(RoundedCornerIndex) every time
+ class RoundedCornerArray
+ {
+ public:
+ RoundedCorner &operator[](RoundedCornerIndex index)
+ {
+ return array[static_cast<int>(index)];
+ }
+
+ void reset()
+ {
+ for (auto &rc : array)
+ rc.reset();
+ }
+
+ private:
+ RoundedCorner array[5];
+ } roundedCorners;
+
+ void addLine(QVector2D point);
+ void addArc(QVector2D point, QVector2D arcRadius, QQuickPathArc::ArcDirection dir,
+ bool largeArc = false);
+
+ qreal getBorderOffset() const;
+
+ // calculates rounded corner at the ellipse center
+ void roundCenter(QVector2D center, QVector2D ellipseRadius);
+ // runs loop where begin and end rounded corners are calculated
+ void roundBeginEnd(QVector2D center, QVector2D ellipseRadius);
+ // calculates rounded corners on the outer arc
+ bool roundOuter(QVector2D center, QVector2D ellipseRadius, qreal deg, qreal arcAngle1,
+ qreal arcAngle2, RoundedCornerIndex index);
+ // calculates rounded corners on the inner arc
+ bool roundInner(QVector2D center, QVector2D ellipseRadius, qreal deg, qreal arcAngle1,
+ qreal arcAngle2, RoundedCornerIndex index);
+
+ // starts path at the center rounded corner and draws center rounded corner
+ void drawCenterCorner();
+ // connects outer and inner arcs with line and draws end rounded corner
+ void drawInnerEndCorner();
+ // starts path at the begin rounded corner and draws begin rounded corner
+ void drawInnerBeginCorner();
+ // connects previous rounded corner (center or begin) with line and draws begin rounded corner
+ void drawOuterBeginCorner();
+
+ // draw outer arc path from begin rounded corner to the end rounded corner
+ void drawOuterArcRounded(QVector2D center, QVector2D ellipseRadius);
+ // draw inner arc path from end rounded corner to the begin rounded corner
+ void drawInnerArcRounded(QVector2D center, QVector2D ellipseRadius);
+
+ // draws outer arc when no rounded corners involved
+ void drawOuterArc(QVector2D center, QVector2D ellipseRadius);
+ // draws full inner arc (no rounded corners)
+ void drawFullInnerArc(QVector2D center, QVector2D ellipseRadius);
+
+ // draws an ellipse when inner radius is not zero
+ void drawWithInnerRadius(QVector2D center, QVector2D ellipseRadius);
+ // draws an ellipse when inner radius is greater than zero
+ void drawWithoutInnerRadius(QVector2D center, QVector2D ellipseRadius);
+
+ void updatePath();
+
+ QQuickShapePath *path = nullptr;
+
+ bool hideLine = false;
+
+ qreal startAngle = 0;
+ qreal sweepAngle = 360;
+ qreal innerArcRatio = 0;
+ qreal cornerRadius = 10;
+ QQuickEllipseShape::BorderMode borderMode = QQuickEllipseShape::BorderMode::Inside;
+};
+
+QT_END_NAMESPACE
+
+#endif // QQUICKELLIPSE1SHAPE_P_P_H
diff --git a/tests/auto/quickshapes/designhelpers/CMakeLists.txt b/tests/auto/quickshapes/designhelpers/CMakeLists.txt
index 28a4c49916..7c6b57ec6e 100644
--- a/tests/auto/quickshapes/designhelpers/CMakeLists.txt
+++ b/tests/auto/quickshapes/designhelpers/CMakeLists.txt
@@ -7,5 +7,6 @@ if(QT_BUILD_MINIMAL_STATIC_TESTS)
endif()
if(QT_FEATURE_private_tests)
+ add_subdirectory(qquickellipseshape)
add_subdirectory(qquickrectangleshape)
endif()
diff --git a/tests/auto/quickshapes/designhelpers/qquickellipseshape/CMakeLists.txt b/tests/auto/quickshapes/designhelpers/qquickellipseshape/CMakeLists.txt
new file mode 100644
index 0000000000..def66085d9
--- /dev/null
+++ b/tests/auto/quickshapes/designhelpers/qquickellipseshape/CMakeLists.txt
@@ -0,0 +1,44 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qquickellipseshape LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+# Collect test data
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_qquickellipseshape
+ SOURCES
+ tst_qquickellipseshape.cpp
+ LIBRARIES
+ Qt::CorePrivate
+ Qt::Gui
+ Qt::GuiPrivate
+ Qt::QmlPrivate
+ Qt::QuickPrivate
+ Qt::QuickShapesPrivate
+ Qt::QuickShapesDesignHelpersPrivate
+ Qt::QuickTestUtilsPrivate
+ TESTDATA ${test_data}
+)
+
+qt_internal_extend_target(tst_qquickellipseshape CONDITION TARGET Qt::Widgets
+ LIBRARIES
+ Qt::Widgets
+)
+
+qt_internal_extend_target(tst_qquickellipseshape CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=":/data"
+)
+
+qt_internal_extend_target(tst_qquickellipseshape CONDITION NOT ANDROID AND NOT IOS
+ DEFINES
+ QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
+)
diff --git a/tests/auto/quickshapes/designhelpers/qquickellipseshape/data/default.qml b/tests/auto/quickshapes/designhelpers/qquickellipseshape/data/default.qml
new file mode 100644
index 0000000000..b346f36e83
--- /dev/null
+++ b/tests/auto/quickshapes/designhelpers/qquickellipseshape/data/default.qml
@@ -0,0 +1,12 @@
+import QtQuick
+import QtQuick.Shapes.DesignHelpers
+
+Item {
+ width: 256
+ height: 256
+
+ EllipseShape {
+ objectName: "ellipseShape"
+ anchors.centerIn: parent
+ }
+}
diff --git a/tests/auto/quickshapes/designhelpers/qquickellipseshape/data/ellipseshape1.qml b/tests/auto/quickshapes/designhelpers/qquickellipseshape/data/ellipseshape1.qml
new file mode 100644
index 0000000000..03bf8f9560
--- /dev/null
+++ b/tests/auto/quickshapes/designhelpers/qquickellipseshape/data/ellipseshape1.qml
@@ -0,0 +1,5 @@
+import QtQuick
+import QtQuick.Shapes.DesignHelpers
+
+EllipseShape {
+}
diff --git a/tests/auto/quickshapes/designhelpers/qquickellipseshape/tst_qquickellipseshape.cpp b/tests/auto/quickshapes/designhelpers/qquickellipseshape/tst_qquickellipseshape.cpp
new file mode 100644
index 0000000000..57fe2eb0c0
--- /dev/null
+++ b/tests/auto/quickshapes/designhelpers/qquickellipseshape/tst_qquickellipseshape.cpp
@@ -0,0 +1,117 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtTest/QtTest>
+#include <QtQuick/qquickview.h>
+#include <QtQuickTest/quicktest.h>
+#include <QtQuickTestUtils/private/viewtestutils_p.h>
+#include <QtQuickTestUtils/private/visualtestutils_p.h>
+#include <QtQuickShapesDesignHelpers/private/qquickellipseshape_p.h>
+
+class tst_QQuickEllipseShape : public QQmlDataTest
+{
+ Q_OBJECT
+public:
+ tst_QQuickEllipseShape();
+
+private slots:
+ void initTestCase() override;
+ void basicShape();
+ void changeSignals();
+ void changeSignals_data();
+
+private:
+ QScopedPointer<QQuickView> window;
+};
+
+tst_QQuickEllipseShape::tst_QQuickEllipseShape() : QQmlDataTest(QT_QMLTEST_DATADIR) { }
+
+void tst_QQuickEllipseShape::initTestCase()
+{
+ QQmlDataTest::initTestCase();
+ window.reset(QQuickViewTestUtils::createView());
+}
+
+void tst_QQuickEllipseShape::basicShape()
+{
+ QQmlEngine engine;
+ QQmlComponent c(&engine, testFileUrl("ellipseshape1.qml"));
+ QQuickEllipseShape *ellipseShape = qobject_cast<QQuickEllipseShape *>(c.create());
+ QVERIFY(ellipseShape);
+
+ QCOMPARE(ellipseShape->sweepAngle(), 360);
+ QCOMPARE(ellipseShape->startAngle(), 0);
+ QCOMPARE(ellipseShape->width(), 100);
+ QCOMPARE(ellipseShape->height(), 100);
+ QCOMPARE(ellipseShape->cornerRadius(), 10);
+ QCOMPARE(ellipseShape->innerArcRatio(), 0);
+ QCOMPARE(ellipseShape->hideLine(), false);
+ QCOMPARE(ellipseShape->strokeWidth(), 4);
+ QCOMPARE(ellipseShape->dashOffset(), 0);
+ QCOMPARE(ellipseShape->capStyle(), QQuickShapePath::SquareCap);
+ QCOMPARE(ellipseShape->joinStyle(), QQuickShapePath::BevelJoin);
+ QCOMPARE(ellipseShape->strokeStyle(), QQuickShapePath::SolidLine);
+ QCOMPARE(ellipseShape->borderMode(), QQuickEllipseShape::BorderMode::Inside);
+ QCOMPARE(ellipseShape->strokeColor(), QColor(Qt::black));
+ QCOMPARE(ellipseShape->fillColor(), QColor(Qt::white));
+}
+
+void tst_QQuickEllipseShape::changeSignals_data()
+{
+ QTest::addColumn<QVariant>("propertyValue");
+ QTest::addColumn<QMetaMethod>("changeSignal");
+
+ QTest::newRow("sweepAngle") << QVariant::fromValue(180)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::sweepAngleChanged);
+ QTest::newRow("startAngle") << QVariant::fromValue(90)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::startAngleChanged);
+ QTest::newRow("cornerRadius") << QVariant::fromValue(
+ 20) << QMetaMethod::fromSignal(&QQuickEllipseShape::cornerRadiusChanged);
+ QTest::newRow("innerArcRatio")
+ << QVariant::fromValue(0.5f)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::innerArcRatioChanged);
+ QTest::newRow("hideLine") << QVariant::fromValue(true)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::hideLineChanged);
+ QTest::newRow("strokeColor") << QVariant::fromValue(
+ QColor(Qt::blue)) << QMetaMethod::fromSignal(&QQuickEllipseShape::strokeColorChanged);
+ QTest::newRow("strokeWidth") << QVariant::fromValue(
+ 0) << QMetaMethod::fromSignal(&QQuickEllipseShape::strokeWidthChanged);
+ QTest::newRow("fillColor") << QVariant::fromValue(QColor(Qt::blue))
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::fillColorChanged);
+ QTest::newRow("joinStyle") << QVariant::fromValue(QQuickShapePath::RoundJoin)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::joinStyleChanged);
+ QTest::newRow("capStyle") << QVariant::fromValue(QQuickShapePath::RoundCap)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::capStyleChanged);
+ QTest::newRow("strokeStyle") << QVariant::fromValue(QQuickShapePath::DashLine)
+ << QMetaMethod::fromSignal(
+ &QQuickEllipseShape::strokeStyleChanged);
+ QTest::newRow("dashOffset") << QVariant::fromValue(4)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::dashOffsetChanged);
+ QTest::newRow("dashPattern") << QVariant::fromValue(QList<qreal>{
+ 1, 2 }) << QMetaMethod::fromSignal(&QQuickEllipseShape::dashPatternChanged);
+ QTest::newRow("borderMode") << QVariant::fromValue(QQuickEllipseShape::BorderMode::Outside)
+ << QMetaMethod::fromSignal(&QQuickEllipseShape::borderModeChanged);
+}
+
+void tst_QQuickEllipseShape::changeSignals()
+{
+ window->setSource(testFileUrl("default.qml"));
+ window->show();
+ QVERIFY(QTest::qWaitForWindowExposed(window.get()));
+ QVERIFY(window->status() == QQuickView::Ready);
+
+ QFETCH(const QVariant, propertyValue);
+ QFETCH(const QMetaMethod, changeSignal);
+
+ QQuickEllipseShape *ellipseShape = QQuickVisualTestUtils::findItem<QQuickEllipseShape>(
+ window->rootObject(), "ellipseShape");
+ QVERIFY(ellipseShape);
+ const QSignalSpy signalSpy(ellipseShape, changeSignal);
+ QVERIFY(signalSpy.isValid());
+ QVERIFY(ellipseShape->setProperty(QTest::currentDataTag(), propertyValue));
+ QCOMPARE(signalSpy.count(), 1);
+}
+
+QTEST_MAIN(tst_QQuickEllipseShape)
+
+#include "tst_qquickellipseshape.moc"
diff --git a/tests/baseline/scenegraph/data/designhelpers/designhelpers_ellipseshape.qml b/tests/baseline/scenegraph/data/designhelpers/designhelpers_ellipseshape.qml
new file mode 100644
index 0000000000..9e711b1472
--- /dev/null
+++ b/tests/baseline/scenegraph/data/designhelpers/designhelpers_ellipseshape.qml
@@ -0,0 +1,223 @@
+import QtQuick
+import QtQuick.Shapes
+import QtQuick.Shapes.DesignHelpers
+
+Rectangle {
+ width: 800
+ height: 800
+ color:"#b5e7a0"
+
+ component TestEllipseShape : EllipseShape {
+ fillColor: "#37c1ff"
+ strokeColor: "#663333"
+ width: 90
+ height: 90
+ }
+
+ Flow {
+ spacing: 2
+ anchors.fill: parent
+ EllipseShape {} // default
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 180
+ cornerRadius: 0
+ strokeStyle: ShapePath.DashLine
+ capStyle: ShapePath.FlatCap
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 270
+ cornerRadius: 0
+ strokeStyle: ShapePath.DashLine
+ capStyle: ShapePath.SquareCap
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 360
+ strokeStyle: ShapePath.DashLine
+ capStyle: ShapePath.RoundCap
+ }
+ TestEllipseShape {
+ startAngle: -90
+ sweepAngle: 360
+ innerArcRatio: 0.5
+ }
+ TestEllipseShape {
+ startAngle: -90
+ sweepAngle: 350
+ cornerRadius: 0
+ innerArcRatio: 0.5
+ strokeWidth: 1
+ }
+ TestEllipseShape {
+ startAngle: -90
+ sweepAngle: 180
+ cornerRadius: 0
+ strokeStyle: ShapePath.DashLine
+ }
+ TestEllipseShape {
+ startAngle: -360
+ sweepAngle: 360
+ }
+ TestEllipseShape {
+ id: ellipseId
+ startAngle: 360
+ sweepAngle: -360
+ strokeWidth: 1
+ fillGradient: RadialGradient {
+ focalX: ellipseId.width * 0.5
+ focalY: ellipseId.height * 0.75
+ centerX: ellipseId.width * 0.5
+ centerY: ellipseId.height * 0.5
+ centerRadius: ellipseId.width * 0.5
+ GradientStop { position:0.1; color:"cyan" }
+ GradientStop { position:0.2; color:"green" }
+ GradientStop { position:0.4; color:"red" }
+ GradientStop { position:0.6; color:"yellow" }
+ GradientStop { position:1.0; color:"blue" }
+ }
+ }
+ TestEllipseShape {
+ startAngle: -360
+ sweepAngle: 360
+ innerArcRatio: 1
+ fillGradient: LinearGradient {
+ x1: 20
+ y1: 20
+ x2: 100
+ y2: 100
+ GradientStop { position: 0.0; color: "red" }
+ GradientStop { position: 0.33; color: "yellow" }
+ GradientStop { position: 1.0; color: "green" }
+ }
+ }
+ TestEllipseShape {
+ startAngle: -360
+ sweepAngle: 360
+ fillColor: "transparent"
+ }
+ TestEllipseShape {
+ startAngle: -360
+ sweepAngle: 360
+ innerArcRatio: 0.9
+ strokeWidth: 2
+ }
+ TestEllipseShape {
+ startAngle: 360
+ sweepAngle: 240
+ cornerRadius: 0
+ innerArcRatio: 0.7
+ strokeWidth: 2
+ }
+ TestEllipseShape {
+ startAngle: -90
+ sweepAngle: 270
+ innerArcRatio: 0.5
+ strokeWidth: 2
+ }
+ TestEllipseShape {
+ startAngle: 360
+ sweepAngle: -270
+ innerArcRatio: 0.5
+ strokeWidth: 2
+ strokeStyle: ShapePath.DashLine
+ }
+ TestEllipseShape {
+ innerArcRatio: 0.7
+ }
+ TestEllipseShape {
+ startAngle: 270
+ sweepAngle: 360
+ innerArcRatio: 0
+ strokeWidth: 1
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 320
+ strokeWidth: 2
+ strokeStyle: ShapePath.DashLine
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 320
+ strokeWidth: 2
+ strokeStyle: ShapePath.DashLine
+ innerArcRatio: 0.5
+ cornerRadius: 0
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 320
+ strokeWidth: 2
+ strokeStyle: ShapePath.DashLine
+ innerArcRatio: 0.5
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 360
+ borderMode: EllipseShape.Inside
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 360
+ borderMode: EllipseShape.Middle
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 360
+ borderMode: EllipseShape.Outisde
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 270
+ cornerRadius: 0
+ strokeWidth: 2
+ hideLine: true
+ fillColor: "transparent"
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 270
+ cornerRadius: 0
+ strokeWidth: 2
+ hideLine: false
+ fillColor: "transparent"
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 360
+ strokeStyle: ShapePath.DashLine
+ dashPattern: [1,2]
+ dashOffset: 2
+ }
+ TestEllipseShape {
+ startAngle: 0
+ sweepAngle: 360
+ strokeStyle: ShapePath.DashLine
+ dashPattern: [2,4]
+ dashOffset: 4
+ }
+ TestEllipseShape {
+ startAngle: 360
+ sweepAngle: 323
+ strokeStyle: ShapePath.DashLine
+ strokeWidth: 3
+ joinStyle: ShapePath.BevelJoin
+ }
+ TestEllipseShape {
+ startAngle: 360
+ sweepAngle: 323
+ strokeStyle: ShapePath.DashLine
+ strokeWidth: 3
+ joinStyle: ShapePath.MiterJoin
+ }
+ TestEllipseShape {
+ startAngle: 360
+ sweepAngle: 323
+ strokeStyle: ShapePath.DashLine
+ strokeWidth: 3
+ joinStyle: ShapePath.RoundJoin
+ }
+ }
+}