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