跳转至

05 · 样式与主题

Style 是什么

style.Style 是不可变的外观与盒模型声明。从零值起头,再链式方法叠加:

下文示例统一假定已通过 activeTheme := core.ActiveTheme(ctx) 取得当前主题值, 颜色直接引用主题的 Colors 字段(主题是值,不是查询——见 architecture):

primary := style.New().Background(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Fill)).
    TextColor(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].OnFill)).
    Radius(8).
    Padding(6, 12).
    Cursor(style.CursorPointer)

挂到控件:

button.New("确定").Style(primary)
// 或
layout.Box(child).Style(style.New().Padding(24))
// 需要拆开写时,一次调用接收多条声明(按顺序合并,后者覆盖前者)
layout.Box(child).Style(style.New().Padding(24), style.New().MaxWidth(760))

不必为每个属性写一次 .Style(...):链式声明能放进同一次调用,多条声明也能一次传入。重复调用 .Style(...) 同样累积(layout.Box 走 style.Join)。

没有单独的 Builder / Build 步骤;每次方法返回新的 Style 值。

级联顺序(固定)

组件 defaults
  → 继承文本(父级 Text 声明)
  → variant
  → size
  → resolve.Scope 祖先
  → 实例 Style
  → Transitions

每一层先应用自身匹配的状态规则,再参与层间合并;后面的层覆盖前面的层。 同一层的规则按 Rank、When 所需状态位数量、声明顺序依次应用。 On 要求掩码内所有状态成立,OnAny 表示任一状态成立,OnNot 额外排除状态。

On:状态样式

primary := style.New().Background(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Fill)).
    TextColor(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].OnFill)).
    Radius(8).
    Cursor(style.CursorPointer).
    On(style.Hovered, style.New().Background(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Hover))).
    On(style.Pressed, style.New().Background(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Pressed)).Scale(0.96)).
    On(style.Disabled, style.New().Opacity(0.5))

常见状态位:style.Hovered、Pressed、FocusVisible、Disabled、Checked、Selected、Invalid、Loading 等,可用 | 组合(如 OnAny(style.Hovered|style.Focused, ...))。

把 Model 布尔值 接进同一条路径——声明每帧由 View 重建,条件直接折叠进构造:

if model.Highlighted {
    primary = primary.Background(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Hover))
}

按钮变体

按钮的语义颜色、Hover/Pressed/Disabled 反馈和焦点环由内置变体统一处理, 通过 Variant 选择操作层级;几何参数仍由按钮组件负责:

button.New("保存").Variant(button.Primary)
button.New("取消").Variant(button.Secondary)
button.New("删除").Variant(button.Danger)
button.New("更多").Variant(button.Ghost)

需要额外的局部样式时,在变体之上追加实例声明:

button.New("删除").Variant(button.Danger).Style(style.New().Radius(4))

resolve.Scope:子树默认

作用域入口在 style/resolve 子包(style 本身是纯声明机制,不依赖框架,需要上下文的入口 都在它旁边),下文按 resolve 引用:

resolve.Scope(
    style.New().FontSize(14).TextColor(style.Solid(activeTheme.Colors.Foreground)),
    layout.Column(
        text.New("继承字号与颜色"),
        button.New("保存").Style(primary),
    ).Gap(8),
)

Scope 向下传递;实例 Style 仍优先。Scope 不会改全局 Theme。

Part:复合控件零件

根 Style 作用在外层盒子;内部用 Part:

barStyle := style.New().Background(style.RGBA(0x111827cc)).
    Part(style.PartTrack, style.New().Height(6).Background(style.Solid(activeTheme.Colors.SurfaceRaised))).
    Part(style.PartFill, style.New().Background(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Fill))).
    Part(style.PartLabel, style.New().TextColor(style.Solid(activeTheme.Colors.MutedForeground)))

progress.Bar("upload", 42).Label("上传").Style(barStyle)

常见 Part:PartContent、PartLabel、PartIcon、PartTrack、PartFill、PartThumb、PartPanel、PartItem、PartBackdrop、PartPlaceholder、PartPrefix、PartSuffix 等。

字段类控件(Select、Input 组)常用 PartContent 表示字段表面。

颜色与主题值

颜色直接引用主题的 Colors 字段,便于明暗主题切换:

style.New().Background(style.Solid(activeTheme.Colors.Surface))
style.New().TextColor(style.Solid(activeTheme.Colors.Foreground))
style.New().Background(style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Fill))

字面色与透明度:

style.RGB(0x0078d4)                             // 0xRRGGBB
style.RGBA(0x00000030)                          // 0xRRGGBBAA
style.WithAlpha(activeTheme.Colors.Ring, 0.5) // 复用主题色改 alpha

⚠️ 主题值在声明处求值:不要把带主题色的 Style 缓存进 Model 字段——主题切换后 缓存的值不会更新。声明应在渲染路径上每次构造。

渐变与自定义绘制

自定义组件需要绘制渐变时,可以先把颜色源转成画刷,再交给公共绘制函数:

brush, ok := resolve.Brush(style.LinearGradient(
    style.ColorStop(0, style.Solid(activeTheme.Colors.Tones[theme.TonePrimary].Fill)),
    style.ColorStop(1, style.Solid(activeTheme.Colors.Tones[theme.ToneDanger].Fill)),
))
if ok {
    draw.Fill(gtx, image.Rect(0, 0, 160, 32), 6, brush)
}

resolve.Color 用于取得单个具体色;draw.FillRRect 用于已有 clip.RRect 的形状。不要把渐变绘制扩展成浮层系统。

主题

启动时

默认主题入口来自 github.com/qianniancn/flowui/theme: 直接使用 theme.Light() / theme.Dark(),或调整种子后构造主题。 默认风格采用 Slate 灰阶、Indigo 强调色、1dp 边框和 32dp 常规控件; 明暗截图与尺寸规范见 默认主题与外部风格。 Light / Dark 提供默认靛蓝;New / NewDark 使用传入的种子颜色。

seed := theme.DefaultSeed()
seed.Primary = color.NRGBA{R: 0x00, G: 0x78, B: 0xd4, A: 0xff}

ui.Run(ui.NewProgram(Model{}, Update, View),
    window.Theme(theme.NewDark(seed)), // 浅色使用 theme.New(seed)
    window.Locale(window.LanguageChinese),
)

组件的局部尺寸和外观通过实例 .Style(...) 调整;全局主题只承载共享语义色、字体、阴影和动效。

运行时(多窗口 Application)

application.SetTheme("main", theme.Dark())
application.SetLanguage("main", window.LanguageChinese)

替换会在目标窗口的事件循环中生效;接口用法见 11-多窗口。

字体

默认主题使用 sans-serif 和 monospace 字体族,并允许 Gio 使用系统字体 回退。这样不需要额外资源就能显示不同语言;但不同系统的字形可能略有差异。 系统字体回退会建立完整的系统字体索引,启动和内存开销都可能明显增加;只需要 固定字体时,建议关闭它并提供完整的字体回退集合。

需要固定字体时,可以把字体文件随程序打包。opentype.ParseCollection 支持 TTF、OTF 和 TTC,直接使用 Gio 的字体解析入口:

package main

import (
    _ "embed"

    "gioui.org/font/opentype"
    "github.com/qianniancn/flowui/theme"
)

//go:embed fonts/Inter-Regular.ttf
var interRegular []byte

func appTheme() theme.Theme {
    faces, err := opentype.ParseCollection(interRegular)
    if err != nil {
        panic(err)
    }
    t := theme.Light()
    t.Typography.Typeface = "Inter, sans-serif"
    t.Fonts.Collection = faces
    t.Fonts.SystemFonts = false
    return t
}

Theme.Fonts.Collection 应在应用启动前准备好,多个窗口可以共享解析后的字体 数据;每个窗口仍会创建自己的 Shaper。关闭系统字体后,请确保集合包含应用需要 的字符范围,必要时将多个字体集合并到同一个 Collection。

将上述字体配置放入自己的应用,并提供 go:embed 引用的字体文件。 需要分析启动与绘制开销时,可在应用中使用标准库 runtime/pprof 或系统进程分析器。

几何常用项

style.New().Width(200).Height(40)
style.New().MinWidth(100).MaxWidth(400)
style.New().Padding(12)           // 全边;也可用 Padding(纵向, 横向)
style.New().Margin(8)
style.New().Radius(8)             // 或四角分别设置
style.New().BorderWidth(1).BorderColor(style.Solid(activeTheme.Colors.Border))
style.New().BorderBottom(1, style.Solid(activeTheme.Colors.Border))
style.New().BoxShadow(0, 6, 18, 0, style.RGBA(0x00000030))
style.New().Opacity(0.9)
style.New().Cursor(style.CursorPointer)
style.New().Overflow(style.OverflowHidden) // 裁剪溢出

Transition(声明式属性动画)

style.New().Background(style.Solid(activeTheme.Colors.Surface)).
    Transition(style.PropBackgroundColor, 120*time.Millisecond).
    On(style.Hovered, style.New().Background(style.Solid(activeTheme.Colors.SurfaceRaised)))

需要稳定 Key;详见 12-动画。

原则小结

  1. 外观与盒模型 → Style
  2. 排列策略 → 布局容器
  3. 状态外观 → On / Rules,不要为 hover 另建一棵 UI 树
  4. 主题色 → 直接引用 Colors 值,少写死 RGB

下一步

实例样式与主题默认外观

默认外观由各 widget 组件实现,使用 theme 提供的明暗主题 token。 .Style(...) 仅覆盖当前实例;调整全局语义颜色会影响使用这些角色的组件。

button.New("保存").Style(primary).Variant(button.Primary)