From e1b60fae1c16a950f51038ae3261cefb34b02fb2 Mon Sep 17 00:00:00 2001 From: dss Date: Thu, 20 Aug 2026 16:55:09 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E6=96=87=E5=AD=97=E6=8C=89=E9=92=AE=E7=9A=84=E7=A1=AC=E6=8A=95?= =?UTF-8?q?=E5=BD=B1=E3=80=81=E5=86=85=E9=98=B4=E5=BD=B1=E5=92=8C=E6=B8=90?= =?UTF-8?q?=E5=8F=98=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BoxPanel: 新增 enableDropShadow/enableInnerShadow/enableGradient 开关, 默认关闭以保持其他控件外观不变。硬投影(blur=0)使用两个圆角矩形 在 z=BelowOrder 层绘制,由按钮自身背景和边框自然遮挡重叠部分, 仅在按钮外部显示带圆角的投影条带 - FlowStyle: 修正 dropShadow/dropShadow2 调色板,normal/hover 远投影 rgba(0,0,0,0.05) 近投影 rgba(0,0,0,0.1),pressed 远投影 rgba(0,0,0,0.1),pressed 文字颜色与 normal 一致 - ButtonPanel: 配置三种状态的硬投影偏移量,normal/hover 为 offsetY=2+offsetY2=1,pressed 为 offsetY=1 - Button: 仅普通文字按钮启用投影、内阴影和渐变 --- qt6/src/qml/BoxPanel.qml | 115 ++++++++++++++++++---------- qt6/src/qml/Button.qml | 8 ++ qt6/src/qml/FlowStyle.qml | 52 +++++++------ qt6/src/qml/private/ButtonPanel.qml | 8 +- 4 files changed, 119 insertions(+), 64 deletions(-) diff --git a/qt6/src/qml/BoxPanel.qml b/qt6/src/qml/BoxPanel.qml index 2e0d7a6df..5ef3ec950 100644 --- a/qt6/src/qml/BoxPanel.qml +++ b/qt6/src/qml/BoxPanel.qml @@ -15,26 +15,58 @@ 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 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 - // } - // } + // 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 - DS.Style.control.borderWidth + y: backgroundRect.y - DS.Style.control.borderWidth + control.boxShadowOffsetY + width: backgroundRect.width + 2 * DS.Style.control.borderWidth + height: backgroundRect.height + 2 * DS.Style.control.borderWidth + radius: backgroundRect.radius + DS.Style.control.borderWidth + color: control.D.ColorSelector.dropShadowColor + } + + 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 - DS.Style.control.borderWidth + y: backgroundRect.y - DS.Style.control.borderWidth + control.boxShadowOffsetY2 + width: backgroundRect.width + 2 * DS.Style.control.borderWidth + height: backgroundRect.height + 2 * DS.Style.control.borderWidth + radius: backgroundRect.radius + DS.Style.control.borderWidth + color: control.D.ColorSelector.dropShadowColor2 + } Rectangle { id: backgroundRect @@ -52,38 +84,41 @@ 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 - // } - // } + 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: 2 + shadowOffsetY: control.innerShadowOffsetY1 + spread: 1 + 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: 1 + shadowOffsetY: control.innerShadowOffsetY2 + shadowColor: innerShadowColor + cornerRadius: backgroundRect.radius + } + } Loader { active: insideBorderColor 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..73171a454 100644 --- a/qt6/src/qml/FlowStyle.qml +++ b/qt6/src/qml/FlowStyle.qml @@ -100,10 +100,11 @@ 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 { @@ -111,7 +112,7 @@ QtObject { crystal: Qt.rgba(1, 1, 1, 0.08) } hovered { - common: ("#e1e1e1") + common: ("#e6e6e6") crystal: Qt.rgba(0, 0, 0, 0.2) } hoveredDark { @@ -119,7 +120,7 @@ QtObject { 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,7 +131,7 @@ QtObject { property D.Palette background2: D.Palette { normal { - common: ("#f0f0f0") + common: ("#efefef") crystal: Qt.rgba(0, 0, 0, 0.1) } normalDark { @@ -138,34 +139,43 @@ QtObject { 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) } 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.05) + pressed: Qt.rgba(0, 0, 0, 0.1) + } + + // 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.1) hovered: Qt.rgba(0, 0, 0, 0.1) + pressed: ("transparent") } property D.Palette innerShadow1: D.Palette { - normal: Qt.rgba(0, 0, 0, 0.05) + normal: ("transparent") pressed: ("transparent") } 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) + 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 +183,22 @@ 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") + hovered { + common: Qt.rgba(0, 0, 0, 0.15) + crystal: Qt.rgba(0, 0, 0, 0.2) + } + pressed: Qt.rgba(0, 0, 0, 0.15) } property D.Palette text: D.Palette { @@ -201,8 +211,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) } } } @@ -509,8 +519,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/private/ButtonPanel.qml b/qt6/src/qml/private/ButtonPanel.qml index 8dd9dfebd..80f1273af 100644 --- a/qt6/src/qml/private/ButtonPanel.qml +++ b/qt6/src/qml/private/ButtonPanel.qml @@ -17,10 +17,14 @@ 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) + // 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: selectValue(control.D.ColorSelector.controlState === D.DTK.HoveredState ? -3 : -1, -1, -1) visible: !button.flat || button.checked || button.highlighted || button.visualFocus || control.D.ColorSelector.controlState === D.DTK.PressedState || control.D.ColorSelector.controlState === D.DTK.HoveredState From 8e67b3cba532807842fa1f50876e56505846ac75 Mon Sep 17 00:00:00 2001 From: dss Date: Fri, 21 Aug 2026 13:51:20 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E6=8A=95=E5=BD=B1=E7=A1=AC=E5=BA=A6=E5=92=8C=E6=B7=B1?= =?UTF-8?q?=E8=89=B2=E6=A8=A1=E5=BC=8F=20normal=20=E8=B0=83=E8=89=B2?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BoxPanel: 投影矩形改为与 backgroundRect 同尺寸同圆角,不再 扩展 borderWidth;加 antialiasing: false 使投影边缘为硬边 - FlowStyle: dropShadow2 近投影 normal/hover 从 0.1 调整为 0.05; 新增 normalDark 调色板:深色模式渐变背景 rgba(60,60,60,1)→ rgba(45,45,45,1)、外描边 rgba(0,0,0,0.05)、顶部内阴影 rgba(0,0,0,0.5)、底部内阴影 rgba(255,255,255,0.07)、 dropShadow/dropShadow2 设为 transparent(深色无外投影) --- qt6/src/qml/BoxPanel.qml | 22 ++++++++++++---------- qt6/src/qml/FlowStyle.qml | 15 +++++++++++---- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/qt6/src/qml/BoxPanel.qml b/qt6/src/qml/BoxPanel.qml index 5ef3ec950..8b7116a2d 100644 --- a/qt6/src/qml/BoxPanel.qml +++ b/qt6/src/qml/BoxPanel.qml @@ -45,12 +45,13 @@ Item { visible: control.enableBoxShadow && control.enableDropShadow && control.boxShadowBlur === 0 && dropShadowColor && control.D.ColorSelector.dropShadowColor.a > 0 - x: backgroundRect.x - DS.Style.control.borderWidth - y: backgroundRect.y - DS.Style.control.borderWidth + control.boxShadowOffsetY - width: backgroundRect.width + 2 * DS.Style.control.borderWidth - height: backgroundRect.height + 2 * DS.Style.control.borderWidth - radius: backgroundRect.radius + DS.Style.control.borderWidth + 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 { @@ -60,12 +61,13 @@ Item { && control.boxShadowBlur === 0 && dropShadowColor2 && control.D.ColorSelector.dropShadowColor2.a > 0 && control.boxShadowOffsetY2 > 0 - x: backgroundRect.x - DS.Style.control.borderWidth - y: backgroundRect.y - DS.Style.control.borderWidth + control.boxShadowOffsetY2 - width: backgroundRect.width + 2 * DS.Style.control.borderWidth - height: backgroundRect.height + 2 * DS.Style.control.borderWidth - radius: backgroundRect.radius + DS.Style.control.borderWidth + 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 { diff --git a/qt6/src/qml/FlowStyle.qml b/qt6/src/qml/FlowStyle.qml index 73171a454..c9425106c 100644 --- a/qt6/src/qml/FlowStyle.qml +++ b/qt6/src/qml/FlowStyle.qml @@ -108,7 +108,7 @@ QtObject { 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, 1) crystal: Qt.rgba(1, 1, 1, 0.08) } hovered { @@ -135,7 +135,7 @@ QtObject { 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, 1) crystal: Qt.rgba(1, 1, 1, 0.1) } hovered { @@ -150,6 +150,7 @@ QtObject { property D.Palette dropShadow: D.Palette { normal: Qt.rgba(0, 0, 0, 0.05) + normalDark: ("transparent") hovered: Qt.rgba(0, 0, 0, 0.05) pressed: Qt.rgba(0, 0, 0, 0.1) } @@ -157,18 +158,21 @@ QtObject { // 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.1) - hovered: Qt.rgba(0, 0, 0, 0.1) + 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) pressed: ("transparent") } property D.Palette innerShadow2: D.Palette { 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") } @@ -194,6 +198,9 @@ QtObject { common: Qt.rgba(0, 0, 0, 0.1) crystal: Qt.rgba(0, 0, 0, 0.08) } + normalDark { + common: Qt.rgba(0, 0, 0, 0.05) + } hovered { common: Qt.rgba(0, 0, 0, 0.15) crystal: Qt.rgba(0, 0, 0, 0.2) From a0eb8432cc3ff6cb88bb99af7cc2aff792e1a3a8 Mon Sep 17 00:00:00 2001 From: dss Date: Wed, 26 Aug 2026 10:11:26 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=99=AE?= =?UTF-8?q?=E9=80=9A=E6=96=87=E5=AD=97=E6=8C=89=E9=92=AE=E6=B7=B1=E8=89=B2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E9=85=8D=E8=89=B2=E4=B8=8E=E7=A1=AC=E6=8A=95?= =?UTF-8?q?=E5=BD=B1=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BoxInsetShadow: blur=0 硬投影时使用对称 border 并加 1px padding, 避免非对称 border 占满中心区域导致 BorderImage 无法拉伸 - BoxPanel: 新增 overlayColor 叠加层;内阴影改为 blur=0/spread=0 硬边; 新增 __hasVisibleHardDropShadow 判断,深色模式无外投影时描边完全内绘 - InsideBoxBorder/OutsideBoxBorder: 新增 borderInside 属性,无外投影时 描边完全内绘避免按钮外侧残留细线 - FlowStyle: 深色模式渐变背景与内/外描边透明度调整,新增 hoveredDark 与 overlayColor 调色板,flatBackground 深色模式配色更新 - dquickimageprovider: 支持 blur=0 硬投影图像生成,内阴影偏移时补充 不透明 padding 填充位移产生的间隙 - 新增 ut_render_dark_hover 渲染测试 --- qt6/src/qml/BoxInsetShadow.qml | 17 +++-- qt6/src/qml/BoxPanel.qml | 24 +++++-- qt6/src/qml/FlowStyle.qml | 25 +++++-- qt6/src/qml/InsideBoxBorder.qml | 11 ++- qt6/src/qml/OutsideBoxBorder.qml | 8 ++- qt6/src/qml/private/ButtonPanel.qml | 3 +- src/private/dquickimageprovider.cpp | 102 ++++++++++++++++++++++++---- tests/CMakeLists.txt | 1 + tests/data.qrc | 1 + tests/qml/RenderDarkHoverButton.qml | 16 +++++ tests/ut_render_dark_hover.cpp | 35 ++++++++++ 11 files changed, 207 insertions(+), 36 deletions(-) create mode 100644 tests/qml/RenderDarkHoverButton.qml create mode 100644 tests/ut_render_dark_hover.cpp 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 8b7116a2d..699a24f5d 100644 --- a/qt6/src/qml/BoxPanel.qml +++ b/qt6/src/qml/BoxPanel.qml @@ -18,6 +18,7 @@ Item { 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 @@ -35,6 +36,12 @@ Item { property bool enableInnerShadow: false property bool enableGradient: false + // 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 @@ -91,6 +98,13 @@ Item { color: D.ColorSelector.color1 } + 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 @@ -99,9 +113,9 @@ Item { z: D.DTK.AboveOrder sourceComponent: BoxInsetShadow { - shadowBlur: 2 + shadowBlur: 0 shadowOffsetY: control.innerShadowOffsetY1 - spread: 1 + spread: 0 shadowColor: innerShadowColor cornerRadius: backgroundRect.radius } @@ -115,7 +129,7 @@ Item { z: D.DTK.AboveOrder sourceComponent: BoxInsetShadow { - shadowBlur: 1 + shadowBlur: 0 shadowOffsetY: control.innerShadowOffsetY2 shadowColor: innerShadowColor cornerRadius: backgroundRect.radius @@ -131,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/FlowStyle.qml b/qt6/src/qml/FlowStyle.qml index c9425106c..02e4b371f 100644 --- a/qt6/src/qml/FlowStyle.qml +++ b/qt6/src/qml/FlowStyle.qml @@ -108,7 +108,7 @@ QtObject { crystal: Qt.rgba(0, 0, 0, 0.1) } normalDark { - common: Qt.rgba(60 / 255, 60 / 255, 60 / 255, 1) + common: Qt.rgba(60 / 255, 60 / 255, 60 / 255, 0.6) crystal: Qt.rgba(1, 1, 1, 0.08) } hovered { @@ -116,7 +116,7 @@ QtObject { 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 { @@ -135,13 +135,16 @@ QtObject { crystal: Qt.rgba(0, 0, 0, 0.1) } normalDark { - common: Qt.rgba(45 / 255, 45 / 255, 45 / 255, 1) + common: Qt.rgba(45 / 255, 45 / 255, 45 / 255, 0.6) crystal: Qt.rgba(1, 1, 1, 0.1) } hovered { 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: Qt.rgba(202 / 255, 202 / 255, 202 / 255, 0.5) crystal: Qt.rgba(16.0 / 255, 16.0 / 255, 16.0 / 255, 0.15) @@ -167,8 +170,13 @@ QtObject { 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.4) @@ -199,12 +207,15 @@ QtObject { crystal: Qt.rgba(0, 0, 0, 0.08) } normalDark { - common: Qt.rgba(0, 0, 0, 0.05) + 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) } @@ -366,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) @@ -459,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) 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 80f1273af..5b5de1728 100644 --- a/qt6/src/qml/private/ButtonPanel.qml +++ b/qt6/src/qml/private/ButtonPanel.qml @@ -20,12 +20,13 @@ BoxPanel { 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) + 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: selectValue(control.D.ColorSelector.controlState === D.DTK.HoveredState ? -3 : -1, -1, -1) + 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(); +}