Effects, the clock and the world
At the end of this chapter, the window’s title counts seconds, a button copies to the clipboard, another pastes, and a third closes the window.
So far the app was pure: a model, a view of it, events that change it. Real apps touch the world — the clipboard, a file, a socket, a timer. kui has three doors for that, and each keeps the loop’s shape.
Door one: the handler gets the core
on_event is &mut self and an event. Its sibling on_event_with
also gets the core of the window the event came from. The core is
where the clipboard, focus and scrolling live:
//! Step 9 — the world outside the frame. Builds on step 8 with the three
//! doors out of the pure loop: the clipboard (a handler that gets the
//! window's core), a thread that wakes the loop (a clock), and the
//! window itself (its title, and closing it). Chapter 10 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_09_effects
use std::sync::{Arc, Mutex};
use std::time::Duration;
use kui_native::widgets;
use kui_native::{
Align, App, Core, Message, NodeSpec, TextStyle, Ui, UiEvent, Waker, WindowCommand,
};
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
Copy,
Paste,
Quit,
/// The root sink's tag: a paste's text arrives under it.
Sink,
}
struct Effects {
/// Seconds since launch, written by a thread.
ticks: Arc<Mutex<u64>>,
pasted: String,
quit: bool,
}
impl App for Effects {
/// Once, before the window opens. The loop sleeps between events, so
/// a thread with news calls `wake()` and the loop draws a frame.
fn setup(&mut self, waker: Waker) {
let ticks = Arc::clone(&self.ticks);
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_secs(1));
*ticks.lock().unwrap() += 1;
waker.wake();
}
});
}
fn view(&mut self, ui: &mut Ui<'_>) {
let t = ui.theme();
let ticks = *self.ticks.lock().unwrap();
// The window is declared like everything else: say the title
// every frame, and the runner applies it when it changes.
ui.window_title(&format!("Effects · {ticks}s"));
if self.quit {
ui.window_command(WindowCommand::Close(ui.env().window.id));
}
// A key sink with focus, so a paste has somewhere to land.
let sink = ui.with_keyed(
"app",
NodeSpec::column()
.fill()
.pad(20.0)
.gap(12.0)
.cross_align(Align::Start)
.bg(t.bg)
.on_key(Msg::Sink),
|ui| {
ui.text(
&format!("up for {ticks}s"),
TextStyle::new(24.0).color(t.fg),
);
ui.with(NodeSpec::row().gap(8.0), |ui| {
widgets::button(ui, "Copy the uptime", Msg::Copy);
widgets::button(ui, "Paste", Msg::Paste);
widgets::button(ui, "Quit", Msg::Quit);
});
ui.text(
&format!(
"pasted: {}",
if self.pasted.is_empty() {
"nothing yet"
} else {
&self.pasted
}
),
TextStyle::new(13.0).color(t.muted),
);
},
);
ui.take_key_focus(sink);
}
/// `on_event` with the core of the window the event came from. What
/// an app does about an event beyond its model — the clipboard, a
/// paste, focus — is a call on it here.
fn on_event_with(&mut self, ev: UiEvent, core: &mut Core) {
match ev.message::<Msg>() {
Some(Msg::Copy) => {
let ticks = *self.ticks.lock().unwrap();
core.set_clipboard(format!("{ticks}s"), None);
}
// The core cannot read the clipboard; the host can. Ask, and
// the text comes back as an event on the focused sink.
Some(Msg::Paste) => core.request_paste(),
Some(Msg::Sink) if ev.kind() == Some("text") => {
if let Some(text) = ev.payload.get_str("text") {
self.pasted = text.to_string();
}
}
Some(Msg::Quit) => self.quit = true,
_ => {}
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
kui_native::app("Effects").size(420.0, 220.0).run(Effects {
ticks: Arc::new(Mutex::new(0)),
pasted: String::new(),
quit: false,
})
}
core.set_clipboard(text, None) writes. There is no read_clipboard,
because the core never reads the OS clipboard itself — the host does.
core.request_paste() asks, and the text comes back as an event: a
text event on the focused key sink. That is why the root of this app
is a sink with focus, as in the keyboard chapter.
Override on_event_with and the default on_event is not called.
Override only on_event and nothing changes.
Door two: a thread wakes the loop
The loop sleeps between events. A thread that has news — a socket read,
a timer — cannot draw, but it can wake the loop, and then view runs:
//! Step 9 — the world outside the frame. Builds on step 8 with the three
//! doors out of the pure loop: the clipboard (a handler that gets the
//! window's core), a thread that wakes the loop (a clock), and the
//! window itself (its title, and closing it). Chapter 10 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_09_effects
use std::sync::{Arc, Mutex};
use std::time::Duration;
use kui_native::widgets;
use kui_native::{
Align, App, Core, Message, NodeSpec, TextStyle, Ui, UiEvent, Waker, WindowCommand,
};
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
Copy,
Paste,
Quit,
/// The root sink's tag: a paste's text arrives under it.
Sink,
}
struct Effects {
/// Seconds since launch, written by a thread.
ticks: Arc<Mutex<u64>>,
pasted: String,
quit: bool,
}
impl App for Effects {
/// Once, before the window opens. The loop sleeps between events, so
/// a thread with news calls `wake()` and the loop draws a frame.
fn setup(&mut self, waker: Waker) {
let ticks = Arc::clone(&self.ticks);
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_secs(1));
*ticks.lock().unwrap() += 1;
waker.wake();
}
});
}
fn view(&mut self, ui: &mut Ui<'_>) {
let t = ui.theme();
let ticks = *self.ticks.lock().unwrap();
// The window is declared like everything else: say the title
// every frame, and the runner applies it when it changes.
ui.window_title(&format!("Effects · {ticks}s"));
if self.quit {
ui.window_command(WindowCommand::Close(ui.env().window.id));
}
// A key sink with focus, so a paste has somewhere to land.
let sink = ui.with_keyed(
"app",
NodeSpec::column()
.fill()
.pad(20.0)
.gap(12.0)
.cross_align(Align::Start)
.bg(t.bg)
.on_key(Msg::Sink),
|ui| {
ui.text(
&format!("up for {ticks}s"),
TextStyle::new(24.0).color(t.fg),
);
ui.with(NodeSpec::row().gap(8.0), |ui| {
widgets::button(ui, "Copy the uptime", Msg::Copy);
widgets::button(ui, "Paste", Msg::Paste);
widgets::button(ui, "Quit", Msg::Quit);
});
ui.text(
&format!(
"pasted: {}",
if self.pasted.is_empty() {
"nothing yet"
} else {
&self.pasted
}
),
TextStyle::new(13.0).color(t.muted),
);
},
);
ui.take_key_focus(sink);
}
/// `on_event` with the core of the window the event came from. What
/// an app does about an event beyond its model — the clipboard, a
/// paste, focus — is a call on it here.
fn on_event_with(&mut self, ev: UiEvent, core: &mut Core) {
match ev.message::<Msg>() {
Some(Msg::Copy) => {
let ticks = *self.ticks.lock().unwrap();
core.set_clipboard(format!("{ticks}s"), None);
}
// The core cannot read the clipboard; the host can. Ask, and
// the text comes back as an event on the focused sink.
Some(Msg::Paste) => core.request_paste(),
Some(Msg::Sink) if ev.kind() == Some("text") => {
if let Some(text) = ev.payload.get_str("text") {
self.pasted = text.to_string();
}
}
Some(Msg::Quit) => self.quit = true,
_ => {}
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
kui_native::app("Effects").size(420.0, 220.0).run(Effects {
ticks: Arc::new(Mutex::new(0)),
pasted: String::new(),
quit: false,
})
}
setup is called once before the window opens, with a Waker. Clone
it into as many threads as you like. waker.wake() is cheap and safe
at any rate; wakes coalesce into the next turn of the loop.
The thread writes into shared state; the view reads it. There is no
channel into on_event — the model is the channel.
Door three: the window is declared
//! Step 9 — the world outside the frame. Builds on step 8 with the three
//! doors out of the pure loop: the clipboard (a handler that gets the
//! window's core), a thread that wakes the loop (a clock), and the
//! window itself (its title, and closing it). Chapter 10 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_09_effects
use std::sync::{Arc, Mutex};
use std::time::Duration;
use kui_native::widgets;
use kui_native::{
Align, App, Core, Message, NodeSpec, TextStyle, Ui, UiEvent, Waker, WindowCommand,
};
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
Copy,
Paste,
Quit,
/// The root sink's tag: a paste's text arrives under it.
Sink,
}
struct Effects {
/// Seconds since launch, written by a thread.
ticks: Arc<Mutex<u64>>,
pasted: String,
quit: bool,
}
impl App for Effects {
/// Once, before the window opens. The loop sleeps between events, so
/// a thread with news calls `wake()` and the loop draws a frame.
fn setup(&mut self, waker: Waker) {
let ticks = Arc::clone(&self.ticks);
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_secs(1));
*ticks.lock().unwrap() += 1;
waker.wake();
}
});
}
fn view(&mut self, ui: &mut Ui<'_>) {
let t = ui.theme();
let ticks = *self.ticks.lock().unwrap();
// The window is declared like everything else: say the title
// every frame, and the runner applies it when it changes.
ui.window_title(&format!("Effects · {ticks}s"));
if self.quit {
ui.window_command(WindowCommand::Close(ui.env().window.id));
}
// A key sink with focus, so a paste has somewhere to land.
let sink = ui.with_keyed(
"app",
NodeSpec::column()
.fill()
.pad(20.0)
.gap(12.0)
.cross_align(Align::Start)
.bg(t.bg)
.on_key(Msg::Sink),
|ui| {
ui.text(
&format!("up for {ticks}s"),
TextStyle::new(24.0).color(t.fg),
);
ui.with(NodeSpec::row().gap(8.0), |ui| {
widgets::button(ui, "Copy the uptime", Msg::Copy);
widgets::button(ui, "Paste", Msg::Paste);
widgets::button(ui, "Quit", Msg::Quit);
});
ui.text(
&format!(
"pasted: {}",
if self.pasted.is_empty() {
"nothing yet"
} else {
&self.pasted
}
),
TextStyle::new(13.0).color(t.muted),
);
},
);
ui.take_key_focus(sink);
}
/// `on_event` with the core of the window the event came from. What
/// an app does about an event beyond its model — the clipboard, a
/// paste, focus — is a call on it here.
fn on_event_with(&mut self, ev: UiEvent, core: &mut Core) {
match ev.message::<Msg>() {
Some(Msg::Copy) => {
let ticks = *self.ticks.lock().unwrap();
core.set_clipboard(format!("{ticks}s"), None);
}
// The core cannot read the clipboard; the host can. Ask, and
// the text comes back as an event on the focused sink.
Some(Msg::Paste) => core.request_paste(),
Some(Msg::Sink) if ev.kind() == Some("text") => {
if let Some(text) = ev.payload.get_str("text") {
self.pasted = text.to_string();
}
}
Some(Msg::Quit) => self.quit = true,
_ => {}
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
kui_native::app("Effects").size(420.0, 220.0).run(Effects {
ticks: Arc::new(Mutex::new(0)),
pasted: String::new(),
quit: false,
})
}
ui.window_title(..) is said every frame, and applied when it changes.
ui.window_command(WindowCommand::Close(id)) asks the runner to close
a window; ui.env().window.id is this one’s id. A second window is
declared the same way, with ui.window(name, ..);
docs/design.md has the multi-window rules under
Window chrome is data.
The messages
//! Step 9 — the world outside the frame. Builds on step 8 with the three
//! doors out of the pure loop: the clipboard (a handler that gets the
//! window's core), a thread that wakes the loop (a clock), and the
//! window itself (its title, and closing it). Chapter 10 of the book.
//!
//! Run: cargo run -p kui-native --example tutorial_09_effects
use std::sync::{Arc, Mutex};
use std::time::Duration;
use kui_native::widgets;
use kui_native::{
Align, App, Core, Message, NodeSpec, TextStyle, Ui, UiEvent, Waker, WindowCommand,
};
#[derive(Message, Clone, Debug, PartialEq)]
enum Msg {
Copy,
Paste,
Quit,
/// The root sink's tag: a paste's text arrives under it.
Sink,
}
struct Effects {
/// Seconds since launch, written by a thread.
ticks: Arc<Mutex<u64>>,
pasted: String,
quit: bool,
}
impl App for Effects {
/// Once, before the window opens. The loop sleeps between events, so
/// a thread with news calls `wake()` and the loop draws a frame.
fn setup(&mut self, waker: Waker) {
let ticks = Arc::clone(&self.ticks);
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_secs(1));
*ticks.lock().unwrap() += 1;
waker.wake();
}
});
}
fn view(&mut self, ui: &mut Ui<'_>) {
let t = ui.theme();
let ticks = *self.ticks.lock().unwrap();
// The window is declared like everything else: say the title
// every frame, and the runner applies it when it changes.
ui.window_title(&format!("Effects · {ticks}s"));
if self.quit {
ui.window_command(WindowCommand::Close(ui.env().window.id));
}
// A key sink with focus, so a paste has somewhere to land.
let sink = ui.with_keyed(
"app",
NodeSpec::column()
.fill()
.pad(20.0)
.gap(12.0)
.cross_align(Align::Start)
.bg(t.bg)
.on_key(Msg::Sink),
|ui| {
ui.text(
&format!("up for {ticks}s"),
TextStyle::new(24.0).color(t.fg),
);
ui.with(NodeSpec::row().gap(8.0), |ui| {
widgets::button(ui, "Copy the uptime", Msg::Copy);
widgets::button(ui, "Paste", Msg::Paste);
widgets::button(ui, "Quit", Msg::Quit);
});
ui.text(
&format!(
"pasted: {}",
if self.pasted.is_empty() {
"nothing yet"
} else {
&self.pasted
}
),
TextStyle::new(13.0).color(t.muted),
);
},
);
ui.take_key_focus(sink);
}
/// `on_event` with the core of the window the event came from. What
/// an app does about an event beyond its model — the clipboard, a
/// paste, focus — is a call on it here.
fn on_event_with(&mut self, ev: UiEvent, core: &mut Core) {
match ev.message::<Msg>() {
Some(Msg::Copy) => {
let ticks = *self.ticks.lock().unwrap();
core.set_clipboard(format!("{ticks}s"), None);
}
// The core cannot read the clipboard; the host can. Ask, and
// the text comes back as an event on the focused sink.
Some(Msg::Paste) => core.request_paste(),
Some(Msg::Sink) if ev.kind() == Some("text") => {
if let Some(text) = ev.payload.get_str("text") {
self.pasted = text.to_string();
}
}
Some(Msg::Quit) => self.quit = true,
_ => {}
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
kui_native::app("Effects").size(420.0, 220.0).run(Effects {
ticks: Arc::new(Mutex::new(0)),
pasted: String::new(),
quit: false,
})
}
Try this
- Print something from
teardown. It runs once as the window goes. - Make the tick thread stop after 10 seconds. The loop keeps running; only the wakes stop.
- Copy, then paste into another app. Then paste from another app into this one.
Where this is decided
on_event_with,setupandteardown:kui_native::App.- Effects as data, the Node driver’s version of the same idea:
withEffectsin the Node package. - The reference examples:
features/waker.rs,features/clipboard.rs.