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 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, four optional hooks follow the application through its life.
Each has a default, so an application implements only the ones it needs:
App::resizedhears the size of the screen: first when the application starts, then whenever it changes.App::initruns once, right after that first size, before the first frame is built.App::before_quitis asked whenever the runtime is about to quit on the user’s behalf.App::terminatinghears that the system is ending the application: aSIGTERMor aSIGHUP, 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::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§
Required Methods§
Provided Methods§
Sourcefn action(&self, _name: &str) -> Option<Self::Msg>
fn action(&self, _name: &str) -> Option<Self::Msg>
Turns an [app] keymap action into a message, e.g. "save" into Msg::Save.
Sourcefn init(&mut self) -> Command<Self::Msg>
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.
Sourcefn resized(&self, _size: Size) -> Option<Self::Msg>
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.
Sourcefn before_quit(&self) -> Option<Self::Msg>
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.
Sourcefn terminating(&self, cause: Termination) -> Option<Self::Msg>
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());Sourcefn frame_limit(&self) -> FrameLimit
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. Only frames the application’s own work causes are merged: a frame that answers input 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)
}Sourcefn clipboard(&self, _event: &ClipboardEvent) -> Option<Self::Msg>
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".