外观
输入
元素会报告用户自上一帧以来对它做了什么,就在你构建它的时候:询问即可,并在元素构建处处理。
每个 ui.Element 值只在一次构建过程中有效。其零值表示不存在,对零值查询返回 false 或零值。可选控件用 Valid 检查。过期的使用在开发模式和 Tester 中会被诊断出来;生产环境中相应的方法返回空结果或忽略该操作。
跨构建保持焦点或快捷键时,存储 ui.Handle。每个窗口为该句柄各有自己的绑定;查询时使用该窗口的 Context。
go
card := ui.Column(c).Padding(12).Radius(8).Focusable()
if card.Hovered() {
card.Background(t.SurfaceHover)
}
if card.DoubleClicked() {
app.open(item)
}指针
Hovered、Pressed、Clicked、DoubleClicked、RightClicked、Dragged(按住元素期间指针自上一帧以来移动的距离)和 PointerPosition。ClickModifiers 返回上次点击时按住的修饰键,例如 Shift 点击时的 Shift。c.Modifiers() 返回当前按住的修饰键;某个修饰键单独按下或松开时也会触发,视图随其变化重绘:例如在按住 Cmd 时显示每行 Cmd+1–9 的列表就能读取它。PassThrough 让指针穿透到下方元素。
接收指针的元素把事件交给其下最内层的元素:可点击行内的按钮自己接收它的点击。
指针在元素内按住期间,Pressed 保持为 true。观察到按下开始时会在绘制前重建视图,因此用 if row.Pressed() || row.Clicked() 选中的行会在该帧显示新的选中和焦点颜色。注意防止重复选中同一项。Clicked 报告在元素内释放时的激活,或通过键盘或辅助技术触发的激活;按钮使用的就是这种激活。
当指针按住某元素时,按下开始时指针所经过的元素(例如按钮所在的行)只要指针仍在它们上方就保持 Hovered,如同 CSS,其他元素不再进入悬停状态:拖过其他元素不会点亮它们。在其行悬停时出现的按钮,在按下时仍保持显示,以便接收点击:
go
row := ui.Row(c).Padding(8)
hovered := row.Hovered()
row.Children(func() {
ui.Text(c, item.Title).Grow(1)
if hovered && ui.Button(c, "Remove").Clicked() {
removed = item.ID
}
})键盘焦点
Focusable 元素在被点击时获得焦点,Tab 和 Shift+Tab 在它们之间移动焦点,通过键盘移动时显示焦点环。AutoFocus 在元素出现时给予焦点,例如对话框的第一个输入框;Focus 在你调用它期间保持焦点;Focused、FocusVisible 和 FocusWithin 报告焦点状态。Enter 和空格键会按下获得焦点的按钮或链接,空格键会切换获得焦点的复选框、开关、切换按钮或单选按钮。
持久焦点
把应用自有的句柄在每次构建时绑定到控件:
go
ui.TextInput(c.Key("search"), &app.query).Bind(&app.search)在你的应用状态中声明 search ui.Handle。app.search.Focus() 请求焦点,若控件隐藏则等待。CancelFocus() 取消请求。Focused(c) 和 FocusWithin(c) 可在控件构建之前或之后读取其标识。c.Resolve(app.search) 只返回本次构建过程中的元素。app.search.Bounds(c) 从已提交的帧读取其盒子,包括在输入回调中构建之间的时刻。关闭窗口会取消其焦点请求。
一个句柄可以在多个窗口中绑定。查询需要传入该窗口的 Context;用 Focus(c) 或 CancelFocus(c) 显式选择窗口。无参的焦点形式要求至多只有一个有效绑定。
绑定到数据的焦点
go
type pane int
const (none pane = iota; files; diff)
ui.Column(c.Key("files")).FocusBind(&app.pane, files).Children(func() { app.filesView(c) })
ui.Column(c.Key("diff")).FocusBind(&app.pane, diff).Children(func() { app.diffView(c) })
// 在某个动作中:
app.pane = diff
// 本窗口的实际焦点:
focused := ui.FocusedValue(c, &app.pane)FocusBind 接收指向可比较字段的指针和一个匹配值。每个值标识窗口中的一个控件;零值保留给"无焦点"。字段保存期望的焦点。若其控件隐藏,请求会等待,因此在等待期间用 FocusedValue 读取实际焦点。赋零值会清除焦点。当没有请求在等待时,用户的焦点变化会更新该字段。不同窗口使用不同字段。
焦点组
工具栏、单选组、分段控件或标签列表中的控件在 Tab 序列中只占一站,与原生行为一致:Tab 把焦点移入组内,落到上次获得焦点的控件(或被选中的单选按钮或标签页),再移出组;方向键在组内移动焦点,Home 和 End 移到第一个和最后一个。FocusGroup 为你自己的容器实现这一点,方向键可为 ui.Horizontal、ui.Vertical 或两者;组内的滑块或文本输入保留它所接管的方向键。
go
ui.Row(c).Gap(4).FocusGroup(ui.Horizontal).Children(func() {
for _, tool := range tools {
if ui.Button(c, tool.Name).Clicked() {
app.tool = tool
}
}
})快捷键
c.Shortcut(ui.Cmd, ui.KeyS) 在窗口的任意位置报告恰好以那些修饰键组合按下的键;Element.Shortcut 只在元素或其内部元素拥有焦点时报告,且优先于前者。获得焦点的按钮或链接保留 Enter 和空格键,切换按钮保留空格键,因此 c.Shortcut(0, ui.KeyEnter) 无论焦点在何处都会按下对话框的默认按钮。
go
if c.Shortcut(ui.Cmd, ui.KeyS) {
app.save()
}ui.Cmd 在 macOS 上是 Command 键,在其他平台是 Ctrl。ui.KeyBack 和 ui.KeyForward 是鼠标的后退/前进键以及具备这些按键的键盘上的对应键,路由会接管它们。菜单的快捷键仍然有效,"编辑"菜单的角色(剪切、复制、粘贴、全选、撤销、重做)作用于获得焦点的文本输入。当焦点不接收这类命令时,例如没有可编辑内容的列表,该角色交给对应按键的快捷键,因此在 macOS 上 c.Shortcut(ui.Cmd, ui.KeyV) 也能收到粘贴——尽管菜单先于窗口接管 Command+V。
快捷键在不输入拉丁字母的键盘布局上同样有效:按住 Cmd、Ctrl、Alt 或 Super 时,某个键若键入美国键盘上不存在的字母(如俄语布局的 ы),则代表美国键盘上同一位置的按键,因此 Cmd+ы 即 ui.KeyS。不按这些修饰键时,它仍是文本。
获得焦点的文本输入优先接收该平台的编辑键:在 macOS 上是 Option 和 Command 配合方向键与退格键,以及 Control 配合 A、E、B、F、N、P、D、H、K,与其他 Mac 应用一致;在其他平台上,它放行 Alt 和方向键(用于后退与前进)以及功能键。
快捷键动作可以在绑定其控件之前声明:
go
app.filesView.OnShortcut(c, ui.Cmd, ui.KeyK, app.openSelected)
ui.List(c.Key("files"), &app.list, len(app.files)).Bind(&app.filesView).
Rows(func(row ui.ListRow) {
ui.Text(row.Context, app.files[row.Index].Name)
})Handle.OnShortcut 只在窗口为该句柄构建了启用的控件时,于构建后运行。隐藏的控件不接收命令。c.OnShortcut 在当前父作用域中声明一个动作;根 Context 处理焦点控件未接收的按键以及处于活动状态的覆盖层。元素焦点子树内的动作用 Element.OnShortcut。
输入法
文本输入接收通过输入法组合出的文本,输入法能看到光标周围的文本:macOS 的长按替换其带重音符的字母,日语输入法会重新转换已键入的文本,其他输入法则根据前文进行预测。
拖入的文件
DroppedFiles 返回从 Finder、资源管理器或文件管理器拖放到元素上的文件路径,FileDragOver 报告正被拖过其上的文件,用于显示它可以接收。窗口只在这类元素上方接收文件;当它有 OnFileDrop 监听器时,则在任意位置接收,监听器会收到没有任何元素接收的文件及其放置位置:
go
zone := ui.Column(c).Size(260, 80).Border(1, t.Border)
if files := zone.DroppedFiles(); files != nil {
app.files = files
}
if zone.FileDragOver() {
zone.Border(2, t.Accent)
}在窗口内部或窗口之间拖动的值,以及与其他应用交换的序列化数据,见拖放。
滚动与捏合
滚动容器自己就能用滚轮和触控板滚动。想拿它们做别的事的元素,例如缩放和平移 K 线的图表,用 HandleInput 获取它们:ui.InputScroll 按 DX 和 DY 个 DIP 滚动,正值向内容的下方和右方,X 和 Y 是相对元素的指针位置;Precise 区分按像素滚动的触控板与按格滚动的鼠标滚轮。ui.InputZoom 是触控板的捏合,发生在 X 和 Y 处,按 Scale 缩放:2 表示内容显示为原来的两倍大。macOS、Wayland 上的 Linux 和浏览器会上报捏合;Windows 将其作为按住 Ctrl 的滚动发送,因此在 Windows 上,按住 Ctrl 的触控板滚动同样以 ui.InputZoom 送达,而鼠标滚轮的格数仍是 ui.InputScroll,并在 Mods 中带 ui.Ctrl。在 Linux 上,触摸屏与触控板行为相同:单指拖动会带动其下方的内容滚动(Precise),双指以其间为中心捏合,一同移动时滚动,轻点即点击,手指静止按住半秒即右键。元素接管的事件,其周围的滚动容器不再滚动;元素放行(返回 false)的事件,则由它们处理。
go
ui.Box(c).Fill().HandleInput(func(ev ui.InputEvent) bool {
switch {
case ev.Kind == ui.InputZoom:
app.chart.zoom(ev.X, ev.Scale)
case ev.Kind == ui.InputScroll && !ev.Precise:
// 一格滚动量,在不同平台滚动的距离不同,按四分之一缩放。
if ev.DY < 0 {
app.chart.zoom(ev.X, 1.25)
} else if ev.DY > 0 {
app.chart.zoom(ev.X, 0.8)
}
case ev.Kind == ui.InputScroll:
app.chart.pan(ev.DX)
default:
return false
}
return true
})Tester 用 Scroll(触控板)、Wheel(一格)和 Pinch(捏合)来滚动。
每个按键,随到随收
自行接管每个按键的控件,如终端,用 HandleInput 在下一帧之前即时获取输入:按下与释放的按键、持有焦点期间键入和组合的文本、菜单的编辑命令,以及在其上按下的指针、在其上移动或滚动。InputEvent.PhysicalKey 按键在美国键盘上的位置标识按键,无论布局键入什么,适用于按位置取键的场景,如游戏的 WASD。回调函数报告是否接管了事件;未接管的事件照常交给快捷键、Tab、上下文菜单和滚动容器,而窗口或焦点周围元素用 Shortcut 处理的按键会优先去那里。TextCaret 在此类元素持有焦点期间为其开启系统输入法,在其给出的光标处组合文本;InputMethod(false) 再次关闭输入法,但按键仍键入文本,例如 Vim 的正常模式——浏览器中做不到这一点。c.ReadClipboard、c.WriteClipboard 和 c.OpenURL 为它们复制、粘贴和打开链接;c.OpenURLThen 同样打开链接,其回调稍后得到结果,即系统打开它的情况,当没有应用能打开时带有错误,视图随即重建一帧。Services.ReadClipboardData 和 WriteClipboardData 一次读写多种格式,例如富文本编辑器同时复制自有格式、HTML 和纯文本;ClipboardFormats 无需读取即列出剪贴板的格式,供终端决定粘贴读取什么。应用自有的文本字段在屏幕阅读器移动其选区时收到 InputSelect:即其 Access 所描述文本中从 Start 到 End 的 rune。
go
ui.Box(c).Fill().Focusable().HandleInput(func(ev ui.InputEvent) bool {
switch ev.Kind {
case ui.InputKeyDown:
return app.key(ev.Mods, ev.Key)
case ui.InputText:
app.insert(ev.Text)
return true
}
return false
}).TextCaret(app.caretRect())
c.OpenURLThen(url, func(err error) {
if err != nil {
c.Toast("No app opens " + url)
}
})另见
- 上下文菜单和菜单按钮。
- 工具提示。
- 自定义标题栏:使用原生 UI 的窗口。