Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Text Input

The TextInput widget provides single-line text editing with support for selection, clipboard operations and undo/redo. Its sibling, password_input, is the same field for a password: masked, and holding the text where it cannot be copied, swapped or dumped.

Basic Usage

extern crate guido;
use guido::prelude::*;
fn main() {
let username = create_signal(String::new());

text_input(username)
;
}

TextInput uses two-way binding with signals:

  • When the user types, the signal is automatically updated
  • When the signal changes programmatically, the input reflects the new value

No manual synchronization is needed - just pass a Signal<String> and the binding works automatically.

Styling

Text Color

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
container().child(text_input(value).color(Color::WHITE))
;
}

Cursor Color

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
text_input(value).cursor_color(Color::rgb(0.4, 0.8, 1.0))
;
}

Selection Color

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
text_input(value).selection_color(Color::rgba(0.4, 0.6, 1.0, 0.4))
;
}

Font Size

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
container().child(text_input(value).font_size(16.0))
;
}

Font Family

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
// Predefined families
container().child(text_input(value).font_family(FontFamily::Monospace));

// Shorthand for monospace
container().child(text_input(value));

// Custom font
container().child(text_input(value).font_family(FontFamily::name("JetBrains Mono")))
;
}

Font Weight

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
// Using constants
container().child(text_input(value).font_weight(FontWeight::BOLD));

// Shorthand for bold
container().child(text_input(value))
;
}

Italic and OpenType Features

As on a text: italic(), font_style and font_features. A field of numbers with tabular figures keeps its caret between digits of one width:

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
container().child(text_input(value).font_features(FontFeatures::new().enable(b"tnum")))
;
}

Letter Spacing

Logical pixels after every character, as on a text. The caret, the selection and a click all land where the spaced letters are, and a password field spaces the dots it draws:

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
let password = create_password();
container().child(text_input(value).letter_spacing(1.0));
container().child(password_input(password).letter_spacing(4.0))
;
}

Password Input

A password gets its own field, bound to a Password rather than a RwSignal<String>:

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();

password_input(password)
    .placeholder("Password")
    .on_submit(move || {
        // Moved out, and the field is empty again. Hand it to PAM;
        // dropping it wipes it.
        let secret: Secret = password.take();
        let _ = secret;
    })
;
}

A String is the wrong home for a password: every edit and every get of one leaves a copy on the heap that nothing wipes, its pages can be swapped to disk, and a crash writes it into a core dump. So the field keeps its text in a Secret — pages of its own, locked in memory, left out of core dumps, and zeroed whenever they are given back — and edits it there, in place. It keeps no undo history, since every entry would be a copy; nothing can be copied or cut out of it; and Ctrl+Left/Right goes to the edge rather than revealing where the words end, as a double click selects all of it rather than a word. Pasting in still works, for password managers.

Password is a signal in everything but the copy. Reads subscribe, so state that follows the field is an ordinary closure, and the application reaches the text whenever it needs to — a Sign In button, a form’s Save, a clear after an idle timeout — by borrowing it:

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();
let sign_in = |_: &str| {};
container()
    .enabled(move || !password.is_empty())
    .on_click(move || password.with(|secret| sign_in(secret.expose())))
;
password.set(Secret::from(String::from("from the keyring"))); // pre-fill; the String is wiped
password.clear();                                             // wipe in place
}

Submitting is the application’s to spell, and Enter only says it happened. take moves the secret out and empties the field. To keep the dots on screen while the password is checked, hand over a duplicate instead — a second Secret in pages of its own, as protected as the first — and clear the field when the answer arrives. Enter and a submit button can then share one function:

extern crate guido;
use guido::prelude::*;
fn check(_: Secret) {}
fn main() {
let password = create_password();
let submit = move || check(password.with_untracked(Secret::duplicate));

container()
    .child(password_input(password).on_submit(submit))
    .child(container().on_click(submit).child(text("→")))
;
}

There is no get: whatever the application copies out of expose() is the application’s to wipe.

By default, characters are masked with •. Customize the mask character:

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();
password_input(password).mask_char('*')
;
}

Showing the password

An eye button beside the field is a signal the field is told:

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();
let shown = create_signal(false);

container()
    .layout(Flex::row().spacing(8.0))
    .child(password_input(password).reveal(shown))
    .child(
        container()
            .on_click(move || shown.update(|s| *s = !*s))
            .child(text(move || if shown.get() { "Hide" } else { "Show" }.to_string())),
    )
;
}

Declared rather than rebuilt, so the caret, the selection and the focus stay where they were. Everything else stays as it was: nothing can be copied or cut out, and there is no undo.

The catch: a shown password is drawn like any other text, and drawing text copies it. Shaping makes temporary copies of the text and frees them without wiping them, and the measurement and render caches keep what they shaped. That is ordinary memory, outside what the field controls, so while the password is shown — and afterwards, until those caches let go of it — it can be found in the process’s memory the way any String can. The Secret itself is untouched, and the field’s own copy of what it drew is wiped when it changes. GTK 4’s peek icon makes the same trade. If the threat you are guarding against is a core dump or a memory read, leave reveal off.

Hardening the process

The field keeps the password out of places guido controls. Some copies are outside that, and a lock screen should know about them:

  • Each key press. smithay-client-toolkit, which guido reads the keyboard through, hands every key over as a one-character String and frees it unwiped. That leaves single characters, one per allocation — not the password, but pieces of it.
  • The clipboard. guido reads the clipboard only when something pastes, and wipes what it read. The password manager’s own copy is its business.
  • A revealed password, as above.

A lock screen can close the core-dump route to all of it with one call at startup, before the password is ever typed: a process that is not dumpable leaves no core dump for coredumpctl to read back, and cannot be attached to with ptrace by other processes of the same user.

// not compiled: uses the `libc` crate, which the book's samples do not link.
// SAFETY: a plain prctl with no pointers.
unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0) };

Do not reach for mlockall to keep the process out of swap. It counts against the locked-memory limit, which is usually a few megabytes, and a GPU-rendered application maps far more than that — with MCL_FUTURE, later allocations start failing. The Secret already locks the pages that hold the password, which is what swaylock does too; for the rest, swap is the system’s to encrypt.

Everything else here — placeholder, caret, pointer shape, colours, focus, readonly — is the same on both fields.

Initial Focus

A screen that exists to be typed into should not make the user click first — and on a surface with no pointer there may be nothing to click with:

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();
password_input(password)
    .autofocus()
;
}

The input takes the keyboard at its first layout, unless a widget on its own surface already holds focus. That condition is what makes it safe: an input appearing later never pulls focus off the one being typed into beside it, and two autofocusing inputs do not fight (the first laid out wins).

When the focus is on another surface, the input waits for the keyboard. A popup opened from a focused field has its field focused once the keyboard reaches the popup, and the field below has the focus back when the popup closes. The same view built once per output — a lock screen with two monitors — ends up with the field on the monitor holding the keyboard focused, and one built for a monitor plugged in mid-lock takes nothing from the password being typed.

The offer is made once. A relayout does not ask again, so a resize or a scale change cannot drag focus back from wherever the user has since put it.

Losing Focus

The surface losing the keyboard does not take the focus off the field. Switching to another window, or opening a popup that grabs the keyboard, leaves the field focused with no caret, and when the keyboard comes back the caret does too and the next key is typed where it was going. A surface remembers the widget it had focused when the focus moved to another surface — the field inside that popup, say — and has it back when the keyboard returns, if it is still there.

A press that no widget claimed takes the keyboard off the field. Clicking the surface behind it, or a decorative panel, or empty space in a row: nothing there wanted the press, so there is nothing left for the focus to belong to, and the caret stops.

A press that something did claim leaves the focus alone. A container with an on_click, on_mouse_down or on_mouse_up claims the left presses that land on it, so a toolbar button that acts on the field it sits beside does not blur it, and neither does the field’s own scrollbar. The other two buttons are claimed by the handlers that name them, on_right_click and on_middle_click: a container with only an on_click does not claim a right press, and a right press it did not claim is a press on nothing like any other.

A press inside the focused field is always the field’s, whichever button it was — and it keeps the keyboard without consuming the press, so a row wrapping the field still gets its own click or context menu.

The box drawn around a field is the third case, and it is the one worth knowing: a container that declares when_focused and currently holds that focus keeps that focus against a press inside itself. Clicking its padding, its border, the space beside the caret, is clicking the field it draws.

The press still travels. The box keeps the keyboard and consumes nothing, so a clickable ancestor wrapping the field is clicked as it would be anywhere else.

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
container()
    .padding(8.0)
    .border(1.0, Color::rgb(0.3, 0.3, 0.4))
    // Lights up for the focus, and keeps it: a click on the padding is a
    // click on the field. A ring outside the box, so the field does not move.
    .when_focused(|s| s.outline(2.0, Color::rgb(0.4, 0.8, 1.0), 2.0))
    .child(text_input(value))
;
}

The focus also leaves when the whole surface does — the compositor moving the keyboard elsewhere — and when the field is taken out of the tree.

Placeholder

What the field says while it is empty:

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();
password_input(password)
    .placeholder("Password")
;
}

It is a label, not a value: never masked, so a password field shows the word rather than eight bullets, and it stands in place of the text rather than beside it — there is nothing to overlap, because it is only drawn when the value is empty.

Reactive, so a prompt that changes changes the empty field with it:

extern crate guido;
use guido::prelude::*;
fn main() {
let answer = create_signal(String::new());
let prompt = create_signal(String::from("Type here"));
text_input(answer).placeholder(move || prompt.get())
;
}

The colour is the field’s own text colour at reduced alpha — a placeholder is the same text, quieter. Declare placeholder_color on the field itself: only a field draws a placeholder, so only a field says what colour it is.

extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let theme = Theme::default();
let value = create_signal(String::new());
container()
    .child(text_input(value).placeholder_color(theme.text_weak).color(theme.text).placeholder("Search"))
;
}

No Caret

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();
password_input(password)
    .no_caret()
;
}

Keeps the focus and everything it carries — click to position, drag to select, the keyboard — and draws no caret. For a field where the caret says nothing: a masked one, where every character looks the same and you are always at the end. swaylock draws none for that reason.

It is also the cheapest field there is. A blinking caret is the one thing a still screen redraws on its own, twice a second, for as long as it is focused; without one an idle surface asks the loop for nothing at all.

Pointer Shape

A field shows the I-beam under the pointer unless it says otherwise, and it takes the same cursor a container does — a value, a closure or a signal:

extern crate guido;
use guido::prelude::*;
fn main() {
let password = create_password();
password_input(password)
    .no_caret()
    .cursor(CursorIcon::Hidden)
;
}

A masked field with no caret has nothing a click could place, so on a lock screen whose root hides the pointer the I-beam would promise editing that is not there. The field is the innermost widget under the pointer, so a shape declared around it never reaches it: this is the one place to say it.

Callbacks

On Change

Called whenever the text content changes:

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
text_input(value)
    .on_change(|new_text| {
        println!("Text changed: {}", new_text);
    })
;
}

On Submit

Called when the user presses Enter:

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
text_input(value)
    .on_submit(|text| {
        println!("Submitted: {}", text);
    })
;
}

Keyboard Shortcuts

The TextInput widget supports standard text editing shortcuts:

ShortcutAction
Ctrl+ASelect all
Ctrl+CCopy selection (not in a password field)
Ctrl+XCut selection (not in a password field)
Ctrl+VPaste
Ctrl+ZUndo (not in a password field)
Ctrl+Shift+Z or Ctrl+YRedo (not in a password field)
Left/RightMove cursor
Ctrl+Left/RightMove to the start or the end of a word (to the edge in a password field)
Shift+Left/RightExtend selection
Home/EndMove to start/end
BackspaceDelete before cursor
DeleteDelete after cursor

A character here is what is drawn as one: a letter and its combining accent, or an emoji made of several code points, such as a flag. The arrows step over it and both deletes take all of it. A password field steps one code point at a time, because its mask draws one bullet for each.

Styling with Container

TextInput handles text editing but not visual styling like backgrounds and borders. Wrap it in a Container for full styling:

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
container()
    .padding([8.0, 12.0])
    .background(Color::rgb(0.15, 0.15, 0.2))
    .border(1.0, Color::rgb(0.3, 0.3, 0.4))
    .corners(4.0)
    .child(
        container().child(text_input(value).font_size(14.0).color(Color::WHITE))
    )
;
}

With Focus State

Add visual feedback when the input is focused:

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
container()
    .padding([8.0, 12.0])
    .background(Color::rgb(0.15, 0.15, 0.2))
    .border(1.0, Color::rgb(0.3, 0.3, 0.4))
    .corners(4.0)
    .when_focused(|s| s.border(1.0, Color::rgb(0.4, 0.6, 1.0)))
    .child(
        container().child(text_input(value).color(Color::WHITE))
    )
;
}

Complete Example

A login form with username and password fields:

extern crate guido;
use guido::prelude::*;
fn login_form() -> Container {
    let username = create_signal(String::new());
    let password = create_password();

    container()
        .padding(24.0)
        .background(Color::rgb(0.1, 0.1, 0.15))
        .corners(12.0)
        .layout(Flex::column().spacing(16.0))
        .children([
            // Username field
            container()
                .layout(Flex::column().spacing(4.0))
                .children([
                    container().child(text("Username").font_size(12.0).color(Color::rgb(0.6, 0.6, 0.7))),
                    container()
                        .padding([8.0, 12.0])
                        .background(Color::rgb(0.15, 0.15, 0.2))
                        .border(1.0, Color::rgb(0.3, 0.3, 0.4))
                        .corners(4.0)
                        .when_focused(|s| s.border(1.0, Color::rgb(0.4, 0.6, 1.0)))
                        .child(
                            container().child(text_input(username).font_size(14.0).color(Color::WHITE))
                        ),
                ]),
            // Password field
            container()
                .layout(Flex::column().spacing(4.0))
                .children([
                    container().child(text("Password").font_size(12.0).color(Color::rgb(0.6, 0.6, 0.7))),
                    container()
                        .padding([8.0, 12.0])
                        .background(Color::rgb(0.15, 0.15, 0.2))
                        .border(1.0, Color::rgb(0.3, 0.3, 0.4))
                        .corners(4.0)
                        .when_focused(|s| s.border(1.0, Color::rgb(0.4, 0.6, 1.0)))
                        .child(
                            container().child(password_input(password).font_size(14.0).color(Color::WHITE))
                        ),
                ]),
            // Submit button
            container()
                .padding([10.0, 16.0])
                .background(Color::rgb(0.3, 0.5, 0.9))
                .corners(6.0)
                .when_hovered(|s| s.lighter(0.1))
                .when_pressed(|s| s.darker(0.1))
                .on_click(move || {
                    password.with(|secret| {
                        println!("Login: {} / {} characters", username.get(), secret.expose().chars().count());
                    });
                })
                .child(
                    container().child(text("Sign In").font_size(14.0).color(Color::WHITE))
                ),
        ])
}
fn main() {}

Features

  • Selection: Click and drag to select text, or use Shift+Arrow keys. A double click selects the word under the pointer, and dragging after it extends the selection a word at a time; a triple click selects everything. A word is what Unicode’s word segmentation (UAX #29) makes one, as in GTK, Qt and the browsers: a path or an address divides at its slashes and its @, while a full stop or an apostrophe between two letters, as in example.com or don’t, stays inside the word. A double click on spaces or punctuation selects only those. Ctrl+Left/Right stops at the same ends of words, stepping over the spaces and punctuation between them.
  • Clipboard: Full copy/cut/paste support via Ctrl+C/X/V. Another application’s copy is read only when you paste, off the UI thread, so a paste never blocks it: the text appears once that application answers. The field is one line, so a pasted line break is removed, as a browser’s text input removes it: a line copied from a terminal with its newline arrives as the line alone, and two lines arrive joined
  • Primary selection: selecting text with the mouse sets the primary selection (paste it elsewhere with middle click); middle-clicking the input pastes the primary selection at the click position
  • Undo/Redo: History with intelligent coalescing of rapid edits
  • Scrolling: Long text scrolls horizontally to keep cursor visible
  • Cursor Blinking: Standard blinking cursor when focused. It asks the loop to wake at its next toggle rather than animating, so a focused field costs two wakeups a second instead of a frame at the compositor’s rate; no_caret() drops it entirely and costs nothing
  • Key Repeat: Hold keys for continuous input, at the rate and delay the compositor reports — the same settings every other application on the session obeys

API Reference

// not compiled: a signature listing — these declarations have no bodies.
text_input(signal: RwSignal<String>) -> TextInput
password_input(password: Password) -> PasswordInput   // TextInput<Masked>

// Both fields
impl<C> TextInput<C> {
    pub fn autofocus(self) -> Self;
    pub fn caret<M>(self, caret: impl IntoSignal<bool, M>) -> Self;
    pub fn no_caret(self) -> Self;  // Shorthand for caret(false)
    pub fn cursor<M>(self, cursor: impl IntoSignal<CursorIcon, M>) -> Self;  // Text by default
    pub fn placeholder<M>(self, text: impl IntoSignal<String, M>) -> Self;
    pub fn readonly<M>(self, readonly: impl IntoSignal<bool, M>) -> Self;
}

impl TextInput {
    pub fn on_change<F: Fn(&str) + 'static>(self, callback: F) -> Self;
    pub fn on_submit<F: Fn(&str) + 'static>(self, callback: F) -> Self;
}

impl PasswordInput {
    pub fn mask_char<M>(self, c: impl IntoSignal<char, M>) -> Self;
    pub fn reveal<M>(self, reveal: impl IntoSignal<bool, M>) -> Self;  // copies while shown
    pub fn on_submit<F: Fn() + 'static>(self, callback: F) -> Self;
}

create_password() -> Password
impl Password {
    pub fn with<R>(&self, f: impl FnOnce(&Secret) -> R) -> R;  // tracked
    pub fn is_empty(&self) -> bool;                              // tracked
    pub fn set(&self, secret: Secret);
    pub fn take(&self) -> Secret;                                // empties the field
    pub fn clear(&self);
}

impl Secret {
    pub fn expose(&self) -> &str;
    pub fn is_empty(&self) -> bool;
    pub fn duplicate(&self) -> Secret;  // its own locked pages
}
impl From<String> for Secret   // wipes the String

Note: The on_change callback is optional and is called in addition to the automatic signal update. Use it for side effects like validation or logging, not for updating the signal (that happens automatically).