diff options
author | Alexey Zerkin <[email protected]> | 2025-02-26 12:34:54 +0200 |
---|---|---|
committer | Alexey Zerkin <[email protected]> | 2025-09-17 07:09:27 +0300 |
commit | b300f909d71708202ad2e31872926359ecc167d0 (patch) | |
tree | 99c3529f83508d7e539d1ad553eda53c475997fb | |
parent | c86476db1fa845b9414e2a83191ed93c1c5a7936 (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]>
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 Binary files differnew file mode 100644 index 0000000000..17448dd0b8 --- /dev/null +++ b/src/quick/doc/images/path-ellipseshape.png 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 + } + } +} |