MyGo
Text input

Text input

Text input

Text on one line or several, with selection, undo, the clipboard and input methods.

Text input

ui.TextInput creates a single-line text input editing a *string, and ui.TextArea a multi-line one. They change the string as the user types: read it, or ask Changed for work that follows, and Submitted for Enter in a single-line input.

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 shows a text while the input is empty, on one line in a TextInput, wrapped in a text area. On a field around an input, as a SearchField, TokenField or Combobox, it shows in that input. PlaceholderColor paints it in another color than the theme's TextMuted, as a warning hint in a form.

  • CaretWidth paints the caret wider than one DIP, as a terminal's block or a large-print theme's, or hides it with zero; it changes nothing else.

  • Password hides what it holds, and keeps it from the clipboard and input methods, in the frames that call it: an eye button that shows the text stops calling it. Only a TextInput takes it: as on every platform, a text area has no password mode.

  • AutoFocus gives it the keyboard focus as it appears, as the first field of a dialog.

  • ReadOnly(true) shows the text without letting the user change it: it still takes the focus, without a caret, and its text can be selected and copied.

  • MaxLength(n) keeps the user from making the text longer than n, counted as HTML's maxlength counts it, in UTF-16 code units: what is typed, pasted or committed by an input method is cut to what fits. A value the app sets is not cut, and n <= 0 means no limit.

  • Disabled(true) grays it out.

  • OnPaste(fn) sees a paste (Cmd+V or Ctrl+V, the Edit menu's or the context menu's Paste) before it is inserted, with the clipboard's text, empty when it holds none, as with an image. Returning true takes the paste, which then changes nothing, as preventDefault does on the web:

    ui.TextArea(c, &app.draft).Lines(1, 8).OnPaste(func(text string) bool {
    	if len(text) > 10_000 {
    		app.attach(text) // a large block becomes an attachment
    		return true
    	}
    	if text == "" {
    		if png := mygo.Clipboard.ReadImage(); png != nil {
    			app.attachImage(png)
    			return true
    		}
    	}
    	return false
    })
  • OnTextKey(fn) sees a key that types no text before the input takes it: Enter, Tab, Escape, Backspace and Delete, the arrows and the other keys that move the caret, and chords with Cmd or Ctrl, as AppKit's doCommandBySelector and a web page's keydown do. fn runs as the key comes, after the keys and text typed before it, and returning true takes the key: a text area inserts no newline, a TextInput does not submit, Tab does not move the focus. A key left goes on as without fn. Keys that a Shortcut around the focus handles go to it first, as with HandleInput, unless the input edits with them, as with Enter and the arrows. fn may run between frames, so it reads the text and replaces it through its ui.TextKey rather than an Element kept from the view (they panic once fn returns), as a chat composer sends with Enter, breaks the line with Shift+Enter, and puts the last message back with Up:

    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 breaks the line
    })

A TextInput too narrow for its text keeps the caret in view while it has the focus, and shows the start of its text without it.

TextRanges colors runs of the text, and sets their weight, in the frames that call it, as a message field shows the mentions in what is typed:

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

The ranges are runes of the text, found in it each frame. They may overlap and nest, as the attributes of an attributed string (NSAttributedString) do: a range sets its Color when its alpha is not zero and its Weight when it is not zero, and where ranges overlap, each of the two is the last given range's that sets it, across the calls of the frame. A bold range with a colored mention inside it shows the mention bold and colored:

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

A password shows none. While an input method composes, its text sits unstyled at the caret and the ranges keep their style around it.

A text area is at least a few lines high and grows with its text; given a height, it scrolls within it, with the wheel and a scroll bar, and keeps the caret in view as it moves. Lines(min, max) makes it as high as its text wraps at its width, from min lines up to max, past which it scrolls, as a message field grows with what is typed:

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

A text area lays out only the paragraphs in view and keeps their layouts until they change, so that it holds texts of hundreds of thousands of lines, as a log or a source file, and stays as quick to type in.

Editing

Text inputs edit as the platform's text fields do: selection with the pointer (a double click selects a word, a triple click a line), with Shift and the arrows, and by words and lines with the platform's keys; undo and redo; cut, copy and paste, also from the Edit menu's roles; and a context menu of the editing commands. They take text composed with input methods, which see the text around the caret, so that press and hold, Japanese conversion and predictions work as in other apps. The keys typed while an input method composes are its own, as Enter choosing a candidate or Escape giving the composition up: they submit nothing, press no shortcut and close no dialog. Composing reports whether it composes, its text not yet in the string.

The input keeps the text being edited, its selection and its undo history from frame to frame; setting the string from elsewhere replaces the text. It preserves the last undo step, so an app can reformat what the user typed without losing that step. Setting the string to "" does not discard the history: Undo can restore text from before the clear.

To put a text of the app's in place of some or all of the user's, call ReplaceText(start, end, s) on the input instead. It replaces the runes from start to end, as one step of undo of its own, so Undo brings back what the user had written and Redo the app's text again. The offsets are kept within the text, so 0, math.MaxInt replaces all of it, even text typed in the same frame:

msg := ui.TextArea(c, &app.message).Label("Message")
if text, ok := app.generated(); ok {
	msg.ReplaceText(0, math.MaxInt, text) // Undo restores what the user wrote
}

in := ui.TextInput(c, &app.draft)
if name, ok := app.mentionPicked(); ok {
	_, caret := in.TextSelection()
	in.ReplaceText(app.mentionStart, caret, "@"+name+" ")
}

ReplaceText puts the caret after the text and reports the edit through Changed, as typing does. A single-line input takes line breaks as spaces, and MaxLength does not cut it. It works on TextInputBuffer and TextAreaBuffer too.

To start a new editing session, clear the string and change the input's key. The new identity gets a new editor, without the old selection, composition or undo history:

// Keep secret and secretSession in app state.
ui.TextInput(c.Key(app.secretSession), &app.secret).Password().Label("Secret")

if ui.Button(c, "Cancel").Clicked() {
	app.secret = ""
	app.secretSession++ // the next build creates a fresh editor
}

For indexed storage, ui.TextInputBuffer and ui.TextAreaBuffer edit a *ui.TextBuffer. Replacing its contents externally with buffer.Set("") discards the control's undo and redo history and composition when it next synchronizes with the buffer; its selection is clamped to the new text. Neither changing a key nor replacing a buffer guarantees that old text is erased from memory; existing TextSnapshot values retain their contents.

A single-line input replaces line breaks in pasted text with spaces. Use OnPaste to reject a multiline value before it is changed or inserted:

ui.TextInput(c, &app.secret).Password().OnPaste(func(text string) bool {
	return strings.ContainsAny(text, "\r\n") // import "strings"
})

The caret

TextSelection returns the selection as offsets in runes into the text, the caret where they are equal, and SetTextSelection moves it, as an app completing the word being typed puts the caret after it:

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 returns the selection in order. TextAnchorCaret returns it from where it was started to its caret, so an app can tell a selection made backwards from one made forwards: an editor's own keys that extend a selection, such as Shift-Home moving to the first character of the line, extend it from its anchor:

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

Beside the lines

TrackLines keeps in a ui.TextLines where a text area lays out the lines of its text, for an app that paints beside them: numbers by the first row of each line, or marks on runs of a line. A line is the text between newlines, on several rows where the area wraps it. Top(i) is the top of line i in the area's content, below its padding and before it scrolls, At(y) the line at a height, Rows(i) the runes where each row of line i starts, and Height the height of the text. With the area's ScrollState, a gutter numbers the lines in view:

const pad = 8 // the text area's top 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 // the top of the area's content
		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)
})

The heights of the lines the area has not laid out are estimates, as its scroll bar's are, until it shows them. In Draw the lines are those of the frame; while the view is built, those of the last one.

Errors

In a field, Error marks the value invalid: the input draws its border in the theme's Danger color, and assistive technology reads the message with it.

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

Without a look

ui.TextInputBase and ui.TextAreaBase are text inputs without padding, background, border or corners, for inputs of your own design: see custom widgets.

Accessibility

Assistive technology sees a text field, or a text area, named by its Label or its field, whose value is its text, with the caret and the selection; it edits it as typing does, unless it is read-only. A password field hides its value.