diff --git a/qt6/src/qml/BoxInsetShadow.qml b/qt6/src/qml/BoxInsetShadow.qml index eab41a9e2..01b219569 100644 --- a/qt6/src/qml/BoxInsetShadow.qml +++ b/qt6/src/qml/BoxInsetShadow.qml @@ -18,9 +18,10 @@ Item { property real shadowOffsetY: 0 property color shadowColor: "black" property real spread: 0 - readonly property real __borderBase: cornerRadius + spread + shadowBlur / 2.0 + readonly property real __hardPadding: shadowBlur === 0 ? 1 : 0 + readonly property real __borderBase: cornerRadius + spread + shadowBlur / 2.0 + __hardPadding readonly property real __minImageSize: Math.max(image.border.left + image.border.right, image.border.top + image.border.bottom) - readonly property real __boxSize: Math.max(__minImageSize - shadowBlur, cornerRadius * 2 + 1) + readonly property real __boxSize: Math.max(__minImageSize - shadowBlur - __hardPadding * 2, cornerRadius * 2 + 1) BorderImage { id: image @@ -41,11 +42,15 @@ Item { return val; } + // When blur=0 the shadow offset is handled by translating the shadow + // rect in the image provider, not by asymmetric border sizing. Using + // asymmetric borders would eat the entire center region (top+bottom == + // imageSize), leaving BorderImage with nothing to stretch. border { - left: __borderBase + bound(0, shadowBlur + shadowOffsetX, width - 2 * __borderBase) - right: __borderBase + bound(0, shadowBlur - shadowOffsetX, width - 2 * __borderBase) - top: __borderBase + bound(0, shadowBlur + shadowOffsetY, height - 2 * __borderBase) - bottom: __borderBase + bound(0, shadowBlur - shadowOffsetY, height - 2 * __borderBase) + left: __borderBase + (shadowBlur > 0 ? bound(0, shadowBlur + shadowOffsetX, width - 2 * __borderBase) : 0) + right: __borderBase + (shadowBlur > 0 ? bound(0, shadowBlur - shadowOffsetX, width - 2 * __borderBase) : 0) + top: __borderBase + (shadowBlur > 0 ? bound(0, shadowBlur + shadowOffsetY, height - 2 * __borderBase) : 0) + bottom: __borderBase + (shadowBlur > 0 ? bound(0, shadowBlur - shadowOffsetY, height - 2 * __borderBase) : 0) } } } diff --git a/qt6/src/qml/BoxPanel.qml b/qt6/src/qml/BoxPanel.qml index 2e0d7a6df..699a24f5d 100644 --- a/qt6/src/qml/BoxPanel.qml +++ b/qt6/src/qml/BoxPanel.qml @@ -15,26 +15,67 @@ Item { property D.Palette insideBorderColor: DS.Style.button.insideBorder property D.Palette outsideBorderColor: DS.Style.button.outsideBorder property D.Palette dropShadowColor: DS.Style.button.dropShadow + property D.Palette dropShadowColor2: null property D.Palette innerShadowColor1: DS.Style.button.innerShadow1 property D.Palette innerShadowColor2: DS.Style.button.innerShadow2 + property D.Palette overlayColor: null property int boxShadowBlur: 6 property int boxShadowOffsetY: 4 + property int boxShadowOffsetY2: 0 property int innerShadowOffsetY1: -1 + property int innerShadowOffsetY2: 1 // Background color changes with hover state if `backgroundFlowingHovered` is `true`. property bool backgroundFlowsHovered: true property bool enableBoxShadow: control.D.ColorSelector.family === D.Palette.CommonColor + // Shadow and gradient rendering is opt-in. Commit 52633cb temporarily + // dropped BoxPanel's drop shadow, inner shadow and gradient for every + // consumer, so they stay off by default here. Only the normal text + // Button (see Button.qml) turns them on, which keeps the other button + // styles and panels looking as they did before. + property bool enableDropShadow: false + property bool enableInnerShadow: false + property bool enableGradient: false - // TODO drop shadow temporarily. - // Loader { - // anchors.fill: backgroundRect - // active: enableBoxShadow - // sourceComponent: BoxShadow { - // shadowBlur: control.boxShadowBlur - // shadowOffsetY: control.boxShadowOffsetY - // shadowColor: control.D.ColorSelector.dropShadowColor - // cornerRadius: backgroundRect.radius - // } - // } + // True when a hard drop shadow is actually visible (non-transparent). + // In dark mode where the drop shadow is transparent, the outside border + // is drawn fully inside to avoid a stray line outside the button. + readonly property bool __hasVisibleHardDropShadow: control.enableBoxShadow && control.enableDropShadow + && control.boxShadowBlur === 0 + && dropShadowColor && control.D.ColorSelector.dropShadowColor.a > 0 + // Hard drop shadows (blur == 0): two rounded Rectangles matching the + // border-box, placed below the background and borders in z-order so the + // button's own paint naturally covers the overlapping part. Only the + // strip extending below the button is visible, with full rounded corners. + Rectangle { + id: hardShadow + z: D.DTK.BelowOrder + visible: control.enableBoxShadow && control.enableDropShadow + && control.boxShadowBlur === 0 + && dropShadowColor && control.D.ColorSelector.dropShadowColor.a > 0 + x: backgroundRect.x + y: backgroundRect.y + control.boxShadowOffsetY + width: backgroundRect.width + height: backgroundRect.height + radius: backgroundRect.radius + color: control.D.ColorSelector.dropShadowColor + antialiasing: false + } + + Rectangle { + id: hardShadow2 + z: D.DTK.BelowOrder + visible: control.enableBoxShadow && control.enableDropShadow + && control.boxShadowBlur === 0 + && dropShadowColor2 && control.D.ColorSelector.dropShadowColor2.a > 0 + && control.boxShadowOffsetY2 > 0 + x: backgroundRect.x + y: backgroundRect.y + control.boxShadowOffsetY2 + width: backgroundRect.width + height: backgroundRect.height + radius: backgroundRect.radius + color: control.D.ColorSelector.dropShadowColor2 + antialiasing: false + } Rectangle { id: backgroundRect @@ -52,38 +93,48 @@ Item { anchors.fill: parent radius: control.radius - // gradient: D.ColorSelector.color1 === D.ColorSelector.color2 ? null : backgroundGradient + gradient: control.enableGradient + && D.ColorSelector.color1 !== D.ColorSelector.color2 ? backgroundGradient : null color: D.ColorSelector.color1 } - // Loader { - // anchors.fill: backgroundRect - // readonly property color innerShadowColor: control.D.ColorSelector.innerShadowColor1 - // active: innerShadowColor1 && innerShadowColor.a !== 0 && control.D.ColorSelector.family === D.Palette.CommonColor - // z: D.DTK.AboveOrder - - // sourceComponent: BoxInsetShadow { - // shadowBlur: 2 - // shadowOffsetY: control.innerShadowOffsetY1 - // spread: 1 - // shadowColor: innerShadowColor - // cornerRadius: backgroundRect.radius - // } - // } - - // Loader { - // anchors.fill: backgroundRect - // readonly property color innerShadowColor: control.D.ColorSelector.innerShadowColor2 - // active: innerShadowColor2 && innerShadowColor.a !== 0 && control.D.ColorSelector.family === D.Palette.CommonColor - // z: D.DTK.AboveOrder - - // sourceComponent: BoxInsetShadow { - // shadowBlur: 1 - // shadowOffsetY: 1 - // shadowColor: innerShadowColor - // cornerRadius: backgroundRect.radius - // } - // } + Rectangle { + visible: control.overlayColor + anchors.fill: parent + radius: control.radius + color: control.D.ColorSelector.overlayColor + } + + Loader { + anchors.fill: backgroundRect + readonly property color innerShadowColor: control.D.ColorSelector.innerShadowColor1 + active: control.enableBoxShadow && control.enableInnerShadow + && innerShadowColor1 && innerShadowColor.a !== 0 + z: D.DTK.AboveOrder + + sourceComponent: BoxInsetShadow { + shadowBlur: 0 + shadowOffsetY: control.innerShadowOffsetY1 + spread: 0 + shadowColor: innerShadowColor + cornerRadius: backgroundRect.radius + } + } + + Loader { + anchors.fill: backgroundRect + readonly property color innerShadowColor: control.D.ColorSelector.innerShadowColor2 + active: control.enableBoxShadow && control.enableInnerShadow + && innerShadowColor2 && innerShadowColor.a !== 0 + z: D.DTK.AboveOrder + + sourceComponent: BoxInsetShadow { + shadowBlur: 0 + shadowOffsetY: control.innerShadowOffsetY2 + shadowColor: innerShadowColor + cornerRadius: backgroundRect.radius + } + } Loader { active: insideBorderColor @@ -94,18 +145,20 @@ Item { radius: backgroundRect.radius color: control.D.ColorSelector.insideBorderColor borderWidth: DS.Style.control.borderWidth + borderInside: !control.__hasVisibleHardDropShadow } } Loader { active: outsideBorderColor anchors.fill: backgroundRect - z: D.DTK.AboveOrder + z: D.DTK.NormalOrder sourceComponent: OutsideBoxBorder { radius: backgroundRect.radius color: control.D.ColorSelector.outsideBorderColor borderWidth: DS.Style.control.borderWidth + borderInside: !control.__hasVisibleHardDropShadow } } } diff --git a/qt6/src/qml/Button.qml b/qt6/src/qml/Button.qml index 60f842335..cffe91ef7 100644 --- a/qt6/src/qml/Button.qml +++ b/qt6/src/qml/Button.qml @@ -35,6 +35,14 @@ T.Button { implicitWidth: DS.Style.button.width implicitHeight: DS.Style.button.height button: control + // The normal (non-checked, non-highlighted) text button opts into the + // BoxPanel drop shadow, inner shadow and gradient that were dropped + // for every consumer in commit 52633cb. Checked/highlighted buttons + // and all other ButtonPanel users keep their existing flat look. + radius: control.checked || control.highlighted ? DS.Style.control.radius : DS.Style.button.radius + enableDropShadow: !(control.checked || control.highlighted) + enableInnerShadow: !(control.checked || control.highlighted) + enableGradient: !(control.checked || control.highlighted) } contentItem: Item { diff --git a/qt6/src/qml/FlowStyle.qml b/qt6/src/qml/FlowStyle.qml index 7c089f110..02e4b371f 100644 --- a/qt6/src/qml/FlowStyle.qml +++ b/qt6/src/qml/FlowStyle.qml @@ -100,26 +100,27 @@ QtObject { property int hPadding: control.radius property int vPadding: control.radius / 2.0 property int iconSize: 24 + property int radius: 6 property D.Palette background1: D.Palette { normal { - common: ("#f7f7f7") + common: ("#f5f5f5") crystal: Qt.rgba(0, 0, 0, 0.1) } normalDark { - common: Qt.rgba(1, 1, 1, 0.1) + common: Qt.rgba(60 / 255, 60 / 255, 60 / 255, 0.6) crystal: Qt.rgba(1, 1, 1, 0.08) } hovered { - common: ("#e1e1e1") + common: ("#e6e6e6") crystal: Qt.rgba(0, 0, 0, 0.2) } hoveredDark { - common: Qt.rgba(1, 1, 1, 0.2) + common: Qt.rgba(110 / 255, 110 / 255, 110 / 255, 0.6) crystal: Qt.rgba(1, 1, 1, 0.2) } pressed { - common: ("#bcc4d0") + common: Qt.rgba(169 / 255, 169 / 255, 169 / 255, 0.6) crystal: Qt.rgba(0, 0, 0, 0.15) } pressedDark { @@ -130,42 +131,63 @@ QtObject { property D.Palette background2: D.Palette { normal { - common: ("#f0f0f0") + common: ("#efefef") crystal: Qt.rgba(0, 0, 0, 0.1) } normalDark { - common: Qt.rgba(1, 1, 1, 0.1) + common: Qt.rgba(45 / 255, 45 / 255, 45 / 255, 0.6) crystal: Qt.rgba(1, 1, 1, 0.1) } hovered { - common: ("#d2d2d2") + common: ("#e6e6e6") crystal: Qt.rgba(16.0 / 255, 16.0 / 255, 16.0 / 255, 0.2) } + hoveredDark { + common: Qt.rgba(66 / 255, 66 / 255, 66 / 255, 0.6) + } pressed { - common: ("#cdd6e0") + common: Qt.rgba(202 / 255, 202 / 255, 202 / 255, 0.5) crystal: Qt.rgba(16.0 / 255, 16.0 / 255, 16.0 / 255, 0.15) } } property D.Palette dropShadow: D.Palette { normal: Qt.rgba(0, 0, 0, 0.05) - hovered: Qt.rgba(0, 0, 0, 0.1) + normalDark: ("transparent") + hovered: Qt.rgba(0, 0, 0, 0.05) + pressed: Qt.rgba(0, 0, 0, 0.1) } - property D.Palette innerShadow1: D.Palette { + // 1px near hard drop shadow layered over `dropShadow` (the 2px far + // shadow). Normal and hover both render it; pressed does not. + property D.Palette dropShadow2: D.Palette { normal: Qt.rgba(0, 0, 0, 0.05) + normalDark: ("transparent") + hovered: Qt.rgba(0, 0, 0, 0.05) pressed: ("transparent") } + property D.Palette innerShadow1: D.Palette { + normal: ("transparent") + normalDark: Qt.rgba(0, 0, 0, 0.5) + hoveredDark: Qt.rgba(0, 0, 0, 0.6) + pressed: ("transparent") + } + property D.Palette overlayColor: D.Palette { + normal: ("transparent") + normalDark: Qt.rgba(0, 0, 0, 0.08) + } + property D.Palette innerShadow2: D.Palette { - normal: Qt.rgba(1, 1, 1, 0.2) - hovered: Qt.rgba(1, 1, 1, 0.5) + normal: Qt.rgba(1, 1, 1, 0.4) + normalDark: Qt.rgba(1, 1, 1, 0.07) + hovered: Qt.rgba(1, 1, 1, 0.3) pressed: ("transparent") } property D.Palette insideBorder: D.Palette { normal { - common: Qt.rgba(1, 1, 1, 0.1) + common: ("transparent") crystal: Qt.rgba(1, 1, 1, 0.1) } normalDark { @@ -173,22 +195,28 @@ QtObject { crystal: Qt.rgba(1, 1, 1, 0.1) } hovered { - common: Qt.rgba(1, 1, 1, 0.2) + common: ("transparent") crystal: Qt.rgba(0, 0, 0, 0.05) } - pressed { - common: Qt.rgba(1, 1, 1, 0.03) - crystal: Qt.rgba(0, 0, 0, 0.03) - } + pressed: ("transparent") } property D.Palette outsideBorder: D.Palette { normal { - common: Qt.rgba(0, 0, 0, 0.08) + common: Qt.rgba(0, 0, 0, 0.1) crystal: Qt.rgba(0, 0, 0, 0.08) } - hovered: Qt.rgba(0, 0, 0, 0.2) - pressed: ("transparent") + normalDark { + common: Qt.rgba(0, 0, 0, 0.15) + } + hovered { + common: Qt.rgba(0, 0, 0, 0.15) + crystal: Qt.rgba(0, 0, 0, 0.2) + } + hoveredDark { + common: Qt.rgba(0, 0, 0, 0.05) + } + pressed: Qt.rgba(0, 0, 0, 0.15) } property D.Palette text: D.Palette { @@ -201,8 +229,8 @@ QtObject { crystal: Qt.rgba(0, 0, 0, 1) } pressed { - common: D.DTK.makeColor(D.Color.Highlight) - crystal: D.DTK.makeColor(D.Color.Highlight) + common: Qt.rgba(0, 0, 0, 0.7) + crystal: Qt.rgba(0, 0, 0, 0.7) } } } @@ -349,7 +377,7 @@ QtObject { property int height: 50 property D.Palette background: D.Palette { normal: ("transparent") - normalDark: ("transparent") + normalDark: Qt.rgba(0, 0, 0, 0.5) hovered: Qt.rgba(0, 0, 0, 0.10) hoveredDark: Qt.rgba(1, 1, 1, 0.10) pressed: Qt.rgba(0, 0, 0, 0.15) @@ -442,10 +470,10 @@ QtObject { property D.Palette flatBackground: D.Palette { normal { - common: Qt.rgba(0, 0, 0, 0.1) + common: Qt.rgba(0, 0, 0, 0.15) } normalDark { - common: Qt.rgba(1, 1, 1, 0.1) + common: Qt.rgba(0, 0, 0, 0.05) } hovered { common: Qt.rgba(0, 0, 0, 0.1) @@ -509,8 +537,6 @@ QtObject { property int width: 200 property int height: 36 property int margin: 10 - - property D.Palette background: D.Palette { normal: Qt.rgba(0, 0, 0, 0.05) normalDark: Qt.rgba(255, 255, 255, 0.05) diff --git a/qt6/src/qml/InsideBoxBorder.qml b/qt6/src/qml/InsideBoxBorder.qml index 266c92686..4946a015c 100644 --- a/qt6/src/qml/InsideBoxBorder.qml +++ b/qt6/src/qml/InsideBoxBorder.qml @@ -10,12 +10,21 @@ Item { property real borderWidth: 1 / Screen.devicePixelRatio property color color: Qt.rgba(1, 1, 1, 0.15) property alias radius: rect.radius + // When true, the border is drawn fully inside the parent bounds (no + // portion extends outside). Used when there is no drop shadow to + // cover the outer half of a centered border. + property bool borderInside: false Rectangle { id: rect - anchors.fill: parent color: "transparent" + radius: parent.borderInside ? parent.radius - border.width / 2 : parent.radius + + anchors { + fill: parent + margins: parent.borderInside ? border.width / 2 : 0 + } border { width: borderWidth diff --git a/qt6/src/qml/OutsideBoxBorder.qml b/qt6/src/qml/OutsideBoxBorder.qml index 04cad6821..040d4f5d7 100644 --- a/qt6/src/qml/OutsideBoxBorder.qml +++ b/qt6/src/qml/OutsideBoxBorder.qml @@ -8,16 +8,20 @@ Item { property real borderWidth: 1 property color color: "white" property real radius: 0 + // When true, the border is drawn fully inside the parent bounds (no + // portion extends outside). Used when there is no drop shadow to + // cover the outer half of a centered border. + property bool borderInside: false Rectangle { id: rect color: "transparent" - radius: parent.radius + border.width + radius: parent.borderInside ? parent.radius - border.width / 2 : parent.radius anchors { fill: parent - margins: -border.width + margins: parent.borderInside ? border.width / 2 : 0 } border { diff --git a/qt6/src/qml/private/ButtonPanel.qml b/qt6/src/qml/private/ButtonPanel.qml index 8dd9dfebd..5b5de1728 100644 --- a/qt6/src/qml/private/ButtonPanel.qml +++ b/qt6/src/qml/private/ButtonPanel.qml @@ -17,11 +17,16 @@ BoxPanel { insideBorderColor: selectValue(DS.Style.button.insideBorder, null, DS.Style.highlightedButton.border) outsideBorderColor: selectValue(DS.Style.button.outsideBorder, null, null) dropShadowColor: selectValue(DS.Style.button.dropShadow, DS.Style.checkedButton.dropShadow, DS.Style.highlightedButton.dropShadow) + dropShadowColor2: selectValue(DS.Style.button.dropShadow2, null, null) innerShadowColor1: selectValue(DS.Style.button.innerShadow1, DS.Style.checkedButton.innerShadow, DS.Style.highlightedButton.innerShadow1) innerShadowColor2: selectValue(DS.Style.button.innerShadow2, null, DS.Style.highlightedButton.innerShadow2) - boxShadowBlur: selectValue(control.D.ColorSelector.controlState === D.DTK.PressedState ? 4 : 6, 6, 4) - boxShadowOffsetY: selectValue(control.D.ColorSelector.controlState === D.DTK.PressedState ? 2 : 4, 4, 4) - innerShadowOffsetY1: selectValue(control.D.ColorSelector.controlState === D.DTK.HoveredState ? -3 : -1, -1, -1) + overlayColor: selectValue(DS.Style.button.overlayColor, null, null) + // All states use hard shadows (blur=0). Normal/hover: 2px far shadow + + // 1px near shadow layered on top. Pressed: 1px only (CSS `0 1px 0 0`). + boxShadowBlur: selectValue(0, 6, 4) + boxShadowOffsetY: selectValue(control.D.ColorSelector.controlState === D.DTK.PressedState ? 1 : 2, 4, 4) + boxShadowOffsetY2: selectValue(control.D.ColorSelector.controlState === D.DTK.PressedState ? 0 : 1, 0, 0) + innerShadowOffsetY1: -1 visible: !button.flat || button.checked || button.highlighted || button.visualFocus || control.D.ColorSelector.controlState === D.DTK.PressedState || control.D.ColorSelector.controlState === D.DTK.HoveredState function selectValue(normal, checked, highlighted) { diff --git a/src/private/dquickimageprovider.cpp b/src/private/dquickimageprovider.cpp index 4ada14557..a4a83da16 100644 --- a/src/private/dquickimageprovider.cpp +++ b/src/private/dquickimageprovider.cpp @@ -449,18 +449,85 @@ QImage DQuickShadowProvider::requestImage(const QString &id, QSize *size, const if (!shadow) break; + const qreal hardPadding = qIsNull(config.blurRadius) ? 1.0 : 0.0; + + // For inner hard shadows (blur=0) with non-zero offset, the 1px + // padding is consumed by the offset translation, leaving a gap at + // one edge of the output. Enlarge the source with opaque padding so + // the translated shadow still covers the full clip area. + QImage sourceImage; + int extraLeft = 0, extraTop = 0; + if (hardPadding > 0.0 && config.isInner()) { + // For inner hard shadows with offset, translate(-offset) shifts the + // source frame, exposing a gap on one side. Enlarge the source only + // on the side where the frame is pushed, filling with opaque so the + // shadow stays flush with the clip edge and remains 1px thick. + const int extraRight = xOffset < 0 ? qRound(-xOffset) : 0; + const int extraBottom = yOffset < 0 ? qRound(-yOffset) : 0; + extraLeft = xOffset > 0 ? qRound(xOffset) : 0; + extraTop = yOffset > 0 ? qRound(yOffset) : 0; + if (extraLeft || extraRight || extraTop || extraBottom) { + const int srcW = shadow->image.width() + extraLeft + extraRight; + const int srcH = shadow->image.height() + extraTop + extraBottom; + sourceImage = QImage(srcW, srcH, QImage::Format_ARGB32_Premultiplied); + sourceImage.fill(Qt::black); + QPainter sp(&sourceImage); + sp.setCompositionMode(QPainter::CompositionMode_Source); + sp.drawImage(extraLeft, extraTop, shadow->image); + // The original source has a 1px opaque frame on ALL four sides + // (from hardPadding). After translation, only the frame on the + // offset side should remain visible (replaced by the extra + // opaque padding). Clear the original frame on ALL sides so + // only the extra padding contributes the 1px shadow. For sides + // without extra padding, clearing removes the frame entirely, + // preventing unwanted shadow on those sides. + sp.setCompositionMode(QPainter::CompositionMode_Clear); + // Top: always clear (the extra padding at top handles the shadow) + sp.fillRect(QRectF(0, extraTop, srcW, 1), Qt::transparent); + // Bottom: always clear + sp.fillRect(QRectF(0, extraTop + shadow->image.height() - 1, + srcW, 1), Qt::transparent); + // Left: always clear + sp.fillRect(QRectF(extraLeft, 0, 1, srcH), Qt::transparent); + // Right: always clear + sp.fillRect(QRectF(extraLeft + shadow->image.width() - 1, 0, + 1, srcH), Qt::transparent); + sp.end(); + } + } + if (sourceImage.isNull()) + sourceImage = shadow->image; const qreal shadowSize = config.boxSize + config.blurRadius * (config.isInner() ? 2 : 4) - + (config.isInner() ? 0 : 2 * config.spread); + + (config.isInner() ? 0 : 2 * config.spread) + 2 * hardPadding; image = QImage(qRound(shadowSize), qRound(shadowSize), QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QRectF shadowRect(image.rect()); - shadowRect.moveCenter(QRectF(shadow->image.rect()).center()); + // Center the output rect on the ORIGINAL source position within the + // enlarged source, not the enlarged source center. This keeps the 1px + // frame aligned correctly after the offset translation. + QRectF origSourceRect(extraLeft, extraTop, + shadow->image.width(), shadow->image.height()); + shadowRect.moveCenter(origSourceRect.center()); QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(Qt::NoPen); - QPainterPath clipPath = roundedRectPath(image.rect(), config); + // For hard shadows (blur=0) the image has 1px padding on each side + // so BorderImage has a non-zero center to stretch. The clip path must + // match the button's actual rounded rect (boxSize, centered in the + // padded image), not the full image rect, otherwise a 1px shadow ring + // appears on all four sides. + QRectF clipRect = image.rect(); + // For outer hard shadows, reduce clip by hardPadding to avoid a 1px + // shadow ring on all four sides. For inner shadows, use the full + // image rect so the shadow sits flush against the button edge. + if (hardPadding > 0.0 && !config.isInner()) { + clipRect = QRectF(hardPadding, hardPadding, + image.width() - 2.0 * hardPadding, + image.height() - 2.0 * hardPadding); + } + QPainterPath clipPath = roundedRectPath(clipRect, config); painter.setClipPath(clipPath); QPointF offset(xOffset, yOffset); @@ -469,7 +536,7 @@ QImage DQuickShadowProvider::requestImage(const QString &id, QSize *size, const } painter.setCompositionMode(QPainter::CompositionMode_Source); - painter.drawImage(QPointF(0, 0), shadow->image, shadowRect); + painter.drawImage(QPointF(0, 0), sourceImage, shadowRect); painter.end(); // draw color @@ -666,10 +733,6 @@ static void cleanFunction(void *image) { delete static_cast(image); } ShadowImage *DQuickShadowProvider::getRawShadow(const ShadowConfig &config) { - if (Q_UNLIKELY(qIsNull(config.blurRadius))) { - return nullptr; - } - // 限制 blurRadius 在合理范围内,防止异常值导致内存崩溃 if (Q_UNLIKELY(config.blurRadius < 0 || config.blurRadius > 100)) { return nullptr; @@ -687,13 +750,20 @@ ShadowImage *DQuickShadowProvider::getRawShadow(const ShadowConfig &config) } if (!image) { - const int effectiveBlurRadius = calculateEffectiveBlurRadius(config.blurRadius); - const qreal imageSize = (effectiveBlurRadius + config.blurRadius) * 2 + config.boxSize; + // blurRadius == 0 produces a hard-edged shadow with no blur padding. + const bool hardShadow = qIsNull(config.blurRadius); + const int effectiveBlurRadius = hardShadow ? 0 : calculateEffectiveBlurRadius(config.blurRadius); + // For hard shadows (blur=0), add 1px padding on each side so the + // generated image is larger than the BorderImage border total, + // giving BorderImage a non-zero center region to stretch. + const int hardPadding = hardShadow ? 1 : 0; + const qreal padding = effectiveBlurRadius + hardPadding; + const qreal imageSize = (effectiveBlurRadius + config.blurRadius) * 2 + config.boxSize + 2 * hardPadding; QImage source(qRound(imageSize), qRound(imageSize), QImage::Format_Alpha8); source.fill(config.isInner() ? Qt::black : Qt::transparent); - QRectF boxRect(effectiveBlurRadius, effectiveBlurRadius, - imageSize - 2 * effectiveBlurRadius, imageSize - 2 * effectiveBlurRadius); + QRectF boxRect(padding, padding, + imageSize - 2 * padding, imageSize - 2 * padding); QPainter sourcePainter(&source); sourcePainter.setRenderHint(QPainter::Antialiasing, true); sourcePainter.setPen(Qt::NoPen); @@ -709,9 +779,11 @@ ShadowImage *DQuickShadowProvider::getRawShadow(const ShadowConfig &config) sourcePainter.drawPath(roundedRectPath(boxRect, config)); sourcePainter.end(); - const QRect blurRect(0, 0, qCeil(imageSize * 0.5), qCeil(imageSize * 0.5)); - doBoxShdowBlur(source, static_cast(config.blurRadius), blurRect); - mirrorTopLeftQuadrant(source); + if (!hardShadow) { + const QRect blurRect(0, 0, qCeil(imageSize * 0.5), qCeil(imageSize * 0.5)); + doBoxShdowBlur(source, static_cast(config.blurRadius), blurRect); + mirrorTopLeftQuadrant(source); + } // you can save the source to the local directory here and add it to the qrc, // prevent repeated drawing of shadow pictures. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c6ae1d4e5..1bc46dce6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,7 @@ file(GLOB TEST_SOURCES ut_dquickopacitymask.cpp ut_dquickblitframebuffer.cpp ut_dtkdeclatative_qmls.cpp + ut_render_dark_hover.cpp ) if (DTK5) list(APPEND TEST_SOURCES diff --git a/tests/data.qrc b/tests/data.qrc index acccdb533..64c0c9906 100644 --- a/tests/data.qrc +++ b/tests/data.qrc @@ -36,5 +36,6 @@ qml/SoftwareOpacityMask.qml qml/DQuickBlitFramebuffer.qml qml/Config.qml + qml/RenderDarkHoverButton.qml diff --git a/tests/qml/RenderDarkHoverButton.qml b/tests/qml/RenderDarkHoverButton.qml new file mode 100644 index 000000000..0b9151093 --- /dev/null +++ b/tests/qml/RenderDarkHoverButton.qml @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +import QtQuick +import org.deepin.dtk as D + +Rectangle { + width: 500; height: 260 + color: "#1e1e1e" + + Column { + anchors.centerIn: parent + spacing: 24 + + D.Button { text: "Normal 按钮"; width: 200; height: 36 } + D.Button { text: "Hovered 按钮"; width: 200; height: 36; D.ColorSelector.hovered: true } + } +} diff --git a/tests/ut_render_dark_hover.cpp b/tests/ut_render_dark_hover.cpp new file mode 100644 index 000000000..ea108d454 --- /dev/null +++ b/tests/ut_render_dark_hover.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include "test_helper.hpp" +#include +#include +#include +#include + +TEST(ut_RenderDarkHoverButton, grab) +{ + DGuiApplicationHelper::ColorType oldType = DGuiApplicationHelper::instance()->themeType(); + QPalette oldPalette = qApp->palette(); + + DGuiApplicationHelper::instance()->setPaletteType(DGuiApplicationHelper::DarkType); + qApp->setPalette(DGuiApplicationHelper::standardPalette(DGuiApplicationHelper::DarkType)); + QTest::qWait(300); + + QuickViewHelper<> helper("qrc:/qml/RenderDarkHoverButton.qml"); + ASSERT_TRUE(helper.object) << helper.view->errors().count(); + helper.requestExposed(); + QTest::qWait(500); + + QImage img = helper.view->grabWindow(); + ASSERT_FALSE(img.isNull()); + ASSERT_TRUE(img.save(QStringLiteral("/tmp/dark_hover_button.png"))); + + qDebug() << "=== Dark-mode render" << img.size().width() << "x" << img.size().height() << "==="; + qDebug() << "saved /tmp/dark_hover_button.png"; + + qApp->setPalette(oldPalette); + DGuiApplicationHelper::instance()->setPaletteType(oldType); + SUCCEED(); +}