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

State and messages

At the end of this chapter, three buttons change a number.

The shape of an app

An app has three parts:

  1. A model: the state, held in your struct.
  2. A view: a function of the model that declares the frame.
  3. An on_event: a function that receives what the user did and changes the model.

The loop is: view runs, the frame is drawn, the user clicks, the click becomes an event, on_event changes the model, view runs again. You write the three parts. kui runs the loop.

The model

//! Step 3 — state and messages. Builds on step 2 by giving the app a
//! model, buttons that carry a message, and an `on_event` that moves the
//! model. Chapter 4 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_03_counter

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

/// What the view can say to the app. `derive(Message)` turns each variant
/// into plain data (`{kind: "inc"}`) on the way out and back into `Msg`
/// on the way in.
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    Inc,
    Dec,
    Reset,
}

#[derive(Default)]
struct Counter {
    count: i64,
}

impl App for Counter {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(
            NodeSpec::column().fill().center().gap(16.0).bg(t.bg),
            |ui| {
                ui.text(&self.count.to_string(), TextStyle::new(56.0).color(t.fg));
                ui.with(NodeSpec::row().gap(8.0), |ui| {
                    // A button takes the message it sends when clicked.
                    widgets::button(ui, "-1", Msg::Dec);
                    widgets::button(ui, "+1", Msg::Inc);
                    widgets::button(ui, "reset", Msg::Reset);
                });
            },
        );
    }

    /// Every event the frame produced comes here, one at a time, after
    /// the frame. A click carries the button's message.
    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Inc) => self.count += 1,
            Some(Msg::Dec) => self.count -= 1,
            Some(Msg::Reset) => self.count = 0,
            // Not ours: a resize, a focus change, a window event.
            None => {}
        }
    }
}

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

The messages

A button needs to say what it means when clicked. In kui, it says so with a value that travels through the frame and comes back in on_event. The value is any Value — a map, a string, a number — but building those by hand is tedious and untyped.

#[derive(Message)] does it for you:

//! Step 3 — state and messages. Builds on step 2 by giving the app a
//! model, buttons that carry a message, and an `on_event` that moves the
//! model. Chapter 4 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_03_counter

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

/// What the view can say to the app. `derive(Message)` turns each variant
/// into plain data (`{kind: "inc"}`) on the way out and back into `Msg`
/// on the way in.
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    Inc,
    Dec,
    Reset,
}

#[derive(Default)]
struct Counter {
    count: i64,
}

impl App for Counter {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(
            NodeSpec::column().fill().center().gap(16.0).bg(t.bg),
            |ui| {
                ui.text(&self.count.to_string(), TextStyle::new(56.0).color(t.fg));
                ui.with(NodeSpec::row().gap(8.0), |ui| {
                    // A button takes the message it sends when clicked.
                    widgets::button(ui, "-1", Msg::Dec);
                    widgets::button(ui, "+1", Msg::Inc);
                    widgets::button(ui, "reset", Msg::Reset);
                });
            },
        );
    }

    /// Every event the frame produced comes here, one at a time, after
    /// the frame. A click carries the button's message.
    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Inc) => self.count += 1,
            Some(Msg::Dec) => self.count -= 1,
            Some(Msg::Reset) => self.count = 0,
            // Not ours: a resize, a focus change, a window event.
            None => {}
        }
    }
}

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

Each variant becomes a small map on the way out: Msg::Inc is {kind: "inc"}. On the way back, ev.message::<Msg>() turns the map into Msg::Inc again. Variants can carry fields; chapter 6 uses one.

The view

//! Step 3 — state and messages. Builds on step 2 by giving the app a
//! model, buttons that carry a message, and an `on_event` that moves the
//! model. Chapter 4 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_03_counter

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

/// What the view can say to the app. `derive(Message)` turns each variant
/// into plain data (`{kind: "inc"}`) on the way out and back into `Msg`
/// on the way in.
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    Inc,
    Dec,
    Reset,
}

#[derive(Default)]
struct Counter {
    count: i64,
}

impl App for Counter {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(
            NodeSpec::column().fill().center().gap(16.0).bg(t.bg),
            |ui| {
                ui.text(&self.count.to_string(), TextStyle::new(56.0).color(t.fg));
                ui.with(NodeSpec::row().gap(8.0), |ui| {
                    // A button takes the message it sends when clicked.
                    widgets::button(ui, "-1", Msg::Dec);
                    widgets::button(ui, "+1", Msg::Inc);
                    widgets::button(ui, "reset", Msg::Reset);
                });
            },
        );
    }

    /// Every event the frame produced comes here, one at a time, after
    /// the frame. A click carries the button's message.
    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Inc) => self.count += 1,
            Some(Msg::Dec) => self.count -= 1,
            Some(Msg::Reset) => self.count = 0,
            // Not ours: a resize, a focus change, a window event.
            None => {}
        }
    }
}

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

widgets::button(ui, text, message) is the stock button. It draws its rest, hover and pressed states, takes keyboard focus, and sends its message when clicked. You give it the message and forget about it.

The handler

//! Step 3 — state and messages. Builds on step 2 by giving the app a
//! model, buttons that carry a message, and an `on_event` that moves the
//! model. Chapter 4 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_03_counter

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

/// What the view can say to the app. `derive(Message)` turns each variant
/// into plain data (`{kind: "inc"}`) on the way out and back into `Msg`
/// on the way in.
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
    Inc,
    Dec,
    Reset,
}

#[derive(Default)]
struct Counter {
    count: i64,
}

impl App for Counter {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        ui.with(
            NodeSpec::column().fill().center().gap(16.0).bg(t.bg),
            |ui| {
                ui.text(&self.count.to_string(), TextStyle::new(56.0).color(t.fg));
                ui.with(NodeSpec::row().gap(8.0), |ui| {
                    // A button takes the message it sends when clicked.
                    widgets::button(ui, "-1", Msg::Dec);
                    widgets::button(ui, "+1", Msg::Inc);
                    widgets::button(ui, "reset", Msg::Reset);
                });
            },
        );
    }

    /// Every event the frame produced comes here, one at a time, after
    /// the frame. A click carries the button's message.
    fn on_event(&mut self, ev: UiEvent) {
        match ev.message::<Msg>() {
            Some(Msg::Inc) => self.count += 1,
            Some(Msg::Dec) => self.count -= 1,
            Some(Msg::Reset) => self.count = 0,
            // Not ours: a resize, a focus change, a window event.
            None => {}
        }
    }
}

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

ev.message::<Msg>() is Some when the event carries one of your messages, and None when it is something else the frame produced — a resize, a window losing focus. Match exhaustively and the compiler tells you when you add a variant and forget to handle it.

on_event runs after the frame, once per event, in order. It never runs during view. So the model changes between frames, never in the middle of one.

Why data, not callbacks

A closure on the button would be shorter. But a closure is opaque: it cannot be logged, replayed, tested or sent across a language boundary. A message can. The devtools show every message as it arrives. A test asserts on them. Node, Lua and C receive the same maps.

Try this

  • Add Msg::Double. The compiler will point at the match.
  • Put .on_click(Msg::Inc) on the root column instead of a button (NodeSpec::column().fill().on_click(Msg::Inc)). Now the whole window is a button. Any box can carry a message.
  • Open the devtools (.devtools(true) on the launcher) and watch the events tab as you click.

Where this is decided