Skip to content

文本输入(Text input) ​

ui.TextInput 创建一个编辑 *string 的单行文本输入框,ui.TextArea 则创建多行输入框。它们会随着用户输入而改变字符串:你可以直接读取它,用 Changed 询问随之而来的工作,单行输入框中的回车则由 Submitted 报告。

go
if ui.TextInput(c, &app.query).Placeholder("Search").Label("Search").Changed() {
	app.results = search(app.query)
}

if ui.TextInput(c, &app.name).AutoFocus().Submitted() {
	app.rename()
}

ui.TextArea(c, &app.notes).Height(160)
  • Placeholder 在输入框为空时显示一段提示文字:在 TextInput 中显示为一行,在文本区域中会自动换行。对于包裹输入框的字段,如 SearchField、TokenField 或 Combobox,它显示在其中的输入框里。

  • Password 会隐藏其内容,并在调用它的帧中阻止其进入剪贴板和输入法:一个用于显示明文的眼睛按钮会停止调用它。只有 TextInput 支持它:与各平台一样,文本区域没有密码模式。

  • AutoFocus 在它出现时(如对话框的第一个字段)赋予它键盘焦点。

  • ReadOnly(true) 显示文本但不允许用户修改:它仍可获得焦点,但没有插入符,文本可以被选中和复制。

  • Disabled(true) 将其置灰。

  • OnPaste(fn) 在粘贴内容(Cmd+V 或 Ctrl+V,"编辑"菜单或上下文菜单中的"粘贴")插入之前看到它,并带上剪贴板的文本;当剪贴板没有文本(例如是图片)时该文本为空。返回 true 会接管这次粘贴,粘贴因此不改变任何内容,就像网页上的 preventDefault:

    go
    ui.TextArea(c, &app.draft).Lines(1, 8).OnPaste(func(text string) bool {
    	if len(text) > 10_000 {
    		app.attach(text) // 大块文本变为附件
    		return true
    	}
    	if text == "" {
    		if png := mygo.Clipboard.ReadImage(); png != nil {
    			app.attachImage(png)
    			return true
    		}
    	}
    	return false
    })
  • OnTextKey(fn) 在输入框接收某个不产生文本的按键之前看到它:Enter、Tab、Escape、Backspace 和 Delete、方向键以及其他移动插入符的键,还有带 Cmd 或 Ctrl 的组合键——就像 AppKit 的 doCommandBySelector 和网页的 keydown 那样。fn 在按键到来时运行,先于其之前键入的按键和文本处理完毕之后;返回 true 会接管该键:文本区域不会插入换行,TextInput 不会提交,Tab 不会移动焦点。未被接管的按键会像没有 fn 时一样继续传递。焦点周围的 Shortcut 要处理的按键会先交给它,如同 HandleInput 的行为,除非输入框用这些键进行编辑(如 Enter 和方向键)。fn 可能在帧之间运行,因此它应通过其 ui.TextKey 来读取和替换文本,而不是保留自视图的 Element(fn 返回后使用它们会 panic)。例如聊天输入框用 Enter 发送、用 Shift+Enter 换行、用 Up 回填上一条消息:

    go
    ui.TextArea(c, &app.draft).Lines(1, 8).OnTextKey(func(k ui.TextKey) bool {
    	switch {
    	case k.Key == ui.KeyEnter && k.Mods == 0:
    		app.send(k.Text())
    		k.SetText("")
    		return true
    	case k.Key == ui.KeyUp && k.Mods == 0 && k.Text() == "":
    		k.SetText(app.lastSent())
    		return true
    	}
    	return false // Shift+Enter 换行
    })

当 TextInput 的宽度不足以显示其文本时,它在拥有焦点会保持插入符在视野内,没有焦点则显示文本的开头。

TextRanges 在调用它的帧中为文本的各个片段着色并设置其字重,例如消息输入框高亮所输入的提及(mention):

go
in := ui.TextAreaBase(c, &app.draft).Lines(1, 8)
for _, m := range mentions(app.draft) {
	in.TextRanges(ui.TextRange{Start: m.start, End: m.end, Color: m.color, Weight: 600})
}

这些范围以文本的 rune 计数,每帧在其中定位。它们可以重叠和嵌套,就像富字符串(NSAttributedString)的属性那样:范围在其 Color 的 alpha 不为零时设置颜色,在 Weight 不为零时设置字重;当范围重叠时,对这两个属性而言,生效的都是本帧各次调用中最后一个设置它的范围。一个加粗范围内包含一个带色提及,该提及就会既加粗又带色:

go
in.TextRanges(
	ui.TextRange{Start: 0, End: 40, Weight: 700},
	ui.TextRange{Start: 10, End: 16, Color: t.Accent},
)

密码输入框不显示任何范围样式。输入法在组合期间,其文本以无样式状态位于插入符处,各范围在其周围保持自身样式。

文本区域至少有几行高,并随文本增长;给定高度后,它在其中滚动(通过滚轮和滚动条),并在插入符移动时保持其可见。Lines(min, max) 让它的高度等于文本按其宽度换行所需的高度,从 min 行到 max 行,超出后开始滚动,就像消息输入框随输入内容增长:

go
ui.TextArea(c, &app.draft).Lines(1, 8)

文本区域只布局视野内的段落,并保留其布局直到发生变化,因此它可以容纳数十万行的文本(如日志或源文件),输入速度依然很快。

编辑 ​

文本输入框的编辑方式与平台的文本字段一致:用指针选择(双击选中一个单词,三击选中一行),配合 Shift 和方向键选择,以及用平台的按键按单词和行选择;撤销与重做;剪切、复制和粘贴(也可通过"编辑"菜单的角色触发);以及包含编辑命令的上下文菜单。它们接受用输入法组合的文本,输入法能看到插入符周围的文本,因此长按、日文转换和预测输入都能像在其他应用中一样工作。输入法组合期间键入的按键归输入法所有,例如 Enter 选择候选词、Escape 放弃组合:这些键不会提交任何内容、不会触发快捷键、也不会关闭对话框。Composing 报告是否正在组合,此时其文本尚未进入字符串。

输入框在帧与帧之间保留正在编辑的文本、选区和撤销历史;从别处设置字符串会替换文本。

插入符 ​

TextSelection 以文本内 rune 偏移的形式返回选区,两者相等处即为插入符;SetTextSelection 移动它。例如应用补全正在输入的单词后,把插入符放到其后:

go
input := ui.TextArea(c, &app.draft)
if start, _ := input.TextSelection(); app.completed != "" {
	app.draft, start = complete(app.draft, start, app.completed)
	input.SetTextSelection(start, start)
}

TextSelection 按顺序返回选区。TextAnchorCaret 则从选区起点返回到插入符,应用因此可以区分向后拖出的选区和向前拖出的选区:编辑器自身扩展选区的按键,例如 Shift-Home 移到行首,会从锚点(anchor)开始扩展:

go
anchor, caret := input.TextAnchorCaret()
input.SetTextSelection(anchor, lineStart(app.draft, caret))

行侧 ​

TrackLines 把文本区域布局其文本各行的位置记录到一个 ui.TextLines 中,供在其旁边绘制的应用使用:比如给每行的第一行(row)标上行号,或在一行的片段上做标记。一行(line)指两个换行符之间的文本,当区域对其换行时会占据多个 row。Top(i) 是第 i 行在区域内容中的顶部位置(在其 padding 之下、滚动之前),At(y) 返回某高度处的行,Rows(i) 返回第 i 行每个 row 起始的 rune,Height 是文本的高度。配合区域的 ScrollState,侧栏(gutter)可以为视野内的行编号:

go
const pad = 8 // 文本区域的顶部 padding
ui.Row(c).Grow(1).Children(func() {
	ui.Box(c).Width(48).FillHeight().Draw(func(p *ui.Painter, r ui.Rect) {
		top := r.Y + pad - app.scroll.Y // 区域内容的顶部
		p.Clip(r, 0, func() {
			for i := app.lines.At(app.scroll.Y); i < app.lines.Count() && top+app.lines.Top(i) < r.Y+r.H; i++ {
				p.Text(r.X+8, top+app.lines.Top(i), strconv.Itoa(i+1), 12, p.Theme().TextMuted)
			}
		})
	})
	ui.TextArea(c, &app.source).Padding(pad, 12).Grow(1).FillHeight().
		TrackLines(&app.lines).TrackScroll(&app.scroll)
})

在区域布局某些行之前,其高度只是估计值,其滚动条也是如此,直到它们真正显示。在 Draw 中,行是当前帧的;在构建视图时,则是上一帧的。

错误 ​

在字段中,Error 将值标记为无效:输入框用主题的 Danger 颜色绘制边框,辅助技术在朗读时一并读出该消息。

go
ui.Field(c, "Email", func() {
	ui.TextInput(c, &app.email)
}).Error(app.emailError)

无外观 ​

ui.TextInputBase 和 ui.TextAreaBase 是没有 padding、背景、边框和圆角的文本输入框,供你设计自己的输入控件:参见自定义控件。

无障碍 ​

辅助技术将其视为一个文本字段或文本区域,由 Label 或其字段命名,其值就是文本,连同插入符和选区;它可以像键入一样编辑,只读时除外。密码字段会隐藏其值。