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

Disabling a Subtree

A form that stops responding while a request is in flight is one declaration, not a check inside every handler:

extern crate guido;
use guido::prelude::*;
fn main() {
let busy = create_signal(false);
let submit = || {};
container()
    .enabled(move || !busy.get())
    .child(container().on_click(submit).child(text("Send")))
;
}

enabled(false) stops every event at that container. Nothing below it — a click, a key, a scroll, a nested button’s handler — is reached, and nothing below it has to know why.

It still stands in the way, which is the point: a click on a disabled button is a click the application swallowed, not one the desktop behind the surface receives. Letting it through is takes_input(false), a different question asked of the compositor rather than of the widget.

It propagates, and cannot be undone from below

A container declaring enabled(true) inside one declaring enabled(false) is disabled. There is no way for a descendant to re-enable itself, which is what lets a whole form be switched off in one place and is the rule Qt, GTK and SwiftUI all settled on.

The look is declared beside the behaviour

A disabled subtree is painted exactly as declared — guido does not pick a grey for you. when_disabled is how you pick one, and it reads the same declaration that stopped the events, so the two cannot come apart:

extern crate guido;
use guido::prelude::*;
fn main() {
let busy = create_signal(false);
container()
    .enabled(move || !busy.get())
    .child(
        text("Send")
            .color(Color::WHITE)
            .when_disabled(|s| s.color(Color::rgb(0.5, 0.5, 0.5))),
    )
;
}

To fade the whole control instead — fill, label and icon together — when_disabled(|s| s.opacity(0.4)) overrides its opacity.

The text asks its control — the nearest enclosing interaction unit, which declaring enabled makes the container into. That is why the label greys with the form around it without being handed the signal itself.

An icon greys the same way, with when_disabled(|s| s.tint(..)) on the image — see Images.

A condition you already hold reads the same at one call site: state(busy, |s| s.darker(0.2)). What it cannot do is promise that two widgets in two files mean the same thing by it. Disabled is the control’s answer, with its ancestors folded in, so it means one thing everywhere.

What a disabled subtree gives up, and what it keeps

It gives up the pointer and the keyboard. A disabled control is neither hovered nor pressed nor focused, so when_hovered, when_pressed and when_focused all switch off with it — no stale highlight, and no focus ring on something that is not taking keys. A press held when it was disabled is dropped without a click, so enabled again nothing in it comes back pressed. Qt, GTK, SwiftUI and <fieldset disabled> all behave this way.

It is still laid out and still painted. Disabling changes what an event does, not where anything sits — so it costs no layout pass.

Not the same as read-only

There is a second thing you may want, and it is not this one: a field that stops accepting edits but goes on saying the keyboard is aimed at it. A lock screen that has sent the password to PAM wants exactly that — the typed characters must go nowhere, but on a multi-monitor setup the when_focused ring is the only thing saying which screen you are typing at, and disabling would take it away.

That is readonly, and it is a different setter for a different job:

extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
let busy = create_signal(false);
text_input(value).readonly(busy).caret(move || !busy.get())
;
}

It refuses every edit — typing, backspace, delete, paste, cut, undo — and nothing else. The caret still moves, a selection can still be made and copied, Enter still reaches on_submit, and the focus stays put. The caret is not part of it: a field that should stop blinking says so with caret, driven by the same signal, as above.

Every toolkit keeps these two apart, and for this reason: readonly beside disabled on the web, setReadOnly beside setEnabled in Qt, readOnly beside enabled in Flutter. Reach for enabled when the thing is not available, and for readonly when it is yours but frozen.