声明式布局
用行、列、内边距和伸缩来描述页面,而不是算坐标;由 flex 包完成布局。
手工切矩形(见“绘制与布局”)是 Viva 最底层的做法,适合自定义控件。对于由普通部件组成的页面(页眉、表单、列表行、页脚),它要做很多算术,标签变长时算术还会出错。flex 包按 CSS flexbox 的方式,根据描述来布局页面。
view.Draw(canvas, theme, bounds, func(f *flex.Frame) {
f.Column().Pad(24).Gap(12).Children(func() {
f.Text("标题").FontSize(18).Bold()
f.Row().Gap(8).Justify(flex.End).Children(func() {
f.Button(cancel, "取消").Outline()
if f.Button(save, "保存").Clicked() {
model.save()
}
})
})
})
模型
Row从左到右排列子元素,Column从上到下,Stack相互叠放。- 子元素默认和内容一样大,或者按
Width、Height、Size指定。FillWidth、FillHeight、Fill占满容器的交叉方向,或者平分沿着容器方向剩下的空间。 Grow(n)把容器方向的剩余空间按比例分给会伸缩的子元素。Spacer()是会伸缩的空元素。Pad和Margin接收一个数字,更多情况用PadX、PadY、PadSides。Gap是子元素之间的间距。- 交叉方向上子元素默认拉伸;
Items(flex.Center)居中,Self只对一个子元素生效。沿容器方向用Justify(flex.Center)、flex.End、flex.Between摆放剩余空间。Center()两个方向都居中。 Basis(px)是在分配剩余空间之前元素的起始尺寸,WidthPercent、HeightPercent取容器的一个比例。MinWidth、MaxWidth、MinHeight、MaxHeight限定尺寸;被上限卡住的元素吃不下的空间会分给其他元素(和 CSS 一样),所以Grow(1).MaxWidth(470)旁边放一个Spacer(),后面的元素仍然贴着远端。Items(flex.Baseline)让一行里的子元素按第一行文字的基线对齐,大标题、小说明和按钮可以落在同一条基线上。Text会先于固定尺寸的兄弟元素收缩并裁剪。Background、Radius、Border、Shadow、Clip为容器设置样式。Paragraph(text, maxLines)是会按所得宽度换行的文字,所以标签变长会让这一行变高,并把后面的内容往下推。LineHeight设置行高。Grid(minCell)按宽度放下尽可能多的等宽列,并换到新的行。Scroll(offset)是比自己更高的、带裁剪的列。偏移量由你持有(滚轮事件到Config.OnScroll);End之后Content()是子元素的总高度,可据此限制偏移量。
控件就是 components 里的那些,外观完全一致:Button(以及 Outline、Ghost、Secondary、Destructive、Small 等变体)、Checkbox、Switch、Icon、Divider。Clicked() 在声明控件的地方读取:它只在递送这次点击的那一遍里为真。任何元素都可以用 Hit(id, label) 成为指针目标,HoverBackground 在指针悬停时绘制底色。
和手工绘制混用
Slot() 为你自己绘制的内容预留一块矩形,Box.Rect() 告诉你它最后落在哪里。需要在绘制前拿到矩形时,用 view.Begin(...) 和 f.End() 代替 Draw:先声明,调用 End,再往槽位里绘制。自定义控件就是这样放进已布局的页面里的。
开销
声明、测量、排布并绘制一个约六十个元素的页面大约需要 8 微秒,View 见过最大页面之后不再分配内存。为每个每帧都绘制的页面在你的状态里保存一个 View;零值即可使用。传给 Children 的函数立即调用且不会被保存,所以也没有开销。
单行 Text 不换行:放不下的行会被裁剪,并且先于固定尺寸的兄弟元素收缩。需要换行的文字用 Paragraph。
创建项目
viva init my-app 会写出一个能直接运行的项目:用 flex 布局的窗口、无需窗口即可驱动它的测试、viva.toml,以及 AGENTS.md,一页纸的布局、标识、点击和测试指南,供编码助手在修改界面前阅读。之后 viva dev . 以热重载方式运行。
package main
import (
"log"
"github.com/vivaui/viva/app"
"github.com/vivaui/viva/flex"
"github.com/vivaui/viva/ui"
)
var (
tree = ui.Tree{}
page = tree.Feature("layout")
save = tree.ID(page, "save")
cancel = tree.ID(page, "cancel")
)
type model struct {
saved bool
notes bool
view flex.View
}
func newConfig(m *model) app.Config {
theme := ui.LightTheme()
config := app.Config{Tree: &tree, Title: "Layout", Width: 420, Height: 220}
config.Paint = func(canvas *ui.Canvas, size ui.Size) {
canvas.Fill(ui.Rect{Width: size.Width, Height: size.Height}, theme.Background)
bounds := ui.Rect{Width: size.Width, Height: size.Height}
m.view.Draw(canvas, theme, bounds, func(f *flex.Frame) {
f.Column().Fill().Pad(24).Gap(12).Children(func() {
f.Text("Deploy settings").FontSize(18).Bold()
f.Text("Where the build goes when it passes.").Muted()
f.Divider()
f.Row().Items(flex.Center).Gap(8).Children(func() {
f.Text("Notify the team")
f.Spacer()
f.Switch(tree.ID(page, "notes"), m.notes).Label("Notify the team")
})
f.Spacer()
f.Row().Gap(8).Justify(flex.End).Children(func() {
f.Button(cancel, "Cancel").Outline()
if f.Button(save, "Save").Clicked() {
m.saved = true
}
})
})
})
}
return config
}
func main() {
m := &model{}
var canvas ui.Canvas
if err := app.RunCanvas(newConfig(m), &canvas); err != nil {
log.Fatal(err)
}
}