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

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