State and messages
At the end of this chapter, three buttons change a number.
The shape of an app
An app has three parts:
- A model: the state, held in your struct.
- A view: a function of the model that declares the frame.
- 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 thematch. - 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
#[derive(Message)]and its attributes:kui_deriveon docs.rs, anddocs/howto.md.- The full click payload, and every other event’s shape:
docs/props.md, events. - The reference counter, with a context menu:
examples/rust/apps/counter.rs.