Skip to main content

App

Trait App 

Source
pub trait App: 'static {
    type Msg: Send + 'static;

    // Required methods
    fn update(&mut self, msg: Self::Msg) -> Command<Self::Msg>;
    fn view(&self, ui: &mut View<'_, Self::Msg>);

    // Provided methods
    fn action(&self, _name: &str) -> Option<Self::Msg> { ... }
    fn init(&mut self) -> Command<Self::Msg> { ... }
    fn resized(&self, _size: Size) -> Option<Self::Msg> { ... }
    fn graphics(&self, _graphics: Graphics) -> Option<Self::Msg> { ... }
    fn preferences(&self, _preferences: &Preferences) -> Option<Self::Msg> { ... }
    fn before_quit(&self) -> Option<Self::Msg> { ... }
    fn terminating(&self, cause: Termination) -> Option<Self::Msg> { ... }
    fn frame_limit(&self) -> FrameLimit { ... }
    fn clipboard(&self, _event: &ClipboardEvent) -> Option<Self::Msg> { ... }
}
Expand description

An application built with quvyta-framework: data, a function that draws it and a function that changes it.

An application implements this trait and runs in a Runtime, or in a Harness for tests.

use qframe::prelude::*;

struct Counter {
    value: i32,
}

#[derive(Clone)]
enum Msg {
    Increment,
}

impl App for Counter {
    type Msg = Msg;

    fn update(&mut self, msg: Msg) -> Command<Msg> {
        match msg {
            Msg::Increment => self.value += 1,
        }
        Command::none()
    }

    fn view(&self, ui: &mut View<'_, Msg>) {
        ui.column(|ui| {
            ui.add(Text::new(format!("Value: {}", self.value)));
            ui.add(Button::new("Increment").on_press(Msg::Increment));
        });
    }
}

let mut app = Harness::new(Counter { value: 0 }, 30, 4);
app.press("tab").press("enter");
assert!(app.screen().contains("Value: 1"));

§Lifecycle

Besides update and view, six optional hooks follow the application through its life. Each has a default, so an application implements only the ones it needs:

  1. App::resized hears the size of the screen: first when the application starts, then whenever it changes.
  2. App::graphics hears the way the terminal draws pictures, right after that first size and whenever it changes, so a picture is decoded at the size it will be shown.
  3. App::preferences hears the ecosystem’s shared preferences of an application started with Runtime::member: right after the graphics, and whenever another application changes them while this one runs.
  4. App::init runs once, right after the first size, graphics and preferences, before the first frame is built.
  5. App::before_quit is asked whenever the runtime is about to quit on the user’s behalf.
  6. App::terminating hears that the system is ending the application: a SIGTERM or a SIGHUP, when the SSH connection or the terminal went away. It is the one chance to save.

The hooks that only report something (App::resized, App::graphics, App::preferences, App::before_quit, App::terminating, like App::action and App::clipboard) read the state and answer with a message, which then goes through update like every other; the one that starts work (App::init) returns a Command like update does. The Harness runs every hook exactly where the terminal runtime does, so a test sees what a user sees.

use qframe::prelude::*;

#[derive(Default)]
struct Editor {
    size: Size,
    unsaved: bool,
    asking: bool,
}

#[derive(Clone)]
enum Msg {
    Resized(Size),
    AskBeforeQuit,
    Quit,
}

impl App for Editor {
    type Msg = Msg;

    fn init(&mut self) -> Command<Msg> {
        // The first key already reaches the list.
        Command::focus("files")
    }

    fn resized(&self, size: Size) -> Option<Msg> {
        Some(Msg::Resized(size))
    }

    fn before_quit(&self) -> Option<Msg> {
        self.unsaved.then_some(Msg::AskBeforeQuit)
    }

    fn update(&mut self, msg: Msg) -> Command<Msg> {
        match msg {
            Msg::Resized(size) => self.size = size,
            Msg::AskBeforeQuit => self.asking = true,
            // Decided: this quit does not ask again.
            Msg::Quit => return Command::quit(),
        }
        Command::none()
    }

    fn view(&self, ui: &mut View<'_, Msg>) {
        ui.add(List::new(["notes.md", "todo.md"].map(ListItem::new))).id("files");
    }
}

let mut app = Harness::new(Editor { unsaved: true, ..Editor::default() }, 40, 6);
assert!(app.is_focused("files"));
assert_eq!(app.app().size, Size::new(40, 6));
app.resize(30, 4);
assert_eq!(app.app().size, Size::new(30, 4));
app.press("ctrl+q");
assert!(app.app().asking && !app.quit_requested());
app.send(Msg::Quit);
assert!(app.quit_requested());

Required Associated Types§

Source

type Msg: Send + 'static

Everything that can happen in the application.

Required Methods§

Source

fn update(&mut self, msg: Self::Msg) -> Command<Self::Msg>

Applies a message and returns work for the runtime to do.

Source

fn view(&self, ui: &mut View<'_, Self::Msg>)

Describes the screen. Runs after every change; must not do I/O.

Provided Methods§

Source

fn action(&self, _name: &str) -> Option<Self::Msg>

Turns an [app] keymap action into a message, e.g. "save" into Msg::Save.

Source

fn init(&mut self) -> Command<Self::Msg>

Runs once when the application starts and returns its first work: the focus the first key should reach, a tick to start, a dialog to open, a file to read.

It runs at the start of the first frame, after the first App::resized message and before the view of that frame is built, so the first frame already shows what it changed. A Command::focus it returns names a widget that is not on screen yet; focus reaches it as soon as that first frame is painted, before the runtime reads any input, and the frame is drawn again at once with the widget focused. The first key the user presses therefore reaches the focused widget.

The runtime calls it once per run, the Harness once when it is created. The default does nothing.

Source

fn resized(&self, _size: Size) -> Option<Self::Msg>

Hears the size of the screen, in columns and rows: when the application starts, before App::init, and afterwards whenever the terminal is resized. The message it returns goes through App::update, which is where work that needs the size starts, such as Process::pty with the width and height the output will have.

It is the size View::size reports: the terminal size of the frame about to be drawn. The message is applied before that frame’s view is built, so update and view never disagree about it. A resize that ends at the size already reported is not reported again. Harness::new reports the size it is given, and Harness::resize the new one.

The default ignores the size.

Source

fn graphics(&self, _graphics: Graphics) -> Option<Self::Msg>

Hears the way this terminal draws pictures, Env::graphics: when the application starts, after App::resized and before App::init, and afterwards whenever it changes, such as when the glyph mode is switched to ASCII or back while the application runs, or when the terminal’s kitty answer arrives late over a slow link. The message it returns goes through App::update, which is where a picture is decoded at the size the terminal shows: about ten by twenty pixels a cell for Graphics::Kitty, one pixel wide and two tall for half blocks, and not at all where Graphics::can_draw is false.

Like the size, the message is applied before the frame whose view first sees the new value is built, and a value already reported is not reported again. Harness::set_graphics, Harness::set_depth and Harness::set_glyph_mode report a change the way the runtime does.

The default ignores it.

Source

fn preferences(&self, _preferences: &Preferences) -> Option<Self::Msg>

Hears the ecosystem’s shared preferences, for an application started as a member of an ecosystem with Runtime::member: once when it starts, after App::graphics and before App::init, with what it starts with; and afterwards whenever the ecosystem’s shared file or the application’s own file changes what they resolve to, such as when another application of the ecosystem switches the theme for all of them.

The runtime has already switched the screen by then: language, theme, icons and reduced motion as the ecosystem resolves them, and the pillar when the application’s own file changed it. Nothing has to be applied here. An application with a settings screen refreshes it here, so an open screen shows the new values: an Appearance section takes them with Appearance::refresh.

A file written again with what it already said is not reported, so an application that saves its own change hears at most what it saved, once, and never loops. An application started without Runtime::member is never told. Harness::member_in starts a test the same way, and Harness::poll_preferences reads the files again as the runtime does when they change.

use qframe::prelude::*;
use qframe::storage::{Ecosystem, Preferences, Scope, Shared};

#[derive(Default)]
struct Notes {
    theme: String,
}

enum Msg {
    Preferences(Preferences),
}

impl App for Notes {
    type Msg = Msg;

    fn preferences(&self, preferences: &Preferences) -> Option<Msg> {
        Some(Msg::Preferences(preferences.clone()))
    }

    fn update(&mut self, msg: Msg) -> Command<Msg> {
        match msg {
            Msg::Preferences(preferences) => self.theme = preferences.theme().value.clone(),
        }
        Command::none()
    }

    fn view(&self, ui: &mut View<'_, Msg>) {
        ui.add(Text::new(self.theme.clone()));
    }
}

let ecosystem = Ecosystem::QUVYTA;
let mut app = Harness::member_in(Notes::default(), ecosystem, &folder, "notes", 30, 3);
assert_eq!(app.app().theme, "monochrome");
// Another application switches every follower to amber.
ecosystem.set_in(&folder, "desk", Shared::Theme, "amber", Scope::Ecosystem).expect("saved");
app.poll_preferences();
assert_eq!(app.app().theme, "amber");
assert_eq!(app.env().theme().id(), "amber");
Source

fn before_quit(&self) -> Option<Self::Msg>

Asked whenever the runtime is about to quit on the user’s behalf: the global quit action of the keymap, however it was reached (its key, the command palette, a widget that runs the action). None lets the runtime quit. A message keeps the application running and is delivered through App::update instead, e.g. to ask “finish and quit, keep running or cancel” first.

Once the application has decided, it quits with Command::quit, which is its own decision and is never asked about. While an answer is pending the user may ask to quit again, and the hook is asked again; it sees its own state and can, say, keep the question it already shows.

The default lets every quit through.

Source

fn terminating(&self, cause: Termination) -> Option<Self::Msg>

Hears that the system is ending the application, and why: see Termination for each cause and the signal behind it. None quits at once. A message keeps the application running and is delivered through App::update instead, which is where it saves and then returns Command::quit.

The run ends in bounded time whatever the answer: after Termination::grace the runtime quits without the application, and a second SIGTERM or SIGINT quits at once. After a Termination::Hangup the terminal is usually gone, so nothing is drawn any more and a dialog would wait for nobody; save without asking. Work of Command::perform and tasks still run and deliver their messages until the run ends.

The runtime tells the application once per cause: a hangup that repeats is not told again, a hangup during a pending terminate is. Harness::terminate simulates each cause in tests.

The default answers a Termination::Terminate like a quit the user asked for, with App::before_quit, and quits at once on a Termination::Hangup. So an application that implements neither hook quits cleanly on every signal, and one that asks before quitting asks on a SIGTERM too.

use qframe::prelude::*;
use qframe::runtime::Termination;

#[derive(Default)]
struct Timer {
    running: bool,
    saved: bool,
}

#[derive(Clone)]
enum Msg {
    SaveAndQuit,
}

impl App for Timer {
    type Msg = Msg;

    fn terminating(&self, _cause: Termination) -> Option<Msg> {
        // Whether a person or the system ends it, a running timer is saved first.
        self.running.then_some(Msg::SaveAndQuit)
    }

    fn update(&mut self, msg: Msg) -> Command<Msg> {
        match msg {
            Msg::SaveAndQuit => {
                self.saved = true;
                Command::quit()
            }
        }
    }

    fn view(&self, ui: &mut View<'_, Msg>) {
        ui.add(Text::new("25:00"));
    }
}

let mut app = Harness::new(Timer { running: true, ..Timer::default() }, 20, 3);
app.terminate(Termination::Hangup);
assert!(app.app().saved && app.quit_requested());
Source

fn frame_limit(&self) -> FrameLimit

How many frames a second the runtime draws at most; see FrameLimit.

Asked before every frame, so an application may answer from its own state, such as a setting the user changed. The default draws 60 frames a second locally and 20 over a remote connection. The frames the application’s own work causes are merged, and so are the pointer’s motions and the wheel; a frame that answers a key, a paste, a press or a release is never held back.

// A desktop of terminal windows spends a slow link on ten frames a second.
fn frame_limit(&self) -> FrameLimit {
    FrameLimit::per_second(60).remote(10)
}
Source

fn clipboard(&self, _event: &ClipboardEvent) -> Option<Self::Msg>

Hears about the clipboard: text a widget or a mouse selection copied, and pasted text that no focused widget took. Copies the application asked for with Command::copy are not reported, so answering a copy with a copy cannot loop.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§