Skip to content

Commit 2cbd02b

Browse files
committed
feat(theme): 状态态补齐背景渐变 + 覆盖层支持(几何仍冻结避免跳动)
selected/hover 此前只合并颜色/背景图/边框/字体,渐变与覆盖层被静默丢弃。补齐: - effectiveNode 合并 BgGradient + Layers;applyNodeBox 门控纳入 BgGradient (顺带修复基态只配渐变无底色时不生效)。 - buildCandidateItem 装饰层来源由 rv.Item.Layers 改 effItem.Layers(状态可覆盖层)。 - resolveState「有无覆盖」判定纳入渐变/层(只配渐变/层的状态 patch 不再被当空丢弃)。 几何(padding/margin/font_size)仍刻意不随状态变——状态毫秒级切换若改几何会致候选框 跳动,故 state_geometry 保持 unsupported。能力模型收敛为「状态支持除几何外的全部视觉 能力」,capabilities.json 无新增键。 守护:TestEffectiveNode_StateGradientLayers(合并)+ TestResolveState_GradientLayersKept (保留渐变/层、丢弃纯几何)。
1 parent 194ecd7 commit 2cbd02b

6 files changed

Lines changed: 102 additions & 33 deletions

File tree

docs/design/theme-capability-schema.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,13 @@ JSON 形态(稳定、语言无关):
9999

100100
### 状态态覆盖范围的处理(澄清)
101101

102-
状态态 patch(`selected`/`hover`/`disabled`)schema 上是完整 `ViewNode`,但**渲染只消费颜色/边框/字重覆盖**——`resolveState` "有无覆盖"判定不看几何、`effectiveNode` 合并时也不碰 `padding`/`margin`/`font_size`。即"选中态改间距/字号"是声明可写、渲染必忽略的假字段
102+
状态态 patch(`selected`/`hover`/`disabled`)schema 上是完整 `ViewNode`**渲染消费状态态的颜色/背景图/渐变/边框/字体/层覆盖**`effectiveNode` 合并这些、`resolveState` 据此判定"有无覆盖");**唯几何(`padding`/`margin`/`font_size`)不渲染**——状态改几何会牵动行高/列宽致候选框跳动,故刻意不支持
103103

104-
故新增单一能力键 `state_geometry`(粒度="状态态能否改几何",非每个几何叶子 × 每个状态,避免矩阵爆炸),在所有有状态的 view(`item`/`index`/`text`/`comment`/`menu.item`)标 `unsupported`。编辑器据此在状态态编辑器里隐藏/灰显几何控件,只留颜色/边框/字重。转 `supported` 须先补齐 `resolveState`+`effectiveNode` 的几何消费并重做 golden。详见 `theme-dimension-inheritance.md`
104+
故用单一能力键 `state_geometry`(粒度="状态态能否改几何",非每个几何叶子 × 每个状态,避免矩阵爆炸)在所有有状态的 view(`item`/`index`/`text`/`comment`/`menu.item`)标 `unsupported`。编辑器据此在状态态编辑器里隐藏/灰显**几何**控件,保留颜色/背景图/渐变/边框/字体/层。转 `supported` 须先补齐 `resolveState`+`effectiveNode` 的几何消费并重做 golden。
105+
106+
> 状态态背景四件套(色/图/渐变)+ 层与默认态对齐:`effectiveNode` 合并 `BgColor`/`BgImage`/`BgGradient`/`Layers``applyNodeBox` 门控含 `BgGradient`,候选项装饰层取 `effItem.Layers`(非基态)。
107+
108+
详见 `theme-dimension-inheritance.md`
105109

106110
## 五、回归判据
107111

wind_input/internal/ui/viewbox_build.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,21 @@ func effectiveNode(n theme.RVNode, selected, hover bool) theme.RVNode {
172172
if st == nil {
173173
return n
174174
}
175-
// 仅合并颜色/边框/字重/字体族(状态态的有意范围);padding/margin/font_size 不合并
176-
// (capability `state_geometry`=unsupported)。需要状态态改几何时,在此补合并 + 放开 resolveState 判定
175+
// 合并颜色/背景图/渐变/边框/字体/覆盖层(状态态的有意范围);唯几何(padding/margin/font_size
176+
// 不合并——状态改几何会牵动行高/列宽致候选框跳动(capability `state_geometry`=unsupported)。
177177
out := n
178178
if st.BgColor != nil {
179179
out.BgColor = st.BgColor
180180
}
181181
if st.BgImage != nil {
182182
out.BgImage = st.BgImage
183183
}
184+
if st.BgGradient != nil {
185+
out.BgGradient = st.BgGradient
186+
}
187+
if st.Layers != nil {
188+
out.Layers = st.Layers // 整组替换(与 mergeViewNode 一致)
189+
}
184190
if st.BorderColor != nil {
185191
out.BorderColor = st.BorderColor
186192
}
@@ -205,8 +211,8 @@ func effectiveNode(n theme.RVNode, selected, hover bool) theme.RVNode {
205211
// applyNodeBox 把有效节点的背景 + 边框应用到 View——**仅配置了才设**,未配=不动(纯文本/无框,零回归)。
206212
// index/text/comment/item 统一经此上盒模型;padding 由各自构建处控制(item 行内边距含 rail 逻辑,不在此覆盖)。
207213
func (r *Renderer) applyNodeBox(v *View, eff theme.RVNode, scale float64) {
208-
if eff.BgColor != nil || eff.BgImage != nil {
209-
v.Background = r.fillFor(eff.BgColor, eff.BgImage, eff.BgGradient) // 高亮位图优先于底色
214+
if eff.BgColor != nil || eff.BgImage != nil || eff.BgGradient != nil {
215+
v.Background = r.fillFor(eff.BgColor, eff.BgImage, eff.BgGradient) // 优先级:底色 < 渐变 < 背景图
210216
}
211217
if eff.BorderColor != nil || eff.BorderWidth != (theme.Dimension{}) || eff.BorderRadius != (theme.Dimension{}) {
212218
v.Border = Border{Color: eff.BorderColor, Width: eff.BorderWidth.Scaled(scale), Radius: eff.BorderRadius.Scaled(scale)}
@@ -354,7 +360,7 @@ func (r *Renderer) buildCandidateItem(cand Candidate, sel, hov bool, st *candIte
354360
Children: itemChildren,
355361
}
356362
r.applyNodeBox(item, effItem, scale) // 统一:item 行背景 + 边框(含选中/悬停态)
357-
r.appendThemeLayers(item, rv.Item.Layers, sc) // P7-C:候选项装饰层
363+
r.appendThemeLayers(item, effItem.Layers, sc) // P7-C:候选项装饰层(effItem→状态态可覆盖 layers)
358364
return item
359365
}
360366

wind_input/internal/ui/viewbox_element_state_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,26 @@ func TestEffectiveNode(t *testing.T) {
4949
t.Errorf("hover 态 bg 应=red, got %v", eff.BgColor)
5050
}
5151
}
52+
53+
// TestEffectiveNode_StateGradientLayers 守护状态态补齐:选中态可覆盖背景渐变 + 覆盖层
54+
// (几何仍不随状态变,见 TestEffectiveNode)。
55+
func TestEffectiveNode_StateGradientLayers(t *testing.T) {
56+
grad := &theme.RVGradient{Type: "linear", Stops: []theme.RVGradientStop{
57+
{Color: color.RGBA{1, 2, 3, 255}, Pos: 0}, {Color: color.RGBA{4, 5, 6, 255}, Pos: 1},
58+
}}
59+
base := theme.RVNode{TextColor: color.RGBA{0, 0, 0, 255}}
60+
base.Selected = &theme.RVNode{BgGradient: grad, Layers: []theme.RVImage{{Ref: "wm", Z: 1}}}
61+
62+
// 基态:不应带 selected 的渐变/层。
63+
if eff := effectiveNode(base, false, false); eff.BgGradient != nil || eff.Layers != nil {
64+
t.Errorf("基态不应有 selected 的渐变/层, got grad=%v layers=%v", eff.BgGradient, eff.Layers)
65+
}
66+
// 选中态:渐变 + 层被合并。
67+
eff := effectiveNode(base, true, false)
68+
if eff.BgGradient != grad {
69+
t.Errorf("选中态渐变未合并, got %v", eff.BgGradient)
70+
}
71+
if len(eff.Layers) != 1 || eff.Layers[0].Ref != "wm" {
72+
t.Errorf("选中态覆盖层未合并, got %v", eff.Layers)
73+
}
74+
}

wind_input/pkg/theme/candidate_views.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,17 @@ func ResolveCandidateViews(views Views, pal ResolvedPalette) ResolvedViews {
172172
// 复用 resolveViewNode 做完整 ViewNode→RVNode 解析(含几何/边框/字体),再注入该态的 palette
173173
// 默认底色/文字色(defBg/defText,nil=无默认)。全空且无 palette 默认 → 返回 nil(该态无覆盖)。
174174
//
175-
// nil-gating:仅当 patch 显式提供了 bg/bgImage/text/border 色/border 宽/字重,或存在 palette
176-
// 默认色时,才视为「有覆盖」并返回非 nil(与旧 RVState 语义一致,守 golden)
175+
// nil-gating:仅当 patch 显式提供了 bg/bgImage/渐变/层/text/border 色/border 宽/字重,或存在
176+
// palette 默认色时,才视为「有覆盖」并返回非 nil。
177177
//
178-
// 有意不看几何:padding/margin/font_size 不计入"有无覆盖"判定——状态态几何当前不渲染
179-
// (capability `state_geometry`=unsupported)。即只改 padding 的 selected 态会被视为空 patch 而丢弃。
178+
// 有意不看几何:padding/margin/font_size 不计入"有无覆盖"判定——状态态几何**刻意不渲染**
179+
// (状态改几何会牵动行高/列宽致候选框跳动,capability `state_geometry`=unsupported)。
180+
// 即只改 padding 的 selected 态会被视为空 patch 而丢弃;但只改渐变/层的会保留(已支持)。
180181
func resolveState(node *ViewNode, defBg, defText color.Color, resolveColor func(ColorRef) color.Color) *RVNode {
181182
has := defBg != nil || defText != nil
182183
if node != nil {
183184
if resolveColor(node.Background.Color) != nil || node.Background.Image != nil ||
185+
(node.Background.Gradient != nil && len(node.Background.Gradient.Stops) > 0) || len(node.Layers) > 0 ||
184186
resolveColor(node.Color) != nil || resolveColor(node.Border.Color) != nil ||
185187
node.Border.Width != nil || node.FontWeight != nil {
186188
has = true

wind_input/pkg/theme/capability.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,23 @@ const (
3636
CapStateHover = "state_hover"
3737
CapStateDisabled = "state_disabled"
3838
// CapStateGeometry:状态态(selected/hover/disabled)能否覆盖几何(padding/margin/字号)。
39-
// 当前渲染消费仅限颜色/边框/字重——resolveState 判定不看几何、effectiveNode 也不合并几何,
40-
// 故状态态的 padding/margin/font_size 在 schema 可写但不渲染(假字段)→ 有状态的 view 标 unsupported。
41-
CapStateGeometry = "state_geometry"
42-
CapLayers = "layers"
43-
CapShadowOffset = "shadow_offset"
44-
CapShadowBlurSpread = "shadow_blur_spread"
45-
CapLineSpacing = "line_spacing"
46-
CapColGap = "col_gap"
47-
CapTitleGap = "title_gap"
48-
CapItemSpacing = "item_spacing"
49-
CapBandGap = "band_gap"
50-
CapRowGap = "row_gap"
51-
CapIndexLabels = "index_labels"
52-
CapAccentBar = "accent_bar"
53-
CapFooterArrowImage = "footer_arrow_image"
54-
CapPager = "pager"
55-
CapModeStates = "mode_states"
39+
// 状态态支持颜色/背景图/渐变/边框/字体/层覆盖,**唯几何不支持**——状态改几何会牵动行高/列宽
40+
// 致候选框跳动(effectiveNode 不合并几何、resolveState 判定不看几何)→ 有状态的 view 标 unsupported。
41+
CapStateGeometry = "state_geometry"
42+
CapLayers = "layers"
43+
CapShadowOffset = "shadow_offset"
44+
CapShadowBlurSpread = "shadow_blur_spread"
45+
CapLineSpacing = "line_spacing"
46+
CapColGap = "col_gap"
47+
CapTitleGap = "title_gap"
48+
CapItemSpacing = "item_spacing"
49+
CapBandGap = "band_gap"
50+
CapRowGap = "row_gap"
51+
CapIndexLabels = "index_labels"
52+
CapAccentBar = "accent_bar"
53+
CapFooterArrowImage = "footer_arrow_image"
54+
CapPager = "pager"
55+
CapModeStates = "mode_states"
5656
)
5757

5858
// capabilityKeys 能力键白名单。
@@ -120,7 +120,7 @@ var ThemeCapabilities = []ViewCapability{
120120
CapBackgroundColor: CapSupported, CapBackgroundImage: CapSupported, CapLayers: CapSupported,
121121
CapStateSelected: CapSupported, CapStateHover: CapSupported,
122122
CapStateDisabled: CapUnsupported, // 候选项无禁用业务语义(Candidate 无 disabled 字段)
123-
CapStateGeometry: CapUnsupported, // 状态态仅覆盖颜色/边框/字重,几何不渲染
123+
CapStateGeometry: CapUnsupported, // 几何不随状态变(避免跳动);色/图/渐变/边框/字体/层可覆盖
124124
CapBackgroundGradient: CapSupported,
125125
}},
126126
{"index", map[string]CapabilityStatus{
@@ -129,18 +129,18 @@ var ThemeCapabilities = []ViewCapability{
129129
CapBackgroundShape: CapSupported, CapIndexLabels: CapSupported,
130130
CapStateSelected: CapSupported, CapStateHover: CapSupported,
131131
CapStateDisabled: CapUnsupported,
132-
CapStateGeometry: CapUnsupported, // 状态态仅覆盖颜色/边框/字重,几何不渲染
132+
CapStateGeometry: CapUnsupported, // 几何不随状态变(避免跳动);色/图/渐变/边框/字体/层可覆盖
133133
CapBackgroundGradient: CapSupported,
134134
}},
135135
{"text", map[string]CapabilityStatus{
136136
CapMargin: CapSupported, CapTextColor: CapSupported, CapFont: CapSupported,
137137
CapStateSelected: CapSupported, CapStateHover: CapSupported, CapStateDisabled: CapUnsupported,
138-
CapStateGeometry: CapUnsupported, // 状态态仅覆盖颜色/字重,几何不渲染
138+
CapStateGeometry: CapUnsupported, // 几何不随状态变(避免跳动);色/图/渐变/边框/字体可覆盖
139139
}},
140140
{"comment", map[string]CapabilityStatus{
141141
CapMargin: CapSupported, CapTextColor: CapSupported, CapFont: CapSupported,
142142
CapStateSelected: CapSupported, CapStateHover: CapSupported, CapStateDisabled: CapUnsupported,
143-
CapStateGeometry: CapUnsupported, // 状态态仅覆盖颜色/字重,几何不渲染
143+
CapStateGeometry: CapUnsupported, // 几何不随状态变(避免跳动);色/图/渐变/边框/字体可覆盖
144144
}},
145145
{"accent_bar", map[string]CapabilityStatus{
146146
CapAccentBar: CapSupported, CapBackgroundColor: CapSupported,
@@ -187,7 +187,7 @@ var ThemeCapabilities = []ViewCapability{
187187
CapPadding: CapSupported, CapBorder: CapSupported,
188188
CapBackgroundColor: CapSupported, CapTextColor: CapSupported, CapFont: CapSupported,
189189
CapStateHover: CapSupported, CapStateDisabled: CapSupported,
190-
CapStateGeometry: CapUnsupported, // 状态态仅覆盖颜色/边框/字重,几何不渲染
190+
CapStateGeometry: CapUnsupported, // 几何不随状态变(避免跳动);色/图/渐变/边框/字体/层可覆盖
191191
}},
192192
{"menu.separator", map[string]CapabilityStatus{
193193
CapBackgroundColor: CapSupported, // 作分隔线色
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package theme
2+
3+
import (
4+
"image/color"
5+
"testing"
6+
)
7+
8+
// TestResolveState_GradientLayersKept 守护状态态补齐:只配渐变/层的状态 patch 不再被
9+
// "有无覆盖"判定当空 patch 丢弃(渐变/层已支持);只配几何的仍丢弃(state_geometry unsupported)。
10+
func TestResolveState_GradientLayersKept(t *testing.T) {
11+
noColor := func(ColorRef) color.Color { return nil } // 颜色全 nil,隔离出渐变/层/几何的判定
12+
13+
// 只配渐变 → 保留(非 nil)。
14+
gradOnly := &ViewNode{Background: ViewFill{Gradient: &ViewGradient{Stops: []ViewGradientStop{{}, {}}}}}
15+
if resolveState(gradOnly, nil, nil, noColor) == nil {
16+
t.Error("只配渐变的状态 patch 不应被丢弃")
17+
}
18+
19+
// 只配覆盖层 → 保留。
20+
layerOnly := &ViewNode{Layers: []ViewImage{{Ref: "x"}}}
21+
rv := resolveState(layerOnly, nil, nil, noColor)
22+
if rv == nil {
23+
t.Fatal("只配 layers 的状态 patch 不应被丢弃")
24+
}
25+
if len(rv.Layers) != 1 {
26+
t.Errorf("状态 patch 应解析出 Layers, got %d", len(rv.Layers))
27+
}
28+
29+
// 只配几何(padding)→ 仍丢弃(几何不随状态变,避免跳动)。
30+
geomOnly := &ViewNode{Padding: ViewEdges{Top: dimp(4)}}
31+
if resolveState(geomOnly, nil, nil, noColor) != nil {
32+
t.Error("只配几何的状态 patch 应被丢弃(state_geometry unsupported)")
33+
}
34+
}

0 commit comments

Comments
 (0)