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

Controls

At the end of this chapter, a settings card has a switch, a checkbox, a slider, a text field and a select, and each one reports through the model.

The controls keep no state

This is the thing to notice. widgets::switch(ui, "Notifications", self.notify, Msg::Notify) draws the switch on when self.notify is true. The switch does not remember whether it is on. Your model does.

Click it, and Msg::Notify arrives. Your handler flips self.notify. The next frame draws the switch from the new value. If your handler did nothing, the switch would not move.

//! Step 4 — the stock controls. Builds on step 3 by replacing the
//! buttons with a settings card: a switch, a checkbox, a slider, a text
//! field and a select, each drawn from the model and each reporting
//! through a message. Chapter 5 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_04_controls

use kui_native::widgets;
use kui_native::{Align, App, Key, Message, NodeSpec, TextStyle, Ui, UiEvent};

const LANGUAGES: [&str; 3] = ["English", "Deutsch", "日本語"];

#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    /// A toggle flips: the click is the message, nothing else.
    Notify,
    Sound,
    /// A slider proposes a value: the message is a tag, the value rides
    /// beside it on the event.
    Volume,
}

struct Settings {
    notify: bool,
    sound: bool,
    volume: f32,
    language: usize,
    /// The select's key, kept so `on_event` knows its choice by key.
    language_key: Option<Key>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            notify: true,
            sound: false,
            volume: 40.0,
            language: 0,
            language_key: None,
        }
    }
}

impl App for Settings {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(NodeSpec::column().fill().center().bg(t.bg), |ui| {
            ui.with(
                NodeSpec::column()
                    .width(320.0)
                    .pad(20.0)
                    .gap(14.0)
                    .cross_align(Align::Start)
                    .bg(t.surface)
                    .radius(12.0)
                    .border(1.0, t.border),
                |ui| {
                    ui.text("Settings", TextStyle::new(18.0).color(t.fg));

                    // Each control is drawn from the model: `self.notify`
                    // says whether the switch is on. The control keeps no
                    // state of its own.
                    widgets::switch(ui, "Notifications", self.notify, Msg::Notify);
                    widgets::checkbox(ui, "Play a sound", self.sound, Msg::Sound);

                    ui.with(NodeSpec::row().gap(12.0).cross_align(Align::Center), |ui| {
                        widgets::slider(ui, "Volume", self.volume, 0.0, 100.0, 5.0, Msg::Volume);
                        ui.text(
                            &format!("{:.0}", self.volume),
                            TextStyle::new(13.0).color(t.muted),
                        );
                    });

                    // The editor owns its text between frames. The view
                    // reads it back by key.
                    let name = widgets::text_input(ui, "Display name", "");
                    let typed = ui.edit_text(name).unwrap_or_default();
                    ui.text(
                        &format!(
                            "Hello, {}",
                            if typed.is_empty() { "stranger" } else { &typed }
                        ),
                        TextStyle::new(13.0).color(t.muted),
                    );

                    // A select posts the chosen row on its own key.
                    self.language_key = Some(widgets::select(
                        ui,
                        "Language",
                        &LANGUAGES,
                        Some(self.language),
                    ));
                },
            );
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Notify) => self.notify = !self.notify,
            Some(Msg::Sound) => self.sound = !self.sound,
            Some(Msg::Volume) => {
                // A `change` event: the message was its tag, the value is
                // a field beside it.
                if let Some(v) = ev.payload.get_float("value") {
                    self.volume = v as f32;
                }
            }
            None => {
                // The select speaks by key, not by message: a `menu`
                // event with the chosen label as `item`.
                if Some(ev.key) == self.language_key
                    && let Some(item) = ev.payload.get_str("item")
                    && let Some(i) = LANGUAGES.iter().position(|l| *l == item)
                {
                    self.language = i;
                }
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    kui_native::app("Settings")
        .size(480.0, 420.0)
        .run(Settings::default())
}

The model and its messages

//! Step 4 — the stock controls. Builds on step 3 by replacing the
//! buttons with a settings card: a switch, a checkbox, a slider, a text
//! field and a select, each drawn from the model and each reporting
//! through a message. Chapter 5 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_04_controls

use kui_native::widgets;
use kui_native::{Align, App, Key, Message, NodeSpec, TextStyle, Ui, UiEvent};

const LANGUAGES: [&str; 3] = ["English", "Deutsch", "日本語"];

#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    /// A toggle flips: the click is the message, nothing else.
    Notify,
    Sound,
    /// A slider proposes a value: the message is a tag, the value rides
    /// beside it on the event.
    Volume,
}

struct Settings {
    notify: bool,
    sound: bool,
    volume: f32,
    language: usize,
    /// The select's key, kept so `on_event` knows its choice by key.
    language_key: Option<Key>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            notify: true,
            sound: false,
            volume: 40.0,
            language: 0,
            language_key: None,
        }
    }
}

impl App for Settings {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(NodeSpec::column().fill().center().bg(t.bg), |ui| {
            ui.with(
                NodeSpec::column()
                    .width(320.0)
                    .pad(20.0)
                    .gap(14.0)
                    .cross_align(Align::Start)
                    .bg(t.surface)
                    .radius(12.0)
                    .border(1.0, t.border),
                |ui| {
                    ui.text("Settings", TextStyle::new(18.0).color(t.fg));

                    // Each control is drawn from the model: `self.notify`
                    // says whether the switch is on. The control keeps no
                    // state of its own.
                    widgets::switch(ui, "Notifications", self.notify, Msg::Notify);
                    widgets::checkbox(ui, "Play a sound", self.sound, Msg::Sound);

                    ui.with(NodeSpec::row().gap(12.0).cross_align(Align::Center), |ui| {
                        widgets::slider(ui, "Volume", self.volume, 0.0, 100.0, 5.0, Msg::Volume);
                        ui.text(
                            &format!("{:.0}", self.volume),
                            TextStyle::new(13.0).color(t.muted),
                        );
                    });

                    // The editor owns its text between frames. The view
                    // reads it back by key.
                    let name = widgets::text_input(ui, "Display name", "");
                    let typed = ui.edit_text(name).unwrap_or_default();
                    ui.text(
                        &format!(
                            "Hello, {}",
                            if typed.is_empty() { "stranger" } else { &typed }
                        ),
                        TextStyle::new(13.0).color(t.muted),
                    );

                    // A select posts the chosen row on its own key.
                    self.language_key = Some(widgets::select(
                        ui,
                        "Language",
                        &LANGUAGES,
                        Some(self.language),
                    ));
                },
            );
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Notify) => self.notify = !self.notify,
            Some(Msg::Sound) => self.sound = !self.sound,
            Some(Msg::Volume) => {
                // A `change` event: the message was its tag, the value is
                // a field beside it.
                if let Some(v) = ev.payload.get_float("value") {
                    self.volume = v as f32;
                }
            }
            None => {
                // The select speaks by key, not by message: a `menu`
                // event with the chosen label as `item`.
                if Some(ev.key) == self.language_key
                    && let Some(item) = ev.payload.get_str("item")
                    && let Some(i) = LANGUAGES.iter().position(|l| *l == item)
                {
                    self.language = i;
                }
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    kui_native::app("Settings")
        .size(480.0, 420.0)
        .run(Settings::default())
}
//! Step 4 — the stock controls. Builds on step 3 by replacing the
//! buttons with a settings card: a switch, a checkbox, a slider, a text
//! field and a select, each drawn from the model and each reporting
//! through a message. Chapter 5 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_04_controls

use kui_native::widgets;
use kui_native::{Align, App, Key, Message, NodeSpec, TextStyle, Ui, UiEvent};

const LANGUAGES: [&str; 3] = ["English", "Deutsch", "日本語"];

#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    /// A toggle flips: the click is the message, nothing else.
    Notify,
    Sound,
    /// A slider proposes a value: the message is a tag, the value rides
    /// beside it on the event.
    Volume,
}

struct Settings {
    notify: bool,
    sound: bool,
    volume: f32,
    language: usize,
    /// The select's key, kept so `on_event` knows its choice by key.
    language_key: Option<Key>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            notify: true,
            sound: false,
            volume: 40.0,
            language: 0,
            language_key: None,
        }
    }
}

impl App for Settings {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(NodeSpec::column().fill().center().bg(t.bg), |ui| {
            ui.with(
                NodeSpec::column()
                    .width(320.0)
                    .pad(20.0)
                    .gap(14.0)
                    .cross_align(Align::Start)
                    .bg(t.surface)
                    .radius(12.0)
                    .border(1.0, t.border),
                |ui| {
                    ui.text("Settings", TextStyle::new(18.0).color(t.fg));

                    // Each control is drawn from the model: `self.notify`
                    // says whether the switch is on. The control keeps no
                    // state of its own.
                    widgets::switch(ui, "Notifications", self.notify, Msg::Notify);
                    widgets::checkbox(ui, "Play a sound", self.sound, Msg::Sound);

                    ui.with(NodeSpec::row().gap(12.0).cross_align(Align::Center), |ui| {
                        widgets::slider(ui, "Volume", self.volume, 0.0, 100.0, 5.0, Msg::Volume);
                        ui.text(
                            &format!("{:.0}", self.volume),
                            TextStyle::new(13.0).color(t.muted),
                        );
                    });

                    // The editor owns its text between frames. The view
                    // reads it back by key.
                    let name = widgets::text_input(ui, "Display name", "");
                    let typed = ui.edit_text(name).unwrap_or_default();
                    ui.text(
                        &format!(
                            "Hello, {}",
                            if typed.is_empty() { "stranger" } else { &typed }
                        ),
                        TextStyle::new(13.0).color(t.muted),
                    );

                    // A select posts the chosen row on its own key.
                    self.language_key = Some(widgets::select(
                        ui,
                        "Language",
                        &LANGUAGES,
                        Some(self.language),
                    ));
                },
            );
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Notify) => self.notify = !self.notify,
            Some(Msg::Sound) => self.sound = !self.sound,
            Some(Msg::Volume) => {
                // A `change` event: the message was its tag, the value is
                // a field beside it.
                if let Some(v) = ev.payload.get_float("value") {
                    self.volume = v as f32;
                }
            }
            None => {
                // The select speaks by key, not by message: a `menu`
                // event with the chosen label as `item`.
                if Some(ev.key) == self.language_key
                    && let Some(item) = ev.payload.get_str("item")
                    && let Some(i) = LANGUAGES.iter().position(|l| *l == item)
                {
                    self.language = i;
                }
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    kui_native::app("Settings")
        .size(480.0, 420.0)
        .run(Settings::default())
}

A slider proposes a value

A toggle’s message is the whole story: the click is the change. A slider is different. The user drags to some value, and the slider needs to tell you which.

//! Step 4 — the stock controls. Builds on step 3 by replacing the
//! buttons with a settings card: a switch, a checkbox, a slider, a text
//! field and a select, each drawn from the model and each reporting
//! through a message. Chapter 5 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_04_controls

use kui_native::widgets;
use kui_native::{Align, App, Key, Message, NodeSpec, TextStyle, Ui, UiEvent};

const LANGUAGES: [&str; 3] = ["English", "Deutsch", "日本語"];

#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    /// A toggle flips: the click is the message, nothing else.
    Notify,
    Sound,
    /// A slider proposes a value: the message is a tag, the value rides
    /// beside it on the event.
    Volume,
}

struct Settings {
    notify: bool,
    sound: bool,
    volume: f32,
    language: usize,
    /// The select's key, kept so `on_event` knows its choice by key.
    language_key: Option<Key>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            notify: true,
            sound: false,
            volume: 40.0,
            language: 0,
            language_key: None,
        }
    }
}

impl App for Settings {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(NodeSpec::column().fill().center().bg(t.bg), |ui| {
            ui.with(
                NodeSpec::column()
                    .width(320.0)
                    .pad(20.0)
                    .gap(14.0)
                    .cross_align(Align::Start)
                    .bg(t.surface)
                    .radius(12.0)
                    .border(1.0, t.border),
                |ui| {
                    ui.text("Settings", TextStyle::new(18.0).color(t.fg));

                    // Each control is drawn from the model: `self.notify`
                    // says whether the switch is on. The control keeps no
                    // state of its own.
                    widgets::switch(ui, "Notifications", self.notify, Msg::Notify);
                    widgets::checkbox(ui, "Play a sound", self.sound, Msg::Sound);

                    ui.with(NodeSpec::row().gap(12.0).cross_align(Align::Center), |ui| {
                        widgets::slider(ui, "Volume", self.volume, 0.0, 100.0, 5.0, Msg::Volume);
                        ui.text(
                            &format!("{:.0}", self.volume),
                            TextStyle::new(13.0).color(t.muted),
                        );
                    });

                    // The editor owns its text between frames. The view
                    // reads it back by key.
                    let name = widgets::text_input(ui, "Display name", "");
                    let typed = ui.edit_text(name).unwrap_or_default();
                    ui.text(
                        &format!(
                            "Hello, {}",
                            if typed.is_empty() { "stranger" } else { &typed }
                        ),
                        TextStyle::new(13.0).color(t.muted),
                    );

                    // A select posts the chosen row on its own key.
                    self.language_key = Some(widgets::select(
                        ui,
                        "Language",
                        &LANGUAGES,
                        Some(self.language),
                    ));
                },
            );
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Notify) => self.notify = !self.notify,
            Some(Msg::Sound) => self.sound = !self.sound,
            Some(Msg::Volume) => {
                // A `change` event: the message was its tag, the value is
                // a field beside it.
                if let Some(v) = ev.payload.get_float("value") {
                    self.volume = v as f32;
                }
            }
            None => {
                // The select speaks by key, not by message: a `menu`
                // event with the chosen label as `item`.
                if Some(ev.key) == self.language_key
                    && let Some(item) = ev.payload.get_str("item")
                    && let Some(i) = LANGUAGES.iter().position(|l| *l == item)
                {
                    self.language = i;
                }
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    kui_native::app("Settings")
        .size(480.0, 420.0)
        .run(Settings::default())
}

The message you give a slider is a tag. When the user drags, a change event arrives with your tag on it and the proposed value beside it. Store the value, and the next frame draws the knob there.

The value is snapped to the slider’s step (5, here) and clamped to its range before it reaches you.

Two kinds of data on an event

This is the first event that carries more than your message, so it is worth being precise about what ev holds.

ev.payload is a Value: a small JSON-like map. Two things write into it.

  • You, through the message. Msg::Volume becomes {kind: "volume"}, and ev.message::<Msg>() turns it back into the enum. The next chapter puts fields on a message — Msg::Toggle { id } — and those come back typed too, because the derive knows them.
  • The core, through the event’s own fields. A change event adds value. A drag adds x, y and phase. A key press adds code and the modifiers. The core does not know your enum, so these sit beside your message in the map, and you read them by name: ev.payload.get_float("value"), get_str("code"), get_int(..), get_bool(..). Each returns an Option, None when the field is absent.

So the pattern for any event that carries more than a click is: match the message to learn which control spoke, then read the core’s fields to learn what it said. The events table of docs/props.md lists every event’s fields.

A text field owns its text

//! Step 4 — the stock controls. Builds on step 3 by replacing the
//! buttons with a settings card: a switch, a checkbox, a slider, a text
//! field and a select, each drawn from the model and each reporting
//! through a message. Chapter 5 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_04_controls

use kui_native::widgets;
use kui_native::{Align, App, Key, Message, NodeSpec, TextStyle, Ui, UiEvent};

const LANGUAGES: [&str; 3] = ["English", "Deutsch", "日本語"];

#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    /// A toggle flips: the click is the message, nothing else.
    Notify,
    Sound,
    /// A slider proposes a value: the message is a tag, the value rides
    /// beside it on the event.
    Volume,
}

struct Settings {
    notify: bool,
    sound: bool,
    volume: f32,
    language: usize,
    /// The select's key, kept so `on_event` knows its choice by key.
    language_key: Option<Key>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            notify: true,
            sound: false,
            volume: 40.0,
            language: 0,
            language_key: None,
        }
    }
}

impl App for Settings {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(NodeSpec::column().fill().center().bg(t.bg), |ui| {
            ui.with(
                NodeSpec::column()
                    .width(320.0)
                    .pad(20.0)
                    .gap(14.0)
                    .cross_align(Align::Start)
                    .bg(t.surface)
                    .radius(12.0)
                    .border(1.0, t.border),
                |ui| {
                    ui.text("Settings", TextStyle::new(18.0).color(t.fg));

                    // Each control is drawn from the model: `self.notify`
                    // says whether the switch is on. The control keeps no
                    // state of its own.
                    widgets::switch(ui, "Notifications", self.notify, Msg::Notify);
                    widgets::checkbox(ui, "Play a sound", self.sound, Msg::Sound);

                    ui.with(NodeSpec::row().gap(12.0).cross_align(Align::Center), |ui| {
                        widgets::slider(ui, "Volume", self.volume, 0.0, 100.0, 5.0, Msg::Volume);
                        ui.text(
                            &format!("{:.0}", self.volume),
                            TextStyle::new(13.0).color(t.muted),
                        );
                    });

                    // The editor owns its text between frames. The view
                    // reads it back by key.
                    let name = widgets::text_input(ui, "Display name", "");
                    let typed = ui.edit_text(name).unwrap_or_default();
                    ui.text(
                        &format!(
                            "Hello, {}",
                            if typed.is_empty() { "stranger" } else { &typed }
                        ),
                        TextStyle::new(13.0).color(t.muted),
                    );

                    // A select posts the chosen row on its own key.
                    self.language_key = Some(widgets::select(
                        ui,
                        "Language",
                        &LANGUAGES,
                        Some(self.language),
                    ));
                },
            );
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Notify) => self.notify = !self.notify,
            Some(Msg::Sound) => self.sound = !self.sound,
            Some(Msg::Volume) => {
                // A `change` event: the message was its tag, the value is
                // a field beside it.
                if let Some(v) = ev.payload.get_float("value") {
                    self.volume = v as f32;
                }
            }
            None => {
                // The select speaks by key, not by message: a `menu`
                // event with the chosen label as `item`.
                if Some(ev.key) == self.language_key
                    && let Some(item) = ev.payload.get_str("item")
                    && let Some(i) = LANGUAGES.iter().position(|l| *l == item)
                {
                    self.language = i;
                }
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    kui_native::app("Settings")
        .size(480.0, 420.0)
        .run(Settings::default())
}

Typing is the one thing the frame cannot redo from the model every frame — the caret, the selection and the undo history live between frames. So an editor keeps its text, and the view reads it back with ui.edit_text(key). text_input returns the key.

If you need to set the text, ui.set_edit_text(key, "..") does it.

A select speaks by key

//! Step 4 — the stock controls. Builds on step 3 by replacing the
//! buttons with a settings card: a switch, a checkbox, a slider, a text
//! field and a select, each drawn from the model and each reporting
//! through a message. Chapter 5 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_04_controls

use kui_native::widgets;
use kui_native::{Align, App, Key, Message, NodeSpec, TextStyle, Ui, UiEvent};

const LANGUAGES: [&str; 3] = ["English", "Deutsch", "日本語"];

#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    /// A toggle flips: the click is the message, nothing else.
    Notify,
    Sound,
    /// A slider proposes a value: the message is a tag, the value rides
    /// beside it on the event.
    Volume,
}

struct Settings {
    notify: bool,
    sound: bool,
    volume: f32,
    language: usize,
    /// The select's key, kept so `on_event` knows its choice by key.
    language_key: Option<Key>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            notify: true,
            sound: false,
            volume: 40.0,
            language: 0,
            language_key: None,
        }
    }
}

impl App for Settings {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(NodeSpec::column().fill().center().bg(t.bg), |ui| {
            ui.with(
                NodeSpec::column()
                    .width(320.0)
                    .pad(20.0)
                    .gap(14.0)
                    .cross_align(Align::Start)
                    .bg(t.surface)
                    .radius(12.0)
                    .border(1.0, t.border),
                |ui| {
                    ui.text("Settings", TextStyle::new(18.0).color(t.fg));

                    // Each control is drawn from the model: `self.notify`
                    // says whether the switch is on. The control keeps no
                    // state of its own.
                    widgets::switch(ui, "Notifications", self.notify, Msg::Notify);
                    widgets::checkbox(ui, "Play a sound", self.sound, Msg::Sound);

                    ui.with(NodeSpec::row().gap(12.0).cross_align(Align::Center), |ui| {
                        widgets::slider(ui, "Volume", self.volume, 0.0, 100.0, 5.0, Msg::Volume);
                        ui.text(
                            &format!("{:.0}", self.volume),
                            TextStyle::new(13.0).color(t.muted),
                        );
                    });

                    // The editor owns its text between frames. The view
                    // reads it back by key.
                    let name = widgets::text_input(ui, "Display name", "");
                    let typed = ui.edit_text(name).unwrap_or_default();
                    ui.text(
                        &format!(
                            "Hello, {}",
                            if typed.is_empty() { "stranger" } else { &typed }
                        ),
                        TextStyle::new(13.0).color(t.muted),
                    );

                    // A select posts the chosen row on its own key.
                    self.language_key = Some(widgets::select(
                        ui,
                        "Language",
                        &LANGUAGES,
                        Some(self.language),
                    ));
                },
            );
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Notify) => self.notify = !self.notify,
            Some(Msg::Sound) => self.sound = !self.sound,
            Some(Msg::Volume) => {
                // A `change` event: the message was its tag, the value is
                // a field beside it.
                if let Some(v) = ev.payload.get_float("value") {
                    self.volume = v as f32;
                }
            }
            None => {
                // The select speaks by key, not by message: a `menu`
                // event with the chosen label as `item`.
                if Some(ev.key) == self.language_key
                    && let Some(item) = ev.payload.get_str("item")
                    && let Some(i) = LANGUAGES.iter().position(|l| *l == item)
                {
                    self.language = i;
                }
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    kui_native::app("Settings")
        .size(480.0, 420.0)
        .run(Settings::default())
}

A select opens a menu of its options. Choosing one posts a menu event on the select’s own key, with the chosen label as item. The handler recognises it by comparing ev.key to the key the view kept.

The handler

//! Step 4 — the stock controls. Builds on step 3 by replacing the
//! buttons with a settings card: a switch, a checkbox, a slider, a text
//! field and a select, each drawn from the model and each reporting
//! through a message. Chapter 5 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_04_controls

use kui_native::widgets;
use kui_native::{Align, App, Key, Message, NodeSpec, TextStyle, Ui, UiEvent};

const LANGUAGES: [&str; 3] = ["English", "Deutsch", "日本語"];

#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    /// A toggle flips: the click is the message, nothing else.
    Notify,
    Sound,
    /// A slider proposes a value: the message is a tag, the value rides
    /// beside it on the event.
    Volume,
}

struct Settings {
    notify: bool,
    sound: bool,
    volume: f32,
    language: usize,
    /// The select's key, kept so `on_event` knows its choice by key.
    language_key: Option<Key>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            notify: true,
            sound: false,
            volume: 40.0,
            language: 0,
            language_key: None,
        }
    }
}

impl App for Settings {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(NodeSpec::column().fill().center().bg(t.bg), |ui| {
            ui.with(
                NodeSpec::column()
                    .width(320.0)
                    .pad(20.0)
                    .gap(14.0)
                    .cross_align(Align::Start)
                    .bg(t.surface)
                    .radius(12.0)
                    .border(1.0, t.border),
                |ui| {
                    ui.text("Settings", TextStyle::new(18.0).color(t.fg));

                    // Each control is drawn from the model: `self.notify`
                    // says whether the switch is on. The control keeps no
                    // state of its own.
                    widgets::switch(ui, "Notifications", self.notify, Msg::Notify);
                    widgets::checkbox(ui, "Play a sound", self.sound, Msg::Sound);

                    ui.with(NodeSpec::row().gap(12.0).cross_align(Align::Center), |ui| {
                        widgets::slider(ui, "Volume", self.volume, 0.0, 100.0, 5.0, Msg::Volume);
                        ui.text(
                            &format!("{:.0}", self.volume),
                            TextStyle::new(13.0).color(t.muted),
                        );
                    });

                    // The editor owns its text between frames. The view
                    // reads it back by key.
                    let name = widgets::text_input(ui, "Display name", "");
                    let typed = ui.edit_text(name).unwrap_or_default();
                    ui.text(
                        &format!(
                            "Hello, {}",
                            if typed.is_empty() { "stranger" } else { &typed }
                        ),
                        TextStyle::new(13.0).color(t.muted),
                    );

                    // A select posts the chosen row on its own key.
                    self.language_key = Some(widgets::select(
                        ui,
                        "Language",
                        &LANGUAGES,
                        Some(self.language),
                    ));
                },
            );
        });
    }

    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Notify) => self.notify = !self.notify,
            Some(Msg::Sound) => self.sound = !self.sound,
            Some(Msg::Volume) => {
                // A `change` event: the message was its tag, the value is
                // a field beside it.
                if let Some(v) = ev.payload.get_float("value") {
                    self.volume = v as f32;
                }
            }
            None => {
                // The select speaks by key, not by message: a `menu`
                // event with the chosen label as `item`.
                if Some(ev.key) == self.language_key
                    && let Some(item) = ev.payload.get_str("item")
                    && let Some(i) = LANGUAGES.iter().position(|l| *l == item)
                {
                    self.language = i;
                }
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    kui_native::app("Settings")
        .size(480.0, 420.0)
        .run(Settings::default())
}

Try this

  • Disable the checkbox while notifications are off. The stock widgets have a _with form that takes a spec: toggle_with(.., toggle_spec(&m) .checked(..).disabled(!self.notify).on_click(..), None). Look at widgets/controls.rs.
  • Show the volume as a bar whose width is Sizing::Percent(self.volume / 100.0).

Where this is decided

  • Every stock widget: kui_core::widgets, re-exported as kui_native::widgets.
  • The stock controls are built over the accessibility roles, so a switch is a switch to a screen reader and a Tab stop with nothing more from you.
  • The change, changed, submit and menu events: docs/props.md, events.