第参話
布局与样式
Flexbox、尺寸、安全区域、颜色、文字、阴影和渐变。
ui.Style 决定一个视图如何布局、长什么样。布局采用 CSS flexbox,单位是点(point);外观则包括颜色、圆角、边框、阴影和文字。本页会介绍这两部分,以及如何让样式随状态变化。布局在所有平台上都一样:Asuka 在 Go 里算好布局,再把平台的视图摆到对应的位置上。
平台默认值
没有设置的样式会沿用平台的默认值。一个完全不设样式的应用,看起来就和这个平台自带的应用一样,字体、颜色、深色模式和文字大小都与之一致:
- iOS:UIKit 的风格。
- Android:Material 3 的风格。
- macOS:AppKit 的风格。
- Windows:Windows 11 的风格(WinUI 的 Fluent 外观,由宿主在 Win32 控件上绘制)。
- Linux:GNOME 的风格(GTK 4 和 libadwaita)。
- Web:浏览器自带的控件,外围配上 Asuka 自己的一套浅色和深色配色(见 Web)。
只设置你真正想改的部分;颜色和字体尽量用下面介绍的语义颜色和语义字体,而不是写死的值,这样应用才能一直跟随平台。
样式
ui.Style 就是一个普通的结构体。组件接收的样式类型是 Styler:可以是一个 Style,也可以是一个 ui.StyleFunc,用于会变化的样式(见会变化的样式)。
ui.Text(title, ui.Style{Font: fonts.Headline, Color: colors.SecondaryLabel})
ui.Button(ui.ButtonProps{Title: ui.Val("Follow"), Style: ui.Style{Shrink: ui.NoShrink}})
ui.View(ui.ViewProps{Style: ui.Style{Height: ui.Pt(1), Background: colors.Separator}})视觉属性只在原生控件支持的地方生效:按钮会把 Color 当作着色(tint),而视图会忽略 Font。
排列视图
ui.Column 把子视图从上到下排列,ui.Row 则从行首排到行尾。ui.View 是一个容器,排列方向由样式中的 Direction 决定:默认是 ui.ColumnDir,也可以设为 ui.RowDir。样式传 nil 等同于传一个空样式。
ui.Row(ui.Style{Align: ui.AlignCenter, Gap: 12},
Avatar(),
ui.Column(ui.Style{Grow: 1, Gap: 2},
ui.StaticText("Ada Lovelace", ui.Style{Font: fonts.Headline}),
ui.StaticText("Notes on the Analytical Engine", ui.Style{Font: fonts.Subheadline, Color: colors.SecondaryLabel, Lines: 1}),
),
ui.Button(ui.ButtonProps{Title: ui.Val("Follow"), Style: ui.Style{Shrink: ui.NoShrink}}),
)只设置了布局属性的容器不会生成原生视图:它的子视图会被直接放进最近的那个有原生视图的祖先里。因此多嵌套几层也没有任何开销。
Flexbox
布局遵循 CSS flexbox,默认值与 React Native 相同:默认按列排列,尺寸包含内边距和边框。
Gap:子视图之间的间距;换行时也是行与行之间的间距。Justify决定子视图在主轴上的分布:ui.JustifyStart(默认)、ui.JustifyCenter、ui.JustifyEnd、ui.JustifySpaceBetween、ui.JustifySpaceAround、ui.JustifySpaceEvenly。Align决定子视图在交叉轴上的对齐方式:ui.AlignStretch(默认,子视图撑满交叉轴)、ui.AlignStart、ui.AlignCenter、ui.AlignEnd、ui.AlignBaseline。AlignSelf为单个子视图覆盖父视图的Align;零值ui.AlignAuto表示沿用父视图的设置。Wrap允许子视图在放不下时折到下一行。Grow表示子视图在父视图主轴上分得的剩余空间份额。会伸展的子视图同样可以缩到比内容更小,所以把Scroll包在一个设置了Grow的视图里时,内容会正常滚动,而不会把这个视图撑到滚动内容的完整高度。Shrink表示一行放不下时子视图让出多少空间。零值 0 表示使用默认值 1:文字会换行,控件则保持其最小尺寸。ui.NoShrink让子视图保持原有尺寸。Basis是子视图在主轴上的初始尺寸;默认值 Auto 表示取内容的尺寸。
// 两个盒子分摊宽度,其中一个是另一个的两倍。
ui.Row(ui.Style{Gap: 8, Height: ui.Pt(60)},
ui.View(ui.ViewProps{Style: ui.Style{Grow: 2, Background: colors.SecondaryBackground}}),
ui.View(ui.ViewProps{Style: ui.Style{Grow: 1, Background: colors.SecondaryBackground}}),
)
// 标签和数值分居两端。
ui.Row(ui.Style{Justify: ui.JustifySpaceBetween, Align: ui.AlignCenter},
ui.StaticText("Total"),
ui.StaticText("$42", ui.Style{Font: fonts.Title2}),
)
// 放不下时会折行的标签。
ui.Row(ui.Style{Wrap: true, Gap: 8},
ui.StaticText("Go"), ui.StaticText("iOS"), ui.StaticText("Android"),
)尺寸
长度用 ui.Dim 表示:
ui.Pt(v):以点为单位。对应 iOS 和 macOS 的点、Android 的 dp、Windows 上 100% 缩放时的一个像素(1/96 英寸)、Linux 上 GTK 的逻辑像素,以及 Web 上的 CSS 像素。ui.Pct(v):父视图在同一方向上尺寸的百分比。ui.Auto(零值):取内容的尺寸,或由 flexbox 分配。
Width 和 Height 设置尺寸;MinWidth、MinHeight、MaxWidth 和 MaxHeight 限定尺寸的范围。AspectRatio 是宽高比(宽除以高):已知一边时,它会算出另一边。
ui.View(ui.ViewProps{Style: ui.Style{
Width: ui.Pct(50),
MinWidth: ui.Pt(120),
MaxWidth: ui.Pt(320),
AspectRatio: 16.0 / 9,
Background: colors.Gray,
}})文字和控件会用平台自己的测量方式算出自身尺寸,只有想改变它们的大小时才需要指定尺寸。
一个点对应多少像素取决于屏幕的缩放比例:手机上是 2 或 3,Retina 屏的 Mac 上是 2,Windows 和 Linux 上取显示器的缩放设置(150% 时是 1.5),Web 上则是浏览器的设备像素比。具体是多少可以看 ui.UseEnvironment() 的 Scale。把桌面窗口拖到缩放比例不同的显示器上时,窗口会按新显示器的比例绘制,而以点为单位的布局保持不变。
内边距与外边距
Padding 和 Margin 都是 ui.Edges,可以分别设置每一边,单位是点:
ui.All(v):四边都是 v。ui.Symmetric(v, h):上下为 v,左右为 h。ui.Edges{Top: 24}:按名字逐边指定。
ui.Column(ui.Style{Padding: ui.Symmetric(8, 16), Margin: ui.Edges{Top: 24}},
ui.StaticText("Padded"),
)在从右到左书写的语言里,Row 从右侧开始排列,Justify 和 Align 也随之从右侧算起,文字的行首在右边。Left 和 Right 则和 CSS 一样,说左就是左、说右就是右。如果想把间距放在文字开始或结束的那一侧,请用 Start 或 End:
ui.Style{Padding: ui.Edges{Start: 16, End: 8}} // 英语里左边是 16,阿拉伯语里则是右边参见多语言。
绝对定位
Absolute 让视图脱离文档流,再用 Top、Right、Bottom 和 Left 指定它在父视图中的位置。下面的例子在头像的角上放了一个徽标:
ui.View(ui.ViewProps{Style: ui.Style{Width: ui.Pt(44), Height: ui.Pt(44)}},
Avatar(),
ui.View(ui.ViewProps{Style: ui.Style{
Absolute: true, Top: ui.Pt(-4), Right: ui.Pt(-4),
Width: ui.Pt(18), Height: ui.Pt(18), CornerRadius: 9,
Background: colors.Red, Align: ui.AlignCenter, Justify: ui.JustifyCenter,
}}, Badge()),
)排在后面的子视图会画在前面的子视图之上。
Grid
设置了 Grid 的容器会按 CSS grid 排列子视图。网格轨道由 layout 包提供:layout.Fr、layout.Px、layout.Repeat、layout.Minmax 等。子视图通过 GridColumn 和 GridRow 跨越多条轨道。
ui.View(ui.ViewProps{Style: ui.Style{Gap: 8, Grid: &ui.Grid{
Columns: []layout.GridTrack{layout.Repeat(3, layout.Fr(1))},
}}},
Avatar(), Avatar(), Avatar(), Avatar(),
ui.View(ui.ViewProps{Style: ui.Style{GridColumn: layout.Placement{Start: layout.Span(3)}, Height: ui.Pt(20), Background: colors.Gray}}),
)如果是内容很多、需要滚动的网格,请使用带 Columns 的 ui.List(见组件)。
安全区域、键盘与窗口
在手机上,根视图会铺满整个屏幕,延伸到状态栏、刘海和主屏幕指示条下面。ui.SafeArea 是一个 Column,它在自身内边距的基础上,再加上安全区域的边距。键盘弹出时,它的底部内边距就是键盘的高度,所以放在底部的内容(比如文本框、发送按钮)始终会留在键盘上方。
ui.SafeArea(ui.Style{Padding: ui.All(16), Gap: 12},
ui.StaticText("Todos", ui.Style{Font: fonts.LargeTitle.Weighted(fonts.Bold)}),
ui.Scroll(ui.ScrollProps{Style: ui.Style{Grow: 1}}, rows...),
composer,
)这些边距取自它所在的那个页面:在导航栈的页面、标签页或弹出面板(sheet)里,就是那个页面的边距。如果要自己摆放内容,可以通过 ui.UseEnvironment 拿到这些边距和键盘高度(见核心概念)。
在 macOS、Windows 和 Linux 上,根视图铺满窗口的内容区域,位于标题栏(Linux 上是 header bar)下方。桌面窗口没有安全区域,也没有会遮住窗口的屏幕键盘:边距和键盘高度都是 0,ui.SafeArea 就是一个只带自身内边距的 Column。不过还是建议照样使用它,这样同一个页面也能直接适配手机。
除非应用另行指定,窗口大小是 800×600 点。可以用 app.RunWindow 设置窗口的尺寸,以及用户最小能把它缩到多大:
func main() {
app.RunWindow(ui.WindowOptions{Width: 960, Height: 640, MinWidth: 480, MinHeight: 360}, App)
}手机会忽略这些选项。用户调整窗口大小时,环境里的 Width 和 Height 会随之变化,布局也会重新计算,你不需要做任何处理。如果想在特定尺寸下改变布局(比如空间足够时才显示侧边栏),可以像在平板上那样,在 StyleFunc 或 Show 里读取环境的 WidthClass。
在浏览器中,铺满整个页面的应用会从视觉视口(visual viewport)获取设备的安全区域(env(safe-area-inset-*),适用于带刘海的手机)和屏幕键盘的高度。嵌在页面某个局部的应用则两者都没有。
滚动
ui.Scroll 让子视图可以滚动,默认是纵向,设置 Horizontal 后改为横向。记得给它一个尺寸或者 Grow: 1,让它有一个可以在其中滚动的区域。Style 是滚动视图本身的盒子;ContentStyle 是子视图排列所在的盒子,内边距和间距都应该设在这里。
ui.Scroll(ui.ScrollProps{Style: ui.Style{Grow: 1}, ContentStyle: ui.Style{Padding: ui.All(16), Gap: 12}},
ui.StaticText("Long content"),
)颜色
ui.Color 来自 colors 包(github.com/egoist/asuka/colors),有以下几种:
- 平台默认色:即零值,也就是不设置该属性。
- 语义颜色:由平台解析,因此会跟随深色模式变化:
- 文字:
colors.Label、colors.SecondaryLabel、colors.TertiaryLabel; - 背景:
colors.Background、colors.SecondaryBackground、colors.GroupedBackground,以及用于其上分组列表行的colors.SecondaryGroupedBackground(对应 iOS 的 inset grouped 单元格); colors.Separator、colors.Accent(应用的强调色)、colors.OnAccent(在强调色上清晰可读的颜色,比如以强调色填充的按钮上的图标)、colors.Clear(透明);- 系统色调:
colors.Red、colors.Orange、colors.Yellow、colors.Green、colors.Blue、colors.Purple、colors.Pink、colors.Gray。
- 文字:
- 固定颜色:
colors.RGB(r, g, b)、colors.RGBA(r, g, b, a)、colors.Hex(0x7c3aed),或者colors.ParseHex("#7c3aed")(也支持"#rrggbbaa"),后者会返回是否解析成功。
各平台的语义颜色来源:
| 平台 | 语义颜色 |
|---|---|
| iOS | UIKit 的颜色(labelColor、systemBackground、systemRed 等) |
| Android | Material 3 的颜色角色(color roles) |
| macOS | AppKit 的颜色(labelColor、windowBackgroundColor、controlBackgroundColor、separatorColor、systemRedColor 等) |
| Windows | Windows 11 的主题色(WinUI 的 Fluent 主题资源),分浅色和深色 |
| Linux | libadwaita 的具名颜色(window_fg_color、window_bg_color、card_bg_color、destructive_color 等),取主题中的定义,高对比度主题也包括在内 |
| Web | Asuka 自己的配色,以 CSS 变量的形式提供,分浅色和深色 |
在桌面端,分组背景就是普通背景:colors.GroupedBackground 是窗口背景,colors.SecondaryGroupedBackground 和 colors.SecondaryBackground 一样,是卡片的背景。
ui.Column(ui.Style{Padding: ui.All(16), Gap: 4, Background: colors.Background, CornerRadius: 12},
ui.StaticText("Title", ui.Style{Font: fonts.Headline}),
ui.StaticText("Details", ui.Style{Font: fonts.Subheadline, Color: colors.SecondaryLabel}),
)c.Opacity(a) 会以 0 到 1 之间的不透明度绘制颜色(相当于 SwiftUI 的 .opacity、Compose 的 copy(alpha =))。语义颜色调整不透明度之后仍然是语义颜色,所以 colors.Accent.Opacity(0.15) 在浅色和深色模式下都是强调色的一层淡色,适合用作标签的背景、图表的填充或滑轨。
Color 是文字的颜色,或者控件的着色。
背景、边框与阴影
Background:视图的背景色。CornerRadius:圆角半径,单位是点。BorderWidth和BorderColor:边框,画在盒子内侧。它会像内边距一样把内容往里推。Shadow:投影。投影由视图的形状(也就是它的边界,再按CornerRadius取圆角)产生,与视图里画了什么无关。可设置Color(不设则没有投影)、Radius(模糊半径,与 CSS 和 SwiftUI 的含义相同,即模糊 σ 的两倍)以及偏移量X和Y。裁剪子视图的视图(设置了Clip)照样会投下阴影,就像一张裁剪了图片的圆角卡片那样。Android 上画的是它自己的高度(elevation)阴影:半径对应高度,颜色为阴影着色,偏移量由光源决定。macOS、Windows、Linux 和 Web 则与 iOS 的画法一致,模糊和偏移效果都相同。Gradient:覆盖在背景上的线性渐变。ui.LinearGradient(colors...)从上往下渐变;ui.Gradient可以用ui.Point设置Start和End,以视图的尺寸为单位:(0, 0) 是顶部行首的那个角,(1, 1) 是底部行尾的那个角;还可以用Locations为每个颜色指定一个位置。Android 会沿八个方向中最接近的那个绘制渐变;其他平台则严格沿你给出的线条绘制。Material:一种半透明、会模糊其后内容的背景,即 iOS 的材质(material):ui.MaterialUltraThin、ui.MaterialThin、ui.MaterialRegular、ui.MaterialThick、ui.MaterialBar。- iOS:
UIVisualEffectView的模糊效果。 - macOS:在视图内容后面放一个
NSVisualEffectView,模糊窗口中位于其后的内容。 - Windows:由宿主绘制的 Fluent 亚克力效果:把视图背后的内容模糊,再叠上浅色或深色的色调。
- Web:CSS 的
backdrop-filter,即模糊加饱和度。 - Android 和 Linux 无法模糊视图背后的内容,因此显示半透明的表面色(Linux 上是 libadwaita 的卡片颜色)。
- iOS:
Clip:把子视图裁剪到视图的边界和圆角之内。
ui.View(ui.ViewProps{Style: ui.Style{
Height: ui.Pt(120),
CornerRadius: 16,
Gradient: ui.LinearGradient(colors.Hex(0x6366f1), colors.Hex(0xec4899)),
Shadow: ui.Shadow{Color: colors.RGBA(0, 0, 0, 80), Radius: 16, Y: 8},
}})
ui.View(ui.ViewProps{Style: ui.Style{Padding: ui.All(12), BorderWidth: 1, BorderColor: colors.Separator, CornerRadius: 8, Clip: true}},
ui.StaticText("Bordered"),
)
// 从左到右。
ui.View(ui.ViewProps{Style: ui.Style{
Height: ui.Pt(80),
Gradient: &ui.Gradient{Colors: []ui.Color{colors.Blue, colors.Purple}, Start: ui.Point{X: 0, Y: 0.5}, End: ui.Point{X: 1, Y: 0.5}},
}})不透明度与隐藏
Opacity 的取值范围是 0 到 1,但和其他未设置的属性一样,它的零值表示完全不透明:Opacity: 0.5 是半透明,而 Opacity: 0 反而是完全不透明。要隐藏视图,请设置 Hidden。隐藏的视图仍然在布局中占据空间;如果想把它连同其中的视图一起从布局中移除,请使用 ui.Show(见核心概念)。
变换
Transform 在绘制时对视图进行平移、缩放和旋转(TranslateX、TranslateY、Scale、ScaleX、ScaleY,以及以角度为单位的 Rotate);布局感知不到这些变换。所有平台都支持。变换、Transition 和 Match 主要用于动画,见动效。
文字
Font 设置文字的字体,类型是 fonts 包(github.com/egoist/asuka/fonts)中的 fonts.Font:
- 语义字体:平台的文本样式,会随用户设置的文字大小缩放:
fonts.LargeTitle、fonts.Title、fonts.Title2、fonts.Title3、fonts.Headline、fonts.Subheadline、fonts.Body、fonts.Callout、fonts.Footnote、fonts.Caption、fonts.Caption2。 fonts.System(size):指定字号(单位是点)的系统字体。.Weighted(w)为字体指定字重:fonts.Ultralight、fonts.Thin、fonts.Light、fonts.Regular、fonts.Medium、fonts.Semibold、fonts.Bold、fonts.Heavy、fonts.Black。
Color 是文字颜色,TextAlign 是对齐方式(ui.TextStart、ui.TextCenter、ui.TextEnd、ui.TextJustify),Lines 是最多显示的行数,默认值 0 表示不限制。文字默认会自动换行。
ui.StaticText("Brand", ui.Style{Color: colors.RGB(124, 58, 237), Font: fonts.Title.Weighted(fonts.Bold)})
ui.StaticText("A long paragraph that stops after two lines.", ui.Style{Font: fonts.Body, Lines: 2, TextAlign: ui.TextCenter})
ui.StaticText("Fixed size", ui.Style{Font: fonts.System(20).Weighted(fonts.Medium)})如果一段文字里要混用多种样式、链接或内嵌视图,请看 RichText。
这些文本样式沿用了 iOS 的命名,各平台会把它们映射到自己的字号体系上,所以 fonts.Body 就是该平台应用正文所用的大小:
| 平台 | 文本样式 | fonts.Body |
|---|---|---|
| iOS | UIKit 的文本样式,字体为 San Francisco | 17 |
| Android | Material 3 的字体比例(type scale,fonts.Body 对应 Body Large),字体为 Roboto |
16 |
| macOS | AppKit 的文本样式(NSFont.preferredFont(forTextStyle:)),字体为 San Francisco |
13 |
| Windows | Windows 11 的字号体系(type ramp),字体为 Segoe UI Variable(Windows 11 之前为 Segoe UI) | 14 |
| Linux | libadwaita 的字体类(.title-1、.heading、.caption 等),相对于桌面的界面字体 |
界面字体的字号 |
| Web | Asuka 自己的字号体系,使用系统字体(system-ui) |
16 |
在 Windows 上,每个样式取字号体系中最接近的一档:fonts.LargeTitle 对应 Title Large(40),fonts.Title 对应 Title(28),fonts.Title2 对应 Subtitle(20),fonts.Headline 对应 Body Strong(14,semibold),fonts.Footnote 和 fonts.Caption 对应 Caption(12)。在 Linux 上,以 GNOME 的 11 pt 界面字体为例,fonts.LargeTitle 对应 .title-1(181%,heavy),fonts.Title 对应 .title-2,fonts.Headline 对应 .heading(bold),fonts.Caption 对应 .caption(82%)。
哪些字体会随用户设置的文字大小缩放:
- iOS(动态字体,Dynamic Type)和 Android(字体缩放):所有字体,包括语义字体、固定字号字体和自定义字体。
- Windows:所有字体,取决于“设置 > 辅助功能 > 文本大小”。
- Linux:所有字体,取决于桌面的文字缩放(GNOME 的“大号文本”、GTK 的字体 DPI)。各样式所参照的界面字体同样来自桌面设置(GTK 的
gtk-font-name)。 - Web:所有字体,取决于浏览器的文字大小,因为字体都以
rem为单位。 - macOS 没有文字大小的设置,字体保持不变。
ui.UseEnvironment() 的 FontScale 就是这个缩放值,默认为 1(见无障碍)。
自定义字体
把字体文件(.ttf、.otf)放进应用的 assets/fonts 目录。通过字体的 Family,或者 fonts.Custom(family, font),来选择要用的字体:
- 文件名:
assets/fonts/Inter-Bold.ttf对应"Inter-Bold"; - 一组文件构成的字体族:
"Inter"会根据字重选用Inter-Regular、Inter-Bold等文件; - 通用字体族:
"serif"、"monospace"、"rounded"。在 iOS 和 macOS 上,它们是系统字体的几种设计(New York、SF Mono、SF Rounded);在 Windows 上是 Georgia 和 Cascadia Mono;在 Linux 和 Android 上是系统的衬线和等宽字体族;在 Web 上是ui-serif、ui-monospace和ui-rounded(前提是浏览器支持)。Android、Windows 和 Linux 没有圆体,会显示系统字体; - 系统自带的字体族,例如
"Georgia"。
自定义字体会和它所基于的文本样式一样缩放。平台上没有的字体会用系统字体显示。所有平台都只为当前应用加载 assets/fonts 里的文件,不会把它们安装给其他应用使用。
ui.StaticText("Inter Bold", ui.Style{Font: fonts.Custom("Inter-Bold", fonts.Title)})
ui.StaticText("Inter", ui.Style{Font: fonts.Custom("Inter", fonts.Body).Weighted(fonts.Semibold)})
ui.StaticText("Code", ui.Style{Font: fonts.Custom("monospace", fonts.Footnote)})深色模式
语义颜色会自动随深色模式切换,使用它们的应用不需要为此写任何代码。固定颜色则保持不变;如果想为不同模式分别指定颜色,可以在 StyleFunc 里读取环境中的 Dark:
env := ui.UseEnvironment()
ui.View(ui.ViewProps{Style: ui.StyleFunc(func() ui.Style {
s := ui.Style{Height: ui.Pt(80), CornerRadius: 12, Background: colors.RGB(255, 255, 255)}
if env.Get().Dark {
s.Background = colors.RGB(28, 28, 30)
}
return s
})})会变化的样式
ui.StyleFunc 返回一个样式,当它读取过的 signal 变化时会重新运行。新样式会与上一次的样式比较:只有布局属性变了才会重新布局,而且只有发生变化的视觉属性才会发送给视图。
ui.View(ui.ViewProps{
OnPress: func() { selected.Set(!selected.Peek()) },
Style: ui.StyleFunc(func() ui.Style {
s := ui.Style{Padding: ui.All(12), CornerRadius: 10, Background: colors.SecondaryBackground}
if selected.Get() {
s.Background = colors.Accent
s.BorderWidth = 2
s.BorderColor = colors.Label
}
return s
}),
}, ui.StaticText("Tap me"))把这类改动放进 ui.Animate 里,它就会以动画呈现(见动效)。
强调色
colors.Accent 以及用到它的控件,都从 asuka.json 读取应用的强调色:
{ "accent": "#7c3aed" }在 iOS 上它是着色(tint);在 Android 上它是 Material 3 配色方案的种子色(Android 12 及以上)。设置 "android": {"dynamicColor": true} 后,只要系统支持,Android 就会像其他 Material 应用一样,从用户的壁纸中取色。在 macOS、Windows 和 Linux 上,它会在 Asuka 的控件和颜色中替换系统的强调色;在 Web 上,它同时也是控件的 accent-color。
如果没有设置强调色,就使用平台自己的。在 macOS 和 Windows 上,这是用户自己选的强调色(macOS 的“系统设置 > 外观”;Windows 的“设置 > 个性化 > 颜色”);在 Linux 上是 libadwaita 的蓝色;在 Web 上是 Asuka 自带的一种蓝色。参见发布。
浅色与深色
应用会跟随系统的外观:iOS 和 macOS 的深色模式、Android 的深色主题、Windows 的应用模式(“设置 > 个性化 > 颜色”)、GNOME 的深色样式,以及浏览器的 prefers-color-scheme。ui.SetColorScheme 可以无视系统设置,强制以浅色或深色显示,就像应用内置的外观设置那样(对应 SwiftUI 的 preferredColorScheme;Android 上 AppCompat 的夜间模式;macOS 上应用的 NSAppearance;Linux 上 libadwaita 的 style manager);传入 ui.ColorSchemeSystem 则恢复跟随系统。语义颜色、平台控件和系统栏都会随之切换,无需重建应用;当前是哪种模式,可以看 ui.UseEnvironment() 的 Dark。用 store 保存用户的选择,并在应用启动时设置:
var scheme = store.Signal("colorScheme", int(ui.ColorSchemeSystem))
func App() ui.Node {
ui.SetColorScheme(ui.ColorScheme(scheme.Peek()))
// ...
}