Skip to content

Commit 104597f

Browse files
committed
feat(shadow): 候选窗支持 blur+spread 浮动投影,画布扩展后以内容尺寸定位
新增 ViewShadow.Blur / Spread 字段,通过 3 次 box blur 近似高斯模糊实现 WeChat 输入法风格的四向扩散投影(blur+spread+color 可主题化配置)。 渲染层:paintBlurredShadow 在临时缓冲上绘制扩散色块、3-sigma box blur 平滑 alpha 过渡,再以 src-over 合成到主画布;3-sigma 公式 (pad = ceil(3×sqrt(blur×(blur+2)))+2)保证渐变不被截断。 布局层:shadowMargins 返回四向 padding(blur+spread+offset 共同决定), renderTree 用 (marginLeft, marginTop) 偏移内容布局、扩展画布。 RenderResult 回传四向 margin,doShowCandidates 据此以内容尺寸(去阴影后) 调用 AdjustCandidatePosition,结果再减 (marginLeft, marginTop) 得到画布 屏幕坐标,确保内容区域精确对准光标,消除阴影扩展引入的位置偏移。 主题层:views.window.shadow 新增 blur / spread 字段(Dimension 类型,支持 dp 和百分比);capability.json 标记 CapShadowBlurSpread 为 supported; 更新受影响的 golden 快照与几何指纹。
1 parent a052302 commit 104597f

6 files changed

Lines changed: 71 additions & 22 deletions

File tree

wind_input/internal/ui/manager_candidate.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,27 @@ func (m *Manager) doShowCandidates(candidates []Candidate, input string, cursorP
202202
if m.renderer != nil && m.renderer.GetLayout() == config.LayoutHorizontal {
203203
layout = LayoutHorizontal
204204
}
205+
// 阴影画布四向扩展了 shadowMargin 像素,定位应以内容尺寸(不含阴影)为基准,
206+
// 再把画布整体左移/上移 margin,使内容区域精确对准光标。
207+
sml, smt, smr, smb := 0, 0, 0, 0
208+
if renderResult != nil {
209+
sml = renderResult.ShadowMarginLeft
210+
smt = renderResult.ShadowMarginTop
211+
smr = renderResult.ShadowMarginRight
212+
smb = renderResult.ShadowMarginBottom
213+
}
214+
contentW := windowWidth - sml - smr
215+
contentH := windowHeight - smt - smb
216+
if contentW < 1 {
217+
contentW = 1
218+
}
219+
if contentH < 1 {
220+
contentH = 1
221+
}
205222
var showAbove bool
206-
windowX, windowY, showAbove = AdjustCandidatePosition(caretX, caretY, caretHeight, windowWidth, windowHeight, layout, preference)
223+
windowX, windowY, showAbove = AdjustCandidatePosition(caretX, caretY, caretHeight, contentW, contentH, layout, preference)
224+
windowX -= sml
225+
windowY -= smt
207226
m.logger.Debug("Position adjusted", "windowX", windowX, "windowY", windowY, "showAbove", showAbove, "stickyAbove", currentStickyAbove)
208227

209228
// Update sticky state if we're now showing above

wind_input/internal/ui/protocol.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ type RenderResult struct {
2020
Rects []CandidateRect // Bounding rectangles for each candidate
2121
PageUpRect *CandidateRect // Bounding rectangle for page up button
2222
PageDownRect *CandidateRect // Bounding rectangle for page down button
23+
// 阴影画布四向扩展量(像素)。blur/spread>0 时非零,用于将画布坐标还原为内容坐标。
24+
ShadowMarginLeft int
25+
ShadowMarginTop int
26+
ShadowMarginRight int
27+
ShadowMarginBottom int
2328
}
2429

2530
// CandidateCallback defines callbacks for candidate window interactions

wind_input/internal/ui/viewbox_build_vertical.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ func (r *Renderer) buildVerticalCandidateTree(
271271
Padding: Edges{Top: scD(rv.Window.PadTop), Right: scD(rv.Window.PadRight), Bottom: scD(rv.Window.PadBottom), Left: scD(rv.Window.PadLeft)}, // 完整遵循主题 window.padding 四边
272272
Background: r.fillFor(r.resolvedViews.Window.BgColor, r.resolvedViews.Window.BgImage, r.resolvedViews.Window.BgGradient, scale), // P7-C:背景图来自 views.window.background.image
273273
Border: r.windowBorder(rv.Window.BorderRadius.Scaled(scale), sc, scale),
274-
Shadow: &ViewShadow{OffsetX: rv.ShadowOffsetX.Scaled(scale), OffsetY: rv.ShadowOffsetY.Scaled(scale), Color: r.resolvedViews.ShadowColor},
274+
Shadow: &ViewShadow{OffsetX: rv.ShadowOffsetX.Scaled(scale), OffsetY: rv.ShadowOffsetY.Scaled(scale), Blur: rv.ShadowBlur.Scaled(scale), Spread: rv.ShadowSpread.Scaled(scale), Color: r.resolvedViews.ShadowColor},
275275
Children: bands,
276276
}
277277
r.appendThemeLayers(window, rv.Window.Layers, sc) // P7-C:窗口装饰层(水印等)

wind_input/internal/ui/viewbox_render.go

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@ import (
1111
"github.com/huanfeng/wind_input/pkg/theme"
1212
)
1313

14+
// shadowMargins 返回窗口画布为投影额外预留的四向 margin(像素)。
15+
// blur/spread > 0 时四向扩展(与 paintBlurredShadow 的 3-sigma 公式对齐);
16+
// 否则仅右下留出偏移空间(向后兼容无模糊投影)。sh==nil 或 Color==nil 时全零。
17+
func shadowMargins(sh *ViewShadow) (marginLeft, marginTop, marginRight, marginBottom int) {
18+
if sh == nil || sh.Color == nil {
19+
return
20+
}
21+
if sh.Blur > 0 || sh.Spread > 0 {
22+
sigma := math.Sqrt(float64(sh.Blur) * float64(sh.Blur+2))
23+
pad := int(math.Ceil(3*sigma)) + 2
24+
base := pad + sh.Spread
25+
marginLeft = base + maxInt(-sh.OffsetX, 0)
26+
marginTop = base + maxInt(-sh.OffsetY, 0)
27+
marginRight = base + maxInt(sh.OffsetX, 0)
28+
marginBottom = base + maxInt(sh.OffsetY, 0)
29+
} else {
30+
marginRight = maxInt(sh.OffsetX, 0)
31+
marginBottom = maxInt(sh.OffsetY, 0)
32+
}
33+
return
34+
}
35+
1436
// refreshResolvedViews 重建 r.resolvedViews,由渲染入口 render*V2 每次调用。
1537
// 候选窗外观直接消费 theme 包解析结果 ResolveCandidateViews(views 已 merge defaultViews
1638
// 基线、几何+颜色权威);字号/行高/竖排宽是运行时值(用户全局字号派生 + DPI scale),在此回填。
@@ -95,23 +117,7 @@ func (r *Renderer) renderTree(tree *candWindowTree) (*image.RGBA, *RenderResult)
95117
root := tree.root
96118

97119
// 先计算阴影四向 margin,再以 (marginLeft, marginTop) 为起点 layout。
98-
// blur/spread > 0:四向扩展,offset 决定不对称量;否则退化为旧右下扩展。
99-
marginLeft, marginTop, marginRight, marginBottom := 0, 0, 0, 0
100-
if sh := root.Shadow; sh != nil {
101-
if sh.Blur > 0 || sh.Spread > 0 {
102-
// 与 paintBlurredShadow 相同的公式:3-sigma 覆盖 99.7% 模糊梯度。
103-
sigma := math.Sqrt(float64(sh.Blur) * float64(sh.Blur+2))
104-
pad := int(math.Ceil(3*sigma)) + 2
105-
base := pad + sh.Spread
106-
marginLeft = base + maxInt(-sh.OffsetX, 0)
107-
marginTop = base + maxInt(-sh.OffsetY, 0)
108-
marginRight = base + maxInt(sh.OffsetX, 0)
109-
marginBottom = base + maxInt(sh.OffsetY, 0)
110-
} else {
111-
marginRight = maxInt(sh.OffsetX, 0)
112-
marginBottom = maxInt(sh.OffsetY, 0)
113-
}
114-
}
120+
marginLeft, marginTop, marginRight, marginBottom := shadowMargins(root.Shadow)
115121
Layout(root, marginLeft, marginTop, td)
116122

117123
w := root.Rect().Dx() + marginLeft + marginRight
@@ -127,7 +133,13 @@ func (r *Renderer) renderTree(tree *candWindowTree) (*image.RGBA, *RenderResult)
127133
PaintTree(root, dc, img, td)
128134
DrawDebugBanner(img)
129135

130-
res := &RenderResult{Rects: make([]CandidateRect, len(tree.items))}
136+
res := &RenderResult{
137+
Rects: make([]CandidateRect, len(tree.items)),
138+
ShadowMarginLeft: marginLeft,
139+
ShadowMarginTop: marginTop,
140+
ShadowMarginRight: marginRight,
141+
ShadowMarginBottom: marginBottom,
142+
}
131143
for i, it := range tree.items {
132144
res.Rects[i] = rectOf(i, it)
133145
}

wind_input/pkg/theme/candidate_views.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ func resolveViewNode(n ViewNode, resolveColor func(ColorRef) color.Color, defBg,
9797
if n.NextChar != nil {
9898
out.NextChar = *n.NextChar
9999
}
100+
// shadow:候选窗 views.window.shadow / 简单窗口 views.status|tooltip|toast.shadow 共用此解析。
101+
if sh := n.Shadow; sh != nil {
102+
out.ShadowOffsetX = dimOr(sh.OffsetX, Dimension{})
103+
out.ShadowOffsetY = dimOr(sh.OffsetY, Dimension{})
104+
out.ShadowBlur = dimOr(sh.Blur, Dimension{})
105+
out.ShadowSpread = dimOr(sh.Spread, Dimension{})
106+
if c := resolveColor(sh.Color); c != nil {
107+
out.ShadowColor = c
108+
}
109+
}
100110
return out
101111
}
102112

wind_input/pkg/theme/views.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ type ViewNode struct {
113113
Layers []ViewImage `yaml:"layers,omitempty"` // z 层级覆盖图(P7-C,D4):z<0 在内容下、z>0 在上
114114

115115
// 仅特定节点字段(V3-D 属性归位):
116-
Shadow *ViewShadowSpec `yaml:"shadow,omitempty"` // 仅 window:窗口投影(offset/color 已实现,blur/spread 预留
116+
Shadow *ViewShadowSpec `yaml:"shadow,omitempty"` // 窗口投影(候选窗=views.window.shadow;简单窗口=views.status/tooltip/toast.shadow
117117
Gap *Dimension `yaml:"gap,omitempty"` // 仅 candidate_list:横排候选项**横向**间距基数(旧 metrics.item_spacing)
118118
BandGap *Dimension `yaml:"band_gap,omitempty"` // 仅 candidate_list:band 间距(旧 metrics.band_gap → WindowGap)
119119
RowGap *Dimension `yaml:"row_gap,omitempty"` // 仅 candidate_list:竖排候选项**纵向**(行)间距;nil/0=紧贴(横排不用,横向间距走 gap)
@@ -158,7 +158,7 @@ type Views struct {
158158
Menu *MenuViews `yaml:"menu,omitempty"` // P4-D 弹出菜单
159159
}
160160

161-
// ViewShadowSpec 结构化窗口投影(P7-E)。offset_x/offset_y/color 已实现;blur/spread 为预留字段(渲染 later)
161+
// ViewShadowSpec 结构化窗口投影(P7-E)。offset_x/offset_y/color/blur/spread 均已实现
162162
// V3-D:归位到 window 节点(views.window.shadow)。优先级:Shadow 非 nil 时其 offset/color
163163
// 覆盖 ShadowOffset 默认 + palette.Shadow;未给的子字段回退。
164164
type ViewShadowSpec struct {
@@ -228,6 +228,9 @@ type RVNode struct {
228228
PrevImage, NextImage *RVImage // 仅 footer_bar:上/下翻页箭头图(替代内置字符,可 SVG + tint);nil=用字符
229229
PrevChar, NextChar string // 仅 footer_bar:上/下翻页字符;空=内置默认(❮/❯);PrevImage/NextImage 非 nil 时不消费
230230
LineSpacing, ColGap, TitleGap Dimension // 多行/多列布局间距(tooltip/toast 专用;零值=渲染层兜底现状)
231+
ShadowOffsetX, ShadowOffsetY Dimension // 窗口投影偏移(status/tooltip/toast 由 resolveViewNode 填充)
232+
ShadowBlur, ShadowSpread Dimension // 投影模糊/扩散半径
233+
ShadowColor color.Color // nil=无投影(简单窗口不继承 palette.Shadow 默认)
231234
}
232235

233236
// ResolvedViews 候选窗各具名 View 的解析后外观(plain 逻辑像素,渲染器直接读)。

0 commit comments

Comments
 (0)