aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--examples/quick/quickshapes/shapes/cornershapes.qml12
-rw-r--r--src/quick/doc/src/includes/pathrectangle.qdocinc3
-rw-r--r--src/quick/util/qquickpath.cpp124
-rw-r--r--src/quickshapes/designhelpers/qquickrectangleshape.cpp3
-rw-r--r--tests/auto/quick/qquickpath/tst_qquickpath.cpp4
-rw-r--r--tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml77
6 files changed, 177 insertions, 46 deletions
diff --git a/examples/quick/quickshapes/shapes/cornershapes.qml b/examples/quick/quickshapes/shapes/cornershapes.qml
index 34112f51f8..565c3b6057 100644
--- a/examples/quick/quickshapes/shapes/cornershapes.qml
+++ b/examples/quick/quickshapes/shapes/cornershapes.qml
@@ -27,17 +27,20 @@ Rectangle {
GradientStop { position: 0.7; color: "red" }
}
+ // The radius is clamped to half of the rectangle's size, so only
+ // animate within that range to keep the shape moving.
+ readonly property real maxRadius: pathRectangle.width / 2
property real animRadius
SequentialAnimation on animRadius {
loops: Animation.Infinite
NumberAnimation {
- from: 0
- to: 200
+ from: -myPath.maxRadius
+ to: myPath.maxRadius
duration: 3000
}
NumberAnimation {
- from: 200
- to: 0
+ from: myPath.maxRadius
+ to: -myPath.maxRadius
duration: 3000
}
PauseAnimation {
@@ -46,6 +49,7 @@ Rectangle {
}
PathRectangle {
+ id: pathRectangle
x: myShape.width / 5
y: x
width: myShape.width - 2 * x
diff --git a/src/quick/doc/src/includes/pathrectangle.qdocinc b/src/quick/doc/src/includes/pathrectangle.qdocinc
index e9d56bb13b..f5264df74c 100644
--- a/src/quick/doc/src/includes/pathrectangle.qdocinc
+++ b/src/quick/doc/src/includes/pathrectangle.qdocinc
@@ -3,9 +3,6 @@
This property defines the corner radius used to define a rounded rectangle.
-If radius is a positive value, the rectangle path will be defined as a rounded rectangle,
-otherwise it will be defined as a normal rectangle.
-
This property may be overridden by the individual corner radius properties.
\sa topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius
diff --git a/src/quick/util/qquickpath.cpp b/src/quick/util/qquickpath.cpp
index fa05ba0a7b..03e6deab4c 100644
--- a/src/quick/util/qquickpath.cpp
+++ b/src/quick/util/qquickpath.cpp
@@ -2559,6 +2559,20 @@ void QQuickPathRectangle::setStrokeAdjustment(qreal newStrokeAdjustment)
/*!
\include pathrectangle.qdocinc {radius-property} {QtQuick::PathRectangle}
+ If radius is a positive value, the rectangle path will be defined as a rounded rectangle.
+ If radius is zero, it will be defined as a normal rectangle.
+
+ Since Qt 6.13, if radius is negative, the corners are "inverted": instead of the usual
+ convex curve, a concave, scooped-out curve of the same magnitude is used, for both the
+ \c PathRectangle.Rounded and \c PathRectangle.Squircle \l{cornerShape}{corner shapes}. This
+ has no visible effect on \c PathRectangle.Bevel corners, since a straight bevel cut looks the
+ same either way. Before Qt 6.13, a negative radius was treated like zero.
+
+ The effective radius is limited to half of the smaller of the rectangle's width and height,
+ in both the positive and the negative direction.
+
+ \note This differs from \l Rectangle, which draws a normal rectangle for a negative radius.
+
The default value is \c 0.
*/
@@ -2586,6 +2600,9 @@ void QQuickPathRectangle::setRadius(qreal newRadius)
/*!
\include pathrectangle.qdocinc {radius-properties} {PathRectangle} {qml/pathrectangle/pathrectangle.qml} {shape}
+
+ Since Qt 6.13, a negative value defines that corner to be "inverted" (concave), as described
+ for \l radius.
*/
qreal QQuickPathRectangle::cornerRadius(Qt::Corner corner) const
@@ -2812,12 +2829,16 @@ void QQuickPathRectangle::addToPath(QPainterPath &path, const QQuickPathData &da
return;
}
- // Radii must not exceed half of the width or half of the height
+ // Radii must not exceed half of the width or half of the height. Negative
+ // radii are allowed (down to the same limit) and select an "inverted"
+ // (concave, scooped-out) corner instead of the usual convex one, for
+ // both the Rounded and Squircle corner shapes.
const qreal maxDiameter = qMin(rect.width(), rect.height());
- const qreal generalRadius = qBound(qreal(0), _extra->radius, maxDiameter * 0.5);
+ const qreal maxRadius = maxDiameter * 0.5;
+ const qreal generalRadius = qBound(-maxRadius, _extra->radius, maxRadius);
auto effectiveRadius = [&](Qt::Corner corner) {
qreal radius = _extra->cornerData[corner].radius;
- return (_extra->isRadiusSet(corner)) ? qBound(qreal(0), radius, maxDiameter * 0.5)
+ return (_extra->isRadiusSet(corner)) ? qBound(-maxRadius, radius, maxRadius)
: generalRadius;
};
const qreal rTL = effectiveRadius(Qt::TopLeftCorner);
@@ -2830,7 +2851,9 @@ void QQuickPathRectangle::addToPath(QPainterPath &path, const QQuickPathData &da
// the outgoing edge, as fractions of the corner radius. The corner
// curves clockwise, entering horizontally and exiting vertically (a
// "top-right"-style corner); the other three corners are obtained by
- // rotating these offsets by 90, 180 and 270 degrees.
+ // rotating these offsets by 90, 180 and 270 degrees. Swapping the x and
+ // y of each offset mirrors the curve across the chord it spans, which
+ // turns the usual convex curve into the concave "inverted" one.
static constexpr QPointF cornerOffsets[9] = {
{ 0.300, 0.000 }, { 0.473, 0.000 }, { 0.619, 0.039 }, { 0.804, 0.088 }, { 0.912, 0.196 },
{ 0.961, 0.381 }, { 1.000, 0.527 }, { 1.000, 0.700 }, { 1.000, 1.000 }
@@ -2843,87 +2866,112 @@ void QQuickPathRectangle::addToPath(QPainterPath &path, const QQuickPathData &da
return p;
};
- auto addSquircleCorner = [&](QPointF start, int steps, qreal r) {
+ auto addSquircleCorner = [&](QPointF start, int steps, qreal r, bool inverted) {
for (int seg = 0; seg < 3; ++seg) {
- QPointF c1 = start + rotate(cornerOffsets[seg * 3 + 0] * r, steps);
- QPointF c2 = start + rotate(cornerOffsets[seg * 3 + 1] * r, steps);
- QPointF end = start + rotate(cornerOffsets[seg * 3 + 2] * r, steps);
+ QPointF o1 = cornerOffsets[seg * 3 + 0];
+ QPointF o2 = cornerOffsets[seg * 3 + 1];
+ QPointF o3 = cornerOffsets[seg * 3 + 2];
+ if (inverted) {
+ o1 = QPointF(o1.y(), o1.x());
+ o2 = QPointF(o2.y(), o2.x());
+ o3 = QPointF(o3.y(), o3.x());
+ }
+ QPointF c1 = start + rotate(o1 * r, steps);
+ QPointF c2 = start + rotate(o2 * r, steps);
+ QPointF end = start + rotate(o3 * r, steps);
path.cubicTo(c1, c2, end);
}
};
- path.moveTo(rect.left() + rTL, rect.top());
- if (rTR > 0) {
+ // Adds a circular corner at "corner". A convex corner is drawn as the
+ // quarter of the circle inscribed in the 2r x 2r box that extends from the
+ // corner towards the inside of the rectangle ("inward" gives the direction
+ // as unit steps), sweeping clockwise from "startAngle". An inverted corner
+ // is the quarter of the circle centered on the corner point itself, which
+ // starts 90 degrees further and sweeps counter-clockwise.
+ auto addRoundedCorner = [&](QPointF corner, QPointF inward, qreal startAngle, qreal r,
+ bool inverted) {
+ const QRectF box(corner - QPointF(r, r), QSizeF(2 * r, 2 * r));
+ if (inverted)
+ path.arcTo(box, startAngle + 90, 90);
+ else
+ path.arcTo(box.translated(inward * r), startAngle, -90);
+ };
+
+ path.moveTo(rect.left() + qAbs(rTL), rect.top());
+ if (rTR != 0) {
+ const qreal r = qAbs(rTR);
+ const bool inverted = rTR < 0;
switch (cornerShape(Qt::TopRightCorner)) {
case Bevel:
- path.lineTo(QPointF(rect.right() - rTR, rect.top()));
- path.lineTo(QPointF(rect.right(), rect.top() + rTR));
+ path.lineTo(QPointF(rect.right() - r, rect.top()));
+ path.lineTo(QPointF(rect.right(), rect.top() + r));
break;
case Squircle:
- path.lineTo(QPointF(rect.right() - rTR, rect.top()));
- addSquircleCorner(QPointF(rect.right() - rTR, rect.top()), 0, rTR);
+ path.lineTo(QPointF(rect.right() - r, rect.top()));
+ addSquircleCorner(QPointF(rect.right() - r, rect.top()), 0, r, inverted);
break;
default:
- path.arcTo(
- QRectF(QPointF(rect.right() - 2 * rTR, rect.top()), QSizeF(2 * rTR, 2 * rTR)),
- 90, -90);
+ addRoundedCorner(rect.topRight(), QPointF(-1, 1), 90, r, inverted);
break;
}
} else {
path.lineTo(rect.topRight());
}
- if (rBR > 0) {
+ if (rBR != 0) {
+ const qreal r = qAbs(rBR);
+ const bool inverted = rBR < 0;
switch (cornerShape(Qt::BottomRightCorner)) {
case Bevel:
- path.lineTo(QPointF(rect.right(), rect.bottom() - rBR));
- path.lineTo(QPointF(rect.right() - rBR, rect.bottom()));
+ path.lineTo(QPointF(rect.right(), rect.bottom() - r));
+ path.lineTo(QPointF(rect.right() - r, rect.bottom()));
break;
case Squircle:
- path.lineTo(QPointF(rect.right(), rect.bottom() - rBR));
- addSquircleCorner(QPointF(rect.right(), rect.bottom() - rBR), 1, rBR);
+ path.lineTo(QPointF(rect.right(), rect.bottom() - r));
+ addSquircleCorner(QPointF(rect.right(), rect.bottom() - r), 1, r, inverted);
break;
default:
- path.arcTo(QRectF(QPointF(rect.right() - 2 * rBR, rect.bottom() - 2 * rBR),
- QSizeF(2 * rBR, 2 * rBR)),
- 0, -90);
+ addRoundedCorner(rect.bottomRight(), QPointF(-1, -1), 0, r, inverted);
break;
}
} else {
path.lineTo(rect.bottomRight());
}
- if (rBL > 0) {
+ if (rBL != 0) {
+ const qreal r = qAbs(rBL);
+ const bool inverted = rBL < 0;
switch (cornerShape(Qt::BottomLeftCorner)) {
case Bevel:
- path.lineTo(QPointF(rect.left() + rBL, rect.bottom()));
- path.lineTo(QPointF(rect.left(), rect.bottom() - rBL));
+ path.lineTo(QPointF(rect.left() + r, rect.bottom()));
+ path.lineTo(QPointF(rect.left(), rect.bottom() - r));
break;
case Squircle:
- path.lineTo(QPointF(rect.left() + rBL, rect.bottom()));
- addSquircleCorner(QPointF(rect.left() + rBL, rect.bottom()), 2, rBL);
+ path.lineTo(QPointF(rect.left() + r, rect.bottom()));
+ addSquircleCorner(QPointF(rect.left() + r, rect.bottom()), 2, r, inverted);
break;
default:
- path.arcTo(
- QRectF(QPointF(rect.left(), rect.bottom() - 2 * rBL), QSizeF(2 * rBL, 2 * rBL)),
- 270, -90);
+ addRoundedCorner(rect.bottomLeft(), QPointF(1, -1), 270, r, inverted);
break;
}
} else {
path.lineTo(rect.bottomLeft());
}
- if (rTL > 0) {
+ if (rTL != 0) {
+ const qreal r = qAbs(rTL);
+ const bool inverted = rTL < 0;
switch (cornerShape(Qt::TopLeftCorner)) {
case Bevel:
- path.lineTo(QPointF(rect.left(), rect.top() + rTL));
+ path.lineTo(QPointF(rect.left(), rect.top() + r));
break;
case Squircle:
- path.lineTo(QPointF(rect.left(), rect.top() + rTL));
- addSquircleCorner(QPointF(rect.left(), rect.top() + rTL), 3, rTL);
+ path.lineTo(QPointF(rect.left(), rect.top() + r));
+ addSquircleCorner(QPointF(rect.left(), rect.top() + r), 3, r, inverted);
break;
default:
- path.arcTo(QRectF(rect.topLeft(), QSizeF(2 * rTL, 2 * rTL)), 180, -90);
+ addRoundedCorner(rect.topLeft(), QPointF(1, 1), 180, r, inverted);
break;
}
} else {
diff --git a/src/quickshapes/designhelpers/qquickrectangleshape.cpp b/src/quickshapes/designhelpers/qquickrectangleshape.cpp
index 47b3af1676..cc94e2d164 100644
--- a/src/quickshapes/designhelpers/qquickrectangleshape.cpp
+++ b/src/quickshapes/designhelpers/qquickrectangleshape.cpp
@@ -590,6 +590,9 @@ void QQuickRectangleShape::resetDrawLeft()
\include pathrectangle.qdocinc {radius-property}
{QtQuick.Shapes.DesignHelpers::RectangleShape}
+ If radius is a positive value, the rectangle path will be defined as a rounded rectangle,
+ otherwise it will be defined as a normal rectangle.
+
The default value is \c 10.
*/
diff --git a/tests/auto/quick/qquickpath/tst_qquickpath.cpp b/tests/auto/quick/qquickpath/tst_qquickpath.cpp
index 36d8c44d0e..2034d2d192 100644
--- a/tests/auto/quick/qquickpath/tst_qquickpath.cpp
+++ b/tests/auto/quick/qquickpath/tst_qquickpath.cpp
@@ -421,7 +421,9 @@ void tst_QuickPath::rectangle()
void tst_QuickPath::rectangleRadii()
{
- // Test that the radius logic of PathRectangle is the same as Rectangle's
+ // Test that the radius property logic (explicit, inherited and reset per-corner values)
+ // of PathRectangle is the same as Rectangle's. Note that the rendering differs: a negative
+ // radius gives inverted corners on PathRectangle, but a normal rectangle on Rectangle.
QQmlEngine engine;
QQmlComponent c1(&engine);
c1.setData("import QtQuick\n"
diff --git a/tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml b/tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml
new file mode 100644
index 0000000000..5b858c37e7
--- /dev/null
+++ b/tests/baseline/scenegraph/data/shape/shape_inverted_corners.qml
@@ -0,0 +1,77 @@
+import QtQuick
+import QtQuick.Shapes
+
+Rectangle {
+ width: 320
+ height: 480
+ color: "lightgray"
+
+ ListModel {
+ id: renderers
+ ListElement { renderer: Shape.GeometryRenderer }
+ ListElement { renderer: Shape.CurveRenderer }
+ }
+
+ ListModel {
+ id: cornerShapes
+ ListElement { cornerShape: PathRectangle.Rounded }
+ ListElement { cornerShape: PathRectangle.Squircle }
+ ListElement { cornerShape: PathRectangle.Bevel }
+ }
+
+ Row {
+ padding: 10
+ Repeater {
+ model: renderers
+ Column {
+ id: rendererColumn
+ required property int renderer
+ spacing: 10
+ Repeater {
+ model: cornerShapes
+ Shape {
+ id: shape
+ required property int cornerShape
+ width: 150
+ height: 140
+ preferredRendererType: rendererColumn.renderer
+
+ ShapePath {
+ fillColor: "yellow"
+ strokeColor: "green"
+ strokeWidth: 3
+ joinStyle: ShapePath.MiterJoin
+
+ // Uniform inverted corners
+ PathRectangle {
+ x: 5; y: 0
+ width: 120; height: 40
+ cornerShape: shape.cornerShape
+ radius: -12
+ }
+
+ // Mixed inverted, convex and sharp corners
+ PathRectangle {
+ x: 5; y: 50
+ width: 120; height: 40
+ cornerShape: shape.cornerShape
+ radius: 8
+ topLeftRadius: -15
+ bottomRightRadius: -5
+ bottomLeftRadius: 0
+ }
+
+ // Clamped to half of the height
+ PathRectangle {
+ x: 5; y: 100
+ width: 120; height: 40
+ cornerShape: shape.cornerShape
+ radius: -100
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}