Skip to content

输入 ​

元素会报告用户自上一帧以来对它做了什么,就在你构建它的时候:询问即可,并在元素构建处处理。

每个 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)
	}
})

另见 ​