Skip to content

Commit f25ba3e

Browse files
committed
fix(theme): base 主题 ViewImage.Ref 相对路径随继承错位
loadThemeFileWithDir 加载 base 时将 baseDir 丢弃(_ 占位), deepMerge 后 ResolveV3 用 selfDir 解析所有路径,导致来自 base 的 ViewImage.Ref(如 chevron_prev.svg)被解析到子主题目录而非 _base/ 目录,图片找不到后静默退化为内置字符。 修复:base 加载后立即调用 resolveThemePaths(base, baseDir), 把 Views 各节点的 ViewImage.Ref(background.image / layers / prev_image / next_image)及 Resources 条目相对路径升格为绝对 路径,merge 后 ResolveV3 遇到绝对路径直接返回,不受 selfDir 影响。 新增 TestFooterImagesInheritedFromBase 守护 default/msime 从 _base 继承翻页箭头图且路径可访问。
1 parent 4696f8c commit f25ba3e

2 files changed

Lines changed: 112 additions & 1 deletion

File tree

wind_input/pkg/theme/footer_image_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,63 @@ package theme
22

33
import (
44
"image/color"
5+
"os"
6+
"path/filepath"
7+
"runtime"
58
"testing"
69
)
710

11+
// sourceThemesDir 返回仓库内 themes/ 目录的绝对路径(直接使用源码目录,无需构建产物)。
12+
func sourceThemesDir(t *testing.T) string {
13+
t.Helper()
14+
_, file, _, _ := runtime.Caller(0)
15+
// file = .../wind_input/pkg/theme/footer_image_test.go → 上溯 2 层到 wind_input/,再进 themes/
16+
dir := filepath.Join(filepath.Dir(file), "..", "..", "themes")
17+
abs, err := filepath.Abs(dir)
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
if _, statErr := os.Stat(abs); os.IsNotExist(statErr) {
22+
t.Skipf("跳过测试:themes/ 目录不存在:%s", abs)
23+
}
24+
return abs
25+
}
26+
27+
// TestFooterImagesInheritedFromBase 守护回归:继承自 _base 的翻页箭头图(prev_image/next_image)
28+
// 在 default/msime 等子主题中必须解析为可访问的绝对路径——禁止路径因 selfDir 错位而断裂。
29+
func TestFooterImagesInheritedFromBase(t *testing.T) {
30+
dir := sourceThemesDir(t)
31+
m := &Manager{themeDirs: []string{dir}}
32+
33+
for _, id := range []string{"default", "msime"} {
34+
t.Run(id, func(t *testing.T) {
35+
if err := m.LoadTheme(id); err != nil {
36+
t.Fatalf("LoadTheme %s: %v", id, err)
37+
}
38+
r := m.GetResolvedV3()
39+
if r == nil {
40+
t.Fatal("resolved nil")
41+
}
42+
fb := r.Views.FooterBar
43+
if fb.PrevImage == nil {
44+
t.Fatalf("theme=%s: FooterBar.PrevImage 为 nil,_base 继承路径断裂", id)
45+
}
46+
if fb.NextImage == nil {
47+
t.Fatalf("theme=%s: FooterBar.NextImage 为 nil,_base 继承路径断裂", id)
48+
}
49+
if !filepath.IsAbs(fb.PrevImage.Ref) {
50+
t.Errorf("theme=%s: PrevImage.Ref 应为绝对路径,got %q", id, fb.PrevImage.Ref)
51+
}
52+
if _, err := os.Stat(fb.PrevImage.Ref); err != nil {
53+
t.Errorf("theme=%s: PrevImage.Ref 文件不存在:%q", id, fb.PrevImage.Ref)
54+
}
55+
if _, err := os.Stat(fb.NextImage.Ref); err != nil {
56+
t.Errorf("theme=%s: NextImage.Ref 文件不存在:%q", id, fb.NextImage.Ref)
57+
}
58+
})
59+
}
60+
}
61+
862
// TestMergeViewNode_FooterImages 守护回归:footer_bar 的 prev_image/next_image 必须能通过
963
// base 单链继承的 mergeViewNode 保留(曾漏合并这两个字段,导致薄主题配的翻页箭头图被静默丢弃)。
1064
func TestMergeViewNode_FooterImages(t *testing.T) {

wind_input/pkg/theme/manager.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,13 @@ func (m *Manager) loadThemeFileWithDir(name string) (*Theme, string, error) {
239239
return nil, "", fmt.Errorf("主题 base 链成环: %q", baseName)
240240
}
241241
seen[baseName] = true
242-
base, _, err := m.loadRawThemeByName(baseName)
242+
base, baseDir, err := m.loadRawThemeByName(baseName)
243243
if err != nil {
244244
return nil, "", fmt.Errorf("加载 base 主题 %q 失败: %w", baseName, err)
245245
}
246+
// base 的 ViewImage.Ref / resources 相对路径相对于 baseDir 解析。
247+
// 必须在 deepMerge 之前做,否则 selfDir 传入 ResolveV3 后路径上下文错位。
248+
resolveThemePaths(base, baseDir)
246249
chain = append([]*Theme{base}, chain...)
247250
cur = base
248251
}
@@ -294,6 +297,60 @@ func (m *Manager) loadThemeFromPath(path string) (*Theme, error) {
294297
return theme, nil
295298
}
296299

300+
// resolveThemePaths 把 base 主题里所有相对路径就地升格为绝对路径:
301+
// - t.Resources 各条目(ResourceRef.Light / Dark)
302+
// - t.Views 各节点的 ViewImage.Ref(background.image / layers / prev_image / next_image)
303+
//
304+
// 必须在 deepMerge 之前调用,避免 base 的相对路径被带入 self 的目录上下文后错位解析。
305+
func resolveThemePaths(t *Theme, dir string) {
306+
if t == nil || dir == "" {
307+
return
308+
}
309+
for name, ref := range t.Resources {
310+
ref.Light = resolveImagePath(ref.Light, dir)
311+
ref.Dark = resolveImagePath(ref.Dark, dir)
312+
t.Resources[name] = ref
313+
}
314+
if t.Views != nil {
315+
for _, n := range viewNodePtrs(t.Views) {
316+
resolveViewNodeImageRefs(n, dir)
317+
}
318+
}
319+
}
320+
321+
// viewNodePtrs 返回 Views 中所有 ViewNode 的指针(便于统一遍历路径字段)。
322+
func viewNodePtrs(v *Views) []*ViewNode {
323+
ptrs := []*ViewNode{
324+
&v.Window, &v.PreeditBar, &v.CandidateList,
325+
&v.Item, &v.Index, &v.Text, &v.Comment,
326+
&v.AccentBar, &v.FooterBar, &v.ModeLabel,
327+
v.Status, v.Tooltip, v.Toast,
328+
}
329+
return ptrs
330+
}
331+
332+
// resolveViewNodeImageRefs 把单个 ViewNode(含递归状态节点)的 ViewImage.Ref 解析为绝对路径。
333+
func resolveViewNodeImageRefs(n *ViewNode, dir string) {
334+
if n == nil {
335+
return
336+
}
337+
if n.Background.Image != nil {
338+
n.Background.Image.Ref = resolveImagePath(n.Background.Image.Ref, dir)
339+
}
340+
for i := range n.Layers {
341+
n.Layers[i].Ref = resolveImagePath(n.Layers[i].Ref, dir)
342+
}
343+
if n.PrevImage != nil {
344+
n.PrevImage.Ref = resolveImagePath(n.PrevImage.Ref, dir)
345+
}
346+
if n.NextImage != nil {
347+
n.NextImage.Ref = resolveImagePath(n.NextImage.Ref, dir)
348+
}
349+
resolveViewNodeImageRefs(n.Selected, dir)
350+
resolveViewNodeImageRefs(n.Hover, dir)
351+
resolveViewNodeImageRefs(n.Disabled, dir)
352+
}
353+
297354
// GetCurrentTheme returns the current theme
298355
func (m *Manager) GetCurrentTheme() *Theme {
299356
m.mu.RLock()

0 commit comments

Comments
 (0)