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

Buttons & Inputs

Blinc provides ready-to-use input widgets. Their hover, press and checked looks are patched in place from signals; they don’t rebuild when the pointer moves.

Buttons

Basic Button

#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;

button("Save").on_click(|_| println!("Saved!"))
}

A button keeps its pointer state under a key made from where it was created. Buttons made in a loop from one line need a key each:

#![allow(unused)]
fn main() {
div().flex_row().gap(2.0).children(
    ["Cut", "Copy", "Paste"].map(|label| button(label).key(format!("edit-{label}"))),
)
}

Styled Buttons

#![allow(unused)]
fn main() {
button("Primary")
    .bg_color(Color::rgba(0.3, 0.5, 0.9, 1.0))
    .hover_color(Color::rgba(0.4, 0.6, 1.0, 1.0))
    .pressed_color(Color::rgba(0.2, 0.4, 0.8, 1.0))
    .text_color(Color::WHITE)
    .rounded(8.0)
    .px(4.0)
}

The colours default to the theme’s primary tokens. .id(..) and .class(..) let a stylesheet style the button, including :hover, :active and :disabled.

A Label That Follows a Signal

The label takes a string or a signal, and .disabled(..) takes a bool or a signal. Both are patched in place:

#![allow(unused)]
fn main() {
use blinc_core::reactive::{computed, signal};

let saving = signal(false);
let label = computed(move |g| {
    if g.get(saving).unwrap_or(false) { "Saving…".to_string() } else { "Save".to_string() }
});

button(label)
    .disabled(saving)
    .on_click(move |_| saving.set(true))
}

Custom Content Buttons

button_with builds the content once and hands it a ButtonLook. Bind what should follow the button’s state to it:

#![allow(unused)]
fn main() {
use blinc_layout::widgets::button_with;

button_with(|look| {
    div()
        .flex_row()
        .gap(2.0)
        .items_center()
        .child(svg(SAVE_ICON).size(16.0, 16.0).color(look.text_color()))
        .child(text("Save").color(look.text_color()))
})
.on_click(|_| save_file())
}

look.text_color() is the text colour for the button’s current state, after stylesheet overrides. look.interaction() gives its hovered, pressed and focused signals.

Disabled Buttons

#![allow(unused)]
fn main() {
button("Cannot Click")
    .disabled(true)
    .disabled_color(Color::rgba(0.2, 0.2, 0.25, 0.5))
    .disabled_text_color(Color::rgba(1.0, 1.0, 1.0, 0.4))
    .flat_when_disabled(true)
}

A disabled button has its disabled fill and does not click. .disabled_border(width, color) replaces its border while disabled.


Checkboxes

Basic Checkbox

#![allow(unused)]
fn main() {
use blinc_core::context_state::use_state_keyed;
use blinc_layout::widgets::checkbox::checkbox;

let remember = use_state_keyed("remember_me", || false);

checkbox(&remember).on_change(|checked| println!("Checkbox is now: {checked}"))
}

The checkbox reads and writes the State<bool> it is given, so the state is the source of truth: set it from elsewhere and the box follows.

Labeled Checkbox

#![allow(unused)]
fn main() {
checkbox(&remember)
    .label("Remember me")
    .label_color(Color::WHITE)
}

checkbox_labeled(&remember, "Remember me") is the same.

Styled Checkbox

#![allow(unused)]
fn main() {
checkbox(&remember)
    .check_color(Color::WHITE)
    .checked_bg(Color::rgba(0.4, 0.6, 1.0, 1.0))
    .unchecked_bg(Color::rgba(0.2, 0.2, 0.25, 1.0))
    .rounded(4.0)
    .checkbox_size(20.0)
}

Initially Checked

#![allow(unused)]
fn main() {
let remember = use_state_keyed("remember_me", || true); // Start checked
}

Text Input

Basic Text Input

#![allow(unused)]
fn main() {
use blinc_layout::widgets::text_input::{text_input, text_input_state_with_placeholder};

fn my_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let state = text_input_state_with_placeholder("Enter your name...");

    text_input(&state)
        .w(300.0)
        .on_change(|text| {
            println!("Input: {}", text);
        })
}
}

Styled Text Input

#![allow(unused)]
fn main() {
text_input(&state)
    .w(300.0)
    .rounded(8.0)
    .idle_bg_color(Color::rgba(0.15, 0.15, 0.2, 1.0))
    .text_color(Color::WHITE)
    .placeholder_color(Color::rgba(0.5, 0.5, 0.6, 1.0))
    .focused_border_color(Color::rgba(0.4, 0.6, 1.0, 1.0))
}

Reading Input Value

#![allow(unused)]
fn main() {
let state = text_input_state();

// Later, read the current value
let current_text = state.lock().unwrap().value.clone();
}

Setting the Value from Code

The field is built once. Typing, focus, hover and scrolling patch what it shows in place. After changing its data from outside the widget, as a stepper button does, call refresh_text_input so the field shows the change:

#![allow(unused)]
fn main() {
use blinc_layout::widgets::text_input::refresh_text_input;

{
    let mut data = state.lock().unwrap();
    data.value = "42".to_string();
    data.cursor = data.value.chars().count();
}
refresh_text_input(&state);
}

OTP Input

input_otp renders linked single-character text input slots for verification codes and PINs. It keeps a joined State<String> in sync while handling focus movement across slots.

#![allow(unused)]
fn main() {
use blinc_layout::widgets::input_otp::input_otp;

fn verification_code(ctx: &WindowedContext) -> impl ElementBuilder {
    let code = ctx.use_state_keyed("otp_code", || String::new());

    input_otp(&code, 6)
        .numeric_only(true)
        .on_complete(|code| println!("complete code: {code}"))
}
}

Behavior

  • Typing a character advances focus to the next slot.
  • Backspace on an empty slot rewinds focus and clears the previous slot.
  • Pasting distributes characters across the remaining slots.
  • numeric_only(true) filters typed and pasted input to ASCII digits.

Options

#![allow(unused)]
fn main() {
input_otp(&code, 6)
    .numeric_only(true)
    .masked(false)
    .gap(8.0)
    .class("verification-code")
}

Text Area

Basic Text Area

#![allow(unused)]
fn main() {
use blinc_layout::widgets::text_area::{text_area, text_area_state_with_placeholder};

fn my_ui(ctx: &WindowedContext) -> impl ElementBuilder {
    let state = text_area_state_with_placeholder("Enter description...");

    text_area(&state)
        .w(400.0)
        .rows(6)
        .on_change(|text| {
            println!("Content: {}", text);
        })
}
}

Styled Text Area

Size and shape are set on the builder. Colours come from the stylesheet, by class or id, per state:

#![allow(unused)]
fn main() {
text_area(&state)
    .w(400.0)
    .rows(6)
    .rounded(8.0)
    .font_size(14.0)
    .class("notes")
}
.notes { background: #1e1e2a; color: #ffffff; caret-color: #6699ff; }
.notes:hover { background: #24243a; }
.notes:focus { border-color: #6699ff; }

Setting the Text from Code

Like the text input, the text area is built once and patches what it shows in place. After changing its state from outside the widget, call refresh_text_area so the field shows the change:

#![allow(unused)]
fn main() {
use blinc_layout::widgets::text_area::refresh_text_area;

state.lock().unwrap().set_value("First line\nSecond line");
refresh_text_area(&state);
}

Code Editor

Syntax Highlighted Code

use blinc_layout::widgets::code::code;

fn my_ui() -> impl ElementBuilder {
    let source = r#"
fn main() {
    println!("Hello, Blinc!");
}
"#;

    code(source)
        .lang("rust")
        .w_full()
        .h(300.0)
        .rounded(8.0)
        .font("Fira Code")
        .size(14.0)
}

Supported Languages

  • rust, python, javascript, typescript
  • html, css, json, yaml, xml
  • sql, bash, go, java, c, cpp
  • And more…

Form Example

#![allow(unused)]
fn main() {
fn login_form(ctx: &WindowedContext) -> impl ElementBuilder {
    let email_state = text_input_state_with_placeholder("Email address");
    let password_state = text_input_state_with_placeholder("Password");
    let remember_state = use_state_keyed("remember_me", || false);

    div()
        .w(400.0)
        .p(24.0)
        .rounded(16.0)
        .bg(Color::rgba(0.12, 0.12, 0.16, 1.0))
        .flex_col()
        .gap(16.0)
        // Title
        .child(
            text("Sign In")
                .size(24.0)
                .weight(FontWeight::Bold)
                .color(Color::WHITE)
        )
        // Email field
        .child(
            div()
                .flex_col()
                .gap(4.0)
                .child(label("Email").color(Color::WHITE))
                .child(
                    text_input(&email_state)
                        .w_full()
                        .rounded(8.0)
                )
        )
        // Password field
        .child(
            div()
                .flex_col()
                .gap(4.0)
                .child(label("Password").color(Color::WHITE))
                .child(
                    text_input(&password_state)
                        .w_full()
                        .rounded(8.0)
                        // Note: password masking would be a feature to add
                )
        )
        // Remember me
        .child(
            checkbox(&remember_state)
                .label("Remember me")
                .label_color(Color::WHITE)
        )
        // Submit button
        .child(
            button("Sign In")
                .w_full()
                .bg_color(Color::rgba(0.3, 0.5, 0.9, 1.0))
                .text_color(Color::WHITE)
                .rounded(8.0)
                .on_click(|_| {
                    println!("Form submitted!");
                })
        )
}
}

Widget State

WidgetStateSource
ButtonHovered, pressed, focused, disabledSignals from Interaction; .disabled(..)
CheckboxChecked, hoveredThe State<bool> it is given; pointer signals
TextInputTextFieldState: Idle, Hovered, Focused, FocusedHovered, DisabledIts own state handle
TextAreaTextFieldStateSame as TextInput

Best Practices

  1. Use unique keys for state - Each widget needs its own state key.

  2. Handle validation in on_change - Validate input as users type.

  3. Provide visual feedback - Use colors to indicate focus and errors.

  4. Group related inputs - Use flex containers to organize forms.

  5. Add labels - Every input should have an associated label for accessibility.