qframe/runtime/app.rs
1//! The application trait.
2
3use super::clipboard::ClipboardEvent;
4use super::command::Command;
5use super::frame_limit::FrameLimit;
6use super::termination::Termination;
7use crate::geometry::Size;
8use crate::graphics::Graphics;
9use crate::widget::View;
10
11/// An application built with quvyta-framework: data, a function that draws it and a function that
12/// changes it.
13///
14/// An application implements this trait and runs in a [`Runtime`](super::Runtime), or in a
15/// [`Harness`](super::Harness) for tests.
16///
17/// ```
18/// use qframe::prelude::*;
19///
20/// struct Counter {
21/// value: i32,
22/// }
23///
24/// #[derive(Clone)]
25/// enum Msg {
26/// Increment,
27/// }
28///
29/// impl App for Counter {
30/// type Msg = Msg;
31///
32/// fn update(&mut self, msg: Msg) -> Command<Msg> {
33/// match msg {
34/// Msg::Increment => self.value += 1,
35/// }
36/// Command::none()
37/// }
38///
39/// fn view(&self, ui: &mut View<'_, Msg>) {
40/// ui.column(|ui| {
41/// ui.add(Text::new(format!("Value: {}", self.value)));
42/// ui.add(Button::new("Increment").on_press(Msg::Increment));
43/// });
44/// }
45/// }
46///
47/// let mut app = Harness::new(Counter { value: 0 }, 30, 4);
48/// app.press("tab").press("enter");
49/// assert!(app.screen().contains("Value: 1"));
50/// ```
51///
52/// # Lifecycle
53///
54/// Besides `update` and `view`, five optional hooks follow the application through its life.
55/// Each has a default, so an application implements only the ones it needs:
56///
57/// 1. [`App::resized`] hears the size of the screen: first when the application starts, then
58/// whenever it changes.
59/// 2. [`App::graphics`] hears the way the terminal draws pictures, right after that first size
60/// and whenever it changes, so a picture is decoded at the size it will be shown.
61/// 3. [`App::init`] runs once, right after the first size and graphics, before the first frame
62/// is built.
63/// 4. [`App::before_quit`] is asked whenever the runtime is about to quit on the user's behalf.
64/// 5. [`App::terminating`] hears that the system is ending the application: a `SIGTERM` or a
65/// `SIGHUP`, when the SSH connection or the terminal went away. It is the one chance to save.
66///
67/// The hooks that only report something ([`App::resized`], [`App::graphics`], [`App::before_quit`],
68/// [`App::terminating`], like
69/// [`App::action`] and [`App::clipboard`]) read the state and answer with a message, which then
70/// goes through `update` like every other; the one that starts work ([`App::init`]) returns a
71/// [`Command`] like `update` does. The [`Harness`](super::Harness) runs every hook exactly
72/// where the terminal runtime does, so a test sees what a user sees.
73///
74/// ```
75/// use qframe::prelude::*;
76///
77/// #[derive(Default)]
78/// struct Editor {
79/// size: Size,
80/// unsaved: bool,
81/// asking: bool,
82/// }
83///
84/// #[derive(Clone)]
85/// enum Msg {
86/// Resized(Size),
87/// AskBeforeQuit,
88/// Quit,
89/// }
90///
91/// impl App for Editor {
92/// type Msg = Msg;
93///
94/// fn init(&mut self) -> Command<Msg> {
95/// // The first key already reaches the list.
96/// Command::focus("files")
97/// }
98///
99/// fn resized(&self, size: Size) -> Option<Msg> {
100/// Some(Msg::Resized(size))
101/// }
102///
103/// fn before_quit(&self) -> Option<Msg> {
104/// self.unsaved.then_some(Msg::AskBeforeQuit)
105/// }
106///
107/// fn update(&mut self, msg: Msg) -> Command<Msg> {
108/// match msg {
109/// Msg::Resized(size) => self.size = size,
110/// Msg::AskBeforeQuit => self.asking = true,
111/// // Decided: this quit does not ask again.
112/// Msg::Quit => return Command::quit(),
113/// }
114/// Command::none()
115/// }
116///
117/// fn view(&self, ui: &mut View<'_, Msg>) {
118/// ui.add(List::new(["notes.md", "todo.md"].map(ListItem::new))).id("files");
119/// }
120/// }
121///
122/// let mut app = Harness::new(Editor { unsaved: true, ..Editor::default() }, 40, 6);
123/// assert!(app.is_focused("files"));
124/// assert_eq!(app.app().size, Size::new(40, 6));
125/// app.resize(30, 4);
126/// assert_eq!(app.app().size, Size::new(30, 4));
127/// app.press("ctrl+q");
128/// assert!(app.app().asking && !app.quit_requested());
129/// app.send(Msg::Quit);
130/// assert!(app.quit_requested());
131/// ```
132pub trait App: 'static {
133 /// Everything that can happen in the application.
134 type Msg: Send + 'static;
135
136 /// Applies a message and returns work for the runtime to do.
137 fn update(&mut self, msg: Self::Msg) -> Command<Self::Msg>;
138
139 /// Describes the screen. Runs after every change; must not do I/O.
140 fn view(&self, ui: &mut View<'_, Self::Msg>);
141
142 /// Turns an `[app]` keymap action into a message, e.g. `"save"` into `Msg::Save`.
143 fn action(&self, _name: &str) -> Option<Self::Msg> {
144 None
145 }
146
147 /// Runs once when the application starts and returns its first work: the focus the first key
148 /// should reach, a tick to start, a dialog to open, a file to read.
149 ///
150 /// It runs at the start of the first frame, after the first [`App::resized`] message and
151 /// before the view of that frame is built, so the first frame already shows what it
152 /// changed. A [`Command::focus`] it returns names a widget that is not on screen yet; focus
153 /// reaches it as soon as that first frame is painted, before the runtime reads any input,
154 /// and the frame is drawn again at once with the widget focused. The first key the user
155 /// presses therefore reaches the focused widget.
156 ///
157 /// The runtime calls it once per run, the [`Harness`](super::Harness) once when it is
158 /// created. The default does nothing.
159 fn init(&mut self) -> Command<Self::Msg> {
160 Command::none()
161 }
162
163 /// Hears the size of the screen, in columns and rows: when the application starts, before
164 /// [`App::init`], and afterwards whenever the terminal is resized. The message it returns
165 /// goes through [`App::update`], which is where work that needs the size starts, such as
166 /// [`Process::pty`](super::Process::pty) with the width and height the output will have.
167 ///
168 /// It is the size [`View::size`] reports: the terminal size of the frame about to be drawn.
169 /// The message is applied before that frame's view is built, so `update` and `view` never
170 /// disagree about it. A resize that ends at the size already reported is not reported
171 /// again. [`Harness::new`](super::Harness::new) reports the size it is given, and
172 /// [`Harness::resize`](super::Harness::resize) the new one.
173 ///
174 /// The default ignores the size.
175 fn resized(&self, _size: Size) -> Option<Self::Msg> {
176 None
177 }
178
179 /// Hears the way this terminal draws pictures, [`Env::graphics`](crate::env::Env::graphics):
180 /// when the application starts, after [`App::resized`] and before [`App::init`], and
181 /// afterwards whenever it changes, such as when the glyph mode is switched to ASCII or back
182 /// while the application runs, or when the terminal's kitty answer arrives late over a slow
183 /// link. The message it returns goes through [`App::update`], which is where a picture is
184 /// decoded at the size the terminal shows: about ten by twenty pixels a cell for
185 /// [`Graphics::Kitty`], one pixel wide and two tall for half blocks, and not at all where
186 /// [`Graphics::can_draw`] is false.
187 ///
188 /// Like the size, the message is applied before the frame whose view first sees the new
189 /// value is built, and a value already reported is not reported again.
190 /// [`Harness::set_graphics`](super::Harness::set_graphics),
191 /// [`Harness::set_depth`](super::Harness::set_depth) and
192 /// [`Harness::set_glyph_mode`](super::Harness::set_glyph_mode) report a change the way the
193 /// runtime does.
194 ///
195 /// The default ignores it.
196 fn graphics(&self, _graphics: Graphics) -> Option<Self::Msg> {
197 None
198 }
199
200 /// Asked whenever the runtime is about to quit on the user's behalf: the global `quit`
201 /// action of the keymap, however it was reached (its key, the command palette, a widget
202 /// that runs the action). `None` lets the runtime quit. A message keeps the application running and is
203 /// delivered through [`App::update`] instead, e.g. to ask "finish and quit, keep running or
204 /// cancel" first.
205 ///
206 /// Once the application has decided, it quits with [`Command::quit`], which is its own
207 /// decision and is never asked about. While an answer is pending the user may ask to quit
208 /// again, and the hook is asked again; it sees its own state and can, say, keep the
209 /// question it already shows.
210 ///
211 /// The default lets every quit through.
212 fn before_quit(&self) -> Option<Self::Msg> {
213 None
214 }
215
216 /// Hears that the system is ending the application, and why: see [`Termination`] for each
217 /// cause and the signal behind it. `None` quits at once. A message keeps the application
218 /// running and is delivered through [`App::update`] instead, which is where it saves and
219 /// then returns [`Command::quit`].
220 ///
221 /// The run ends in bounded time whatever the answer: after [`Termination::grace`] the runtime
222 /// quits without the application, and a second `SIGTERM` or `SIGINT` quits at once. After a
223 /// [`Termination::Hangup`] the terminal is usually gone, so nothing is drawn any more and a
224 /// dialog would wait for nobody; save without asking. Work of [`Command::perform`] and
225 /// tasks still run and deliver their messages until the run ends.
226 ///
227 /// The runtime tells the application once per cause: a hangup that repeats is not told
228 /// again, a hangup during a pending terminate is. [`Harness::terminate`](super::Harness::terminate)
229 /// simulates each cause in tests.
230 ///
231 /// The default answers a [`Termination::Terminate`] like a quit the user asked for, with
232 /// [`App::before_quit`], and quits at once on a [`Termination::Hangup`]. So an application
233 /// that implements neither hook quits cleanly on every signal, and one that asks before
234 /// quitting asks on a `SIGTERM` too.
235 ///
236 /// ```
237 /// use qframe::prelude::*;
238 /// use qframe::runtime::Termination;
239 ///
240 /// #[derive(Default)]
241 /// struct Timer {
242 /// running: bool,
243 /// saved: bool,
244 /// }
245 ///
246 /// #[derive(Clone)]
247 /// enum Msg {
248 /// SaveAndQuit,
249 /// }
250 ///
251 /// impl App for Timer {
252 /// type Msg = Msg;
253 ///
254 /// fn terminating(&self, _cause: Termination) -> Option<Msg> {
255 /// // Whether a person or the system ends it, a running timer is saved first.
256 /// self.running.then_some(Msg::SaveAndQuit)
257 /// }
258 ///
259 /// fn update(&mut self, msg: Msg) -> Command<Msg> {
260 /// match msg {
261 /// Msg::SaveAndQuit => {
262 /// self.saved = true;
263 /// Command::quit()
264 /// }
265 /// }
266 /// }
267 ///
268 /// fn view(&self, ui: &mut View<'_, Msg>) {
269 /// ui.add(Text::new("25:00"));
270 /// }
271 /// }
272 ///
273 /// let mut app = Harness::new(Timer { running: true, ..Timer::default() }, 20, 3);
274 /// app.terminate(Termination::Hangup);
275 /// assert!(app.app().saved && app.quit_requested());
276 /// ```
277 fn terminating(&self, cause: Termination) -> Option<Self::Msg> {
278 match cause {
279 Termination::Terminate => self.before_quit(),
280 Termination::Hangup => None,
281 }
282 }
283
284 /// How many frames a second the runtime draws at most; see [`FrameLimit`].
285 ///
286 /// Asked before every frame, so an application may answer from its own state, such as a
287 /// setting the user changed. The default draws 60 frames a second locally and 20 over a
288 /// remote connection. The frames the application's own work causes are merged, and so are
289 /// the pointer's motions and the wheel; a frame that answers a key, a paste, a press or a
290 /// release is never held back.
291 ///
292 /// ```
293 /// # use qframe::prelude::*;
294 /// # use qframe::runtime::FrameLimit;
295 /// # struct Desktop;
296 /// # impl App for Desktop {
297 /// # type Msg = ();
298 /// # fn update(&mut self, (): ()) -> Command<()> {
299 /// # Command::none()
300 /// # }
301 /// // A desktop of terminal windows spends a slow link on ten frames a second.
302 /// fn frame_limit(&self) -> FrameLimit {
303 /// FrameLimit::per_second(60).remote(10)
304 /// }
305 /// # fn view(&self, ui: &mut View<'_, ()>) {
306 /// # ui.add(Text::new("windows"));
307 /// # }
308 /// # }
309 /// ```
310 fn frame_limit(&self) -> FrameLimit {
311 FrameLimit::default()
312 }
313
314 /// Hears about the clipboard: text a widget or a mouse selection copied, and pasted text
315 /// that no focused widget took. Copies the application asked for with
316 /// [`Command::copy`] are not reported, so answering a copy with a copy cannot loop.
317 fn clipboard(&self, _event: &ClipboardEvent) -> Option<Self::Msg> {
318 None
319 }
320}