qframe/widget/view.rs
1//! Building the view tree.
2
3use std::time::Duration;
4
5use super::flex::{Axis, Flex};
6use super::idle::{IdleScope, IdleWatch};
7use super::mapped::Mapped;
8use super::place::Placed;
9use super::{Align, Container, FocusAction, Key, Length, Node, Widget};
10use crate::env::Env;
11use crate::geometry::{Padding, Rect, Size};
12use crate::keymap::Scope;
13
14/// Collects the nodes of one container while an application's `view` runs.
15pub struct View<'a, Msg> {
16 nodes: &'a mut Vec<Node<Msg>>,
17 env: &'a Env,
18 size: Size,
19 idle: &'a IdleScope<Msg>,
20}
21
22impl<'a, Msg: 'static> View<'a, Msg> {
23 pub(crate) fn new(nodes: &'a mut Vec<Node<Msg>>, env: &'a Env, size: Size, idle: &'a IdleScope<Msg>) -> Self {
24 Self { nodes, env, size, idle }
25 }
26
27 /// A builder for the children of a container inside this one: the same environment and size.
28 /// The idleness this view reads and declares watches in, for builders that make views of
29 /// their own.
30 pub(crate) fn idle_scope(&self) -> &'a IdleScope<Msg> {
31 self.idle
32 }
33
34 pub(crate) fn nested<'b>(&self, nodes: &'b mut Vec<Node<Msg>>) -> View<'b, Msg>
35 where
36 'a: 'b,
37 {
38 View::new(nodes, self.env, self.size, self.idle)
39 }
40
41 /// The environment: theme, icons, language and keymap.
42 #[must_use]
43 pub fn env(&self) -> &Env {
44 self.env
45 }
46
47 /// The room the application is drawing into: the whole terminal, in columns and rows.
48 ///
49 /// This is the value for an application's own layout decision, such as "below 48 columns,
50 /// fold the three columns into one": `if ui.size().width < 48 { .. } else { .. }` in `view`.
51 ///
52 /// The application's view fills the screen, so at the top of `view` this is exactly the
53 /// area it lays out. Every nested builder reports the same value: the children of `column`,
54 /// `row`, `stack`, `page` and `add_with`, the parts of an `AppShell`, `SidePanel`,
55 /// `Splitter` or `Popover`, the content of a `Modal` or other layer. The view is built
56 /// before layout divides the screen, so a container's own share is not known yet while its
57 /// children are being built; the number never pretends to be that share. A widget that
58 /// adapts to its own rectangle (a column that shortens its labels) does so in `measure` and
59 /// `paint`, which receive it.
60 ///
61 /// Reading it performs no I/O: it is the size of the frame the framework is about to draw,
62 /// which it already holds. After a terminal resize the next frame reports the new size, and
63 /// [`Harness::resize`](crate::runtime::Harness::resize) does the same in tests.
64 #[must_use]
65 pub fn size(&self) -> Size {
66 self.size
67 }
68
69 /// How long no input has reached this terminal: the time since the last key, mouse event or
70 /// paste the runtime received, or since the application started when none came yet.
71 ///
72 /// Everything the user does in this terminal counts: a key going down, repeating or coming
73 /// up, a mouse button, the wheel, the pointer moving over the window, a paste, and the end of
74 /// a [`Handoff`](crate::runtime::Handoff), because the program that had the terminal was
75 /// being used meanwhile. A terminal resize does not count: a window manager or a monitor
76 /// change resizes a window nobody is sitting at. Messages, background work and timers do not
77 /// count either; they are the application, not the user. Other programs and other terminals
78 /// are out of reach: this is idleness *here*, not idleness of the machine.
79 ///
80 /// Reading the value keeps it current on screen: while `view` reads it, the runtime draws
81 /// again each time it passes a whole second, and stops once `view` no longer reads it. A
82 /// view that shows minutes therefore redraws once a second while it shows them; one that
83 /// only needs to act after a silence uses [`View::on_idle`], which wakes the application
84 /// once, at that moment, without drawing in between.
85 ///
86 /// [`Harness::advance`](crate::runtime::Harness::advance) moves it forward in tests, and
87 /// every simulated input starts it again from zero.
88 #[must_use]
89 pub fn idle_for(&self) -> Duration {
90 self.idle.read.set(true);
91 self.idle.silent
92 }
93
94 /// Tells the application when no input has arrived for `after`, and when input comes back.
95 ///
96 /// `message(true)` is delivered once, at the moment the silence reaches `after`: the runtime
97 /// wakes for it even when nothing else happens, and does not draw in between. The first
98 /// input afterwards delivers `message(false)`, before that input reaches any widget, and
99 /// starts the next wait. What counts as input is listed at [`View::idle_for`].
100 ///
101 /// ```
102 /// use std::time::Duration;
103 ///
104 /// use qframe::prelude::*;
105 ///
106 /// #[derive(Default)]
107 /// struct Focus {
108 /// away: bool,
109 /// }
110 ///
111 /// impl App for Focus {
112 /// type Msg = bool;
113 /// fn update(&mut self, away: bool) -> Command<bool> {
114 /// self.away = away;
115 /// Command::none()
116 /// }
117 /// fn view(&self, ui: &mut View<'_, bool>) {
118 /// ui.on_idle(Duration::from_secs(300), |away| away);
119 /// ui.add(Text::new(if self.away { "away" } else { "working" }));
120 /// }
121 /// }
122 ///
123 /// let mut app = Harness::new(Focus::default(), 20, 1);
124 /// app.advance(Duration::from_secs(299));
125 /// assert!(app.screen().contains("working"));
126 /// app.advance(Duration::from_secs(1));
127 /// assert!(app.screen().contains("away"));
128 /// app.press("x");
129 /// assert!(app.screen().contains("working"));
130 /// ```
131 ///
132 /// Declare the watch in every frame it should stay active, like a widget: the runtime
133 /// answers the watches of the latest frame. One that is no longer declared is not told the
134 /// silence ended. A watch declared when the silence has already lasted `after` is told at
135 /// once. Watches with different `after` are independent, so an application can dim the
136 /// screen after one minute and pause a timer after five.
137 pub fn on_idle(&mut self, after: Duration, message: impl Fn(bool) -> Msg + 'static) {
138 self.idle.watches.borrow_mut().push(IdleWatch { after, message: Box::new(message) });
139 }
140
141 /// Adds a widget.
142 pub fn add<W: Widget<Msg>>(&mut self, widget: W) -> NodeMut<'_, Msg> {
143 let index = self.nodes.len();
144 self.nodes.push(Node::new(widget, index));
145 NodeMut { node: self.nodes.last_mut().expect("a node was just pushed") }
146 }
147
148 /// Adds a widget that contains other widgets, built by `build`.
149 pub fn add_with<W: Container<Msg>>(
150 &mut self,
151 mut widget: W,
152 build: impl FnOnce(&mut View<'_, Msg>),
153 ) -> NodeMut<'_, Msg> {
154 let mut children = Vec::new();
155 build(&mut self.nested(&mut children));
156 widget.set_children(children);
157 self.add(widget)
158 }
159
160 /// Adds a column whose children are built by `build`.
161 pub fn column(&mut self, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
162 self.container(Axis::Column, build)
163 }
164
165 /// Adds a row whose children are built by `build`.
166 pub fn row(&mut self, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
167 self.container(Axis::Row, build)
168 }
169
170 /// Adds a stack: children are drawn on top of each other in the same area, later ones on top.
171 pub fn stack(&mut self, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
172 self.container(Axis::Stack, build)
173 }
174
175 /// Adds children at `rect`, for a stack whose children sit where the application says, such
176 /// as windows on a desktop.
177 ///
178 /// Inside a [`stack`](Self::stack), `rect` counts from the stack's top left corner, whatever
179 /// the stack's alignment: the children fill it, drawn on top of each other. A rectangle may
180 /// reach past the stack on any side, also to negative coordinates; what lies outside is not
181 /// drawn and takes no pointer. Children added later are drawn on top and get the pointer
182 /// first where they overlap, so the order of the calls is the stacking order. A placed child
183 /// may draw one cell past its right and bottom edges, where a window drops its shadow; that
184 /// cell never takes the pointer.
185 /// Outside a stack only the size of `rect` counts.
186 ///
187 /// Name every placed child whose position in the stack can change, as when a clicked window
188 /// comes to the front: `ui.place(rect, ..).id("htop")`. Its state, and a drag it is in the
189 /// middle of, follow the name.
190 ///
191 /// ```
192 /// use qframe::prelude::*;
193 ///
194 /// struct Desk;
195 ///
196 /// impl App for Desk {
197 /// type Msg = ();
198 /// fn update(&mut self, (): ()) -> Command<()> {
199 /// Command::none()
200 /// }
201 /// fn view(&self, ui: &mut View<'_, ()>) {
202 /// ui.stack(|ui| {
203 /// ui.place(Rect::new(2, 1, 6, 1), |ui| {
204 /// ui.add(Text::new("below"));
205 /// })
206 /// .id("first");
207 /// ui.place(Rect::new(6, 1, 5, 1), |ui| {
208 /// ui.add(Text::new("above"));
209 /// })
210 /// .id("second");
211 /// })
212 /// .fill();
213 /// }
214 /// }
215 ///
216 /// let app = Harness::new(Desk, 12, 2);
217 /// assert_eq!(app.screen(), "\n beloabove\n");
218 /// ```
219 pub fn place(&mut self, rect: Rect, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
220 let mut children = Vec::new();
221 build(&mut self.nested(&mut children));
222 self.add(Placed::new(rect, children)).width(Length::Cells(rect.width)).height(Length::Cells(rect.height))
223 }
224
225 /// Adds a column that remembers its widgets' state (focus, scroll, cursors) while it is not
226 /// shown. Give every page of a router its own `key`.
227 pub fn page(&mut self, key: impl Into<String>, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
228 let node = self.container(Axis::Column, build);
229 node.node.persistent = true;
230 node.node.key = Key::Named(key.into());
231 node.fill()
232 }
233
234 /// Adds a column whose children are built by `build` with messages of their own type
235 /// `Inner`, each converted by `map` on its way to the application. A screen with its own
236 /// messages writes its view for them, and the application places it in one line:
237 ///
238 /// ```
239 /// use qframe::prelude::*;
240 ///
241 /// mod search {
242 /// use qframe::prelude::*;
243 ///
244 /// #[derive(Clone)]
245 /// pub enum Msg {
246 /// Run,
247 /// }
248 ///
249 /// pub fn view(ui: &mut View<'_, Msg>) {
250 /// ui.add(Button::new("Search").on_press(Msg::Run));
251 /// }
252 /// }
253 ///
254 /// enum Msg {
255 /// Search(search::Msg),
256 /// }
257 ///
258 /// fn view(ui: &mut View<'_, Msg>) {
259 /// ui.map(Msg::Search, search::view).fill();
260 /// }
261 /// ```
262 ///
263 /// Everything the screen does inside arrives converted: the messages of its widgets and
264 /// handlers, the children of [`add_with`](Self::add_with) and nested containers, layers such
265 /// as a `Modal` and the widgets in them, overlays such as an open dropdown. Focus, memory and
266 /// ids work as for any column; [`Command::map`](crate::runtime::Command::map) converts the
267 /// commands the screen's `update` returns the same way.
268 pub fn map<Inner: 'static>(
269 &mut self,
270 map: impl Fn(Inner) -> Msg + 'static,
271 build: impl FnOnce(&mut View<'_, Inner>),
272 ) -> NodeMut<'_, Msg> {
273 let map = std::rc::Rc::new(map);
274 let mut children = Vec::new();
275 // The screen reads and watches the same silence as the application; what it read and the
276 // watches it declared are handed up, their messages converted like any other.
277 let idle = IdleScope::new(self.idle.silent);
278 build(&mut View::new(&mut children, self.env, self.size, &idle));
279 if idle.read.get() {
280 self.idle.read.set(true);
281 }
282 for watch in idle.watches.into_inner() {
283 let map = std::rc::Rc::clone(&map);
284 let message = watch.message;
285 self.idle
286 .watches
287 .borrow_mut()
288 .push(IdleWatch { after: watch.after, message: Box::new(move |away| map(message(away))) });
289 }
290 self.add(Mapped::new(children, move |inner| map(inner)))
291 }
292
293 /// Adds empty space that takes the room left in a row or column.
294 pub fn spacer(&mut self) -> NodeMut<'_, Msg> {
295 self.container(Axis::Stack, |_| {}).fill()
296 }
297
298 fn container(&mut self, axis: Axis, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
299 let mut children = Vec::new();
300 build(&mut self.nested(&mut children));
301 self.add(Flex::new(axis, children))
302 }
303}
304
305/// Adjusts the node just added. Every method changes the node in place, so the result can be
306/// ignored or chained.
307pub struct NodeMut<'a, Msg> {
308 node: &'a mut Node<Msg>,
309}
310
311impl<'a, Msg> NodeMut<'a, Msg> {
312 /// Names the node. Name widgets whose position among their siblings can change (list rows,
313 /// optional widgets) so their state and focus follow them.
314 pub fn id(self, name: impl Into<String>) -> Self {
315 self.node.key = Key::Named(name.into());
316 self
317 }
318
319 /// Sets the width.
320 pub fn width(self, width: Length) -> Self {
321 self.node.layout.width = width;
322 self
323 }
324
325 /// Sets the height.
326 pub fn height(self, height: Length) -> Self {
327 self.node.layout.height = height;
328 self
329 }
330
331 /// Takes all space left in both directions.
332 pub fn fill(self) -> Self {
333 self.width(Length::Fill(1)).height(Length::Fill(1))
334 }
335
336 /// Takes all width left.
337 pub fn fill_width(self) -> Self {
338 self.width(Length::Fill(1))
339 }
340
341 /// Takes all height left.
342 pub fn fill_height(self) -> Self {
343 self.height(Length::Fill(1))
344 }
345
346 /// Keeps `padding` free inside the node.
347 pub fn padding(self, padding: Padding) -> Self {
348 self.node.layout.padding = padding;
349 self
350 }
351
352 /// Leaves `cells` between the children of a row or column.
353 pub fn gap(self, cells: u16) -> Self {
354 self.node.layout.gap = cells;
355 self
356 }
357
358 /// Places children along the main axis of a row or column (both axes of a stack).
359 pub fn justify(self, align: Align) -> Self {
360 self.node.layout.justify = align;
361 self
362 }
363
364 /// Whether a mouse drag may select text in this node. Nothing is selectable unless asked:
365 /// `true` makes the node a selection region, so a drag that starts inside it selects text
366 /// within the node only (widgets such as `CodeView` and `Markdown` are regions by
367 /// themselves). `false` keeps selection out of the node and everything inside it, also out
368 /// of regions within it, e.g. for a secret shown inside a selectable log.
369 pub fn selectable(self, selectable: bool) -> Self {
370 self.node.selectable = Some(selectable);
371 self
372 }
373
374 /// Places children across the main axis of a row or column.
375 pub fn align(self, align: Align) -> Self {
376 self.node.layout.align = align;
377 self
378 }
379}
380
381impl<Msg: Clone + 'static> NodeMut<'_, Msg> {
382 /// While keyboard focus is on this node or inside it, a key bound to the keymap action
383 /// `action` of `scope` sends `message` instead of reaching [`App::action`](crate::runtime::App::action).
384 ///
385 /// This is how an application tells where a shortcut was pressed. With focus elsewhere the
386 /// same key reaches `App::action` as usual, so one key can mean two things: leave a
387 /// terminal while inside it, go back into it from outside. The focus in force when the key
388 /// arrives decides, however it got there (`tab`, a click, [`Command::focus`](crate::runtime::Command::focus)),
389 /// so nothing has to be tracked in application state.
390 ///
391 /// The innermost node that answers the action wins. The key must first get past the focused
392 /// widgets: a widget that uses it (a text field typing a character) keeps it, and a
393 /// [`Terminal`](crate::widgets::Terminal) lets it out only for actions named with its
394 /// `pass_through`. The actions the runtime owns (`quit`, `focus-next`, `focus-prev`,
395 /// `debug`, `copy`, `paste`, `toggle-panel`) are not answered here. Call once per action.
396 ///
397 /// ```
398 /// use qframe::env::Env;
399 /// use qframe::prelude::*;
400 /// use qframe::widgets::TextInput;
401 ///
402 /// #[derive(Clone, Debug, PartialEq)]
403 /// enum Msg {
404 /// Leave,
405 /// Enter,
406 /// }
407 ///
408 /// struct Editor;
409 ///
410 /// impl App for Editor {
411 /// type Msg = Msg;
412 ///
413 /// fn update(&mut self, msg: Msg) -> Command<Msg> {
414 /// match msg {
415 /// Msg::Leave => Command::focus("files"),
416 /// Msg::Enter => Command::focus("note"),
417 /// }
418 /// }
419 ///
420 /// fn view(&self, ui: &mut View<'_, Msg>) {
421 /// ui.add(List::new(["notes.md", "todo.md"].map(ListItem::new))).id("files");
422 /// ui.add(TextInput::new("")).id("note").on_action(Scope::App, "switch", Msg::Leave);
423 /// }
424 ///
425 /// // Reached only while focus is outside the note.
426 /// fn action(&self, name: &str) -> Option<Msg> {
427 /// (name == "switch").then_some(Msg::Enter)
428 /// }
429 /// }
430 ///
431 /// let mut env = Env::builtin();
432 /// env.keymap_mut().bind(Scope::App, "switch", &["alt+s".parse().unwrap()]);
433 /// let mut app = Harness::with_env(Editor, env, 30, 3);
434 /// app.press("alt+s");
435 /// assert!(app.is_focused("note"));
436 /// app.press("alt+s");
437 /// assert!(app.is_focused("files"));
438 /// ```
439 pub fn on_action(self, scope: Scope, action: impl Into<String>, message: Msg) -> Self {
440 self.node.actions.push(FocusAction {
441 scope,
442 action: action.into(),
443 message: Box::new(move || message.clone()),
444 });
445 self
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use std::cell::RefCell;
452
453 use crate::geometry::Size;
454 use crate::runtime::{App, Command, Harness};
455 use crate::widget::{Length, View};
456 use crate::widgets::{Modal, Text};
457
458 /// Records the size every builder of its view saw: the root, a nested column and a row with
459 /// a fixed width inside it, and the content of a modal layer.
460 #[derive(Default)]
461 struct Probe {
462 seen: RefCell<Vec<Size>>,
463 }
464
465 impl Probe {
466 fn take(&self) -> Vec<Size> {
467 std::mem::take(&mut *self.seen.borrow_mut())
468 }
469 }
470
471 impl App for Probe {
472 type Msg = ();
473
474 fn update(&mut self, (): ()) -> Command<()> {
475 Command::none()
476 }
477
478 fn view(&self, ui: &mut View<'_, ()>) {
479 self.seen.borrow_mut().push(ui.size());
480 ui.column(|ui| {
481 self.seen.borrow_mut().push(ui.size());
482 ui.row(|ui| {
483 self.seen.borrow_mut().push(ui.size());
484 ui.add(Text::new("probe"));
485 })
486 .width(Length::Cells(10));
487 });
488 ui.add_with(Modal::new(), |ui| {
489 self.seen.borrow_mut().push(ui.size());
490 ui.add(Text::new("layer"));
491 });
492 }
493 }
494
495 #[test]
496 fn every_builder_of_the_view_sees_the_terminal_size() {
497 let mut harness = Harness::new(Probe::default(), 83, 27);
498 let seen = harness.app().take();
499 assert!(seen.len() >= 4, "{seen:?}");
500 assert!(seen.iter().all(|size| *size == Size::new(83, 27)), "{seen:?}");
501
502 harness.resize(31, 9);
503 let seen = harness.app().take();
504 assert!(seen.len() >= 4, "{seen:?}");
505 assert!(seen.iter().all(|size| *size == Size::new(31, 9)), "{seen:?}");
506
507 harness.resize(0, 0);
508 let seen = harness.app().take();
509 assert!(seen.len() >= 4, "{seen:?}");
510 assert!(seen.iter().all(|size| *size == Size::new(0, 0)), "{seen:?}");
511 }
512
513 /// Three columns side by side from 48 columns up; below that the groups fold into a strip
514 /// above the list and the detail is left out.
515 struct Folding;
516
517 impl App for Folding {
518 type Msg = ();
519
520 fn update(&mut self, (): ()) -> Command<()> {
521 Command::none()
522 }
523
524 fn view(&self, ui: &mut View<'_, ()>) {
525 if ui.size().width < 48 {
526 ui.column(|ui| {
527 ui.add(Text::new("groups"));
528 ui.add(Text::new("items"));
529 })
530 .fill();
531 } else {
532 ui.row(|ui| {
533 ui.add(Text::new("groups"));
534 ui.add(Text::new("items"));
535 ui.add(Text::new("detail"));
536 })
537 .gap(2)
538 .fill();
539 }
540 }
541 }
542
543 #[test]
544 fn an_application_folds_its_layout_below_a_width() {
545 let mut harness = Harness::new(Folding, 120, 10);
546 assert_eq!(harness.screen().lines().next(), Some("groups items detail"), "{}", harness.screen());
547
548 harness.resize(40, 10);
549 let screen = harness.screen();
550 let lines: Vec<&str> = screen.lines().collect();
551 assert_eq!(lines.get(..2), Some(&["groups", "items"][..]), "{screen}");
552 assert!(!screen.contains("detail"), "{screen}");
553
554 harness.resize(120, 10);
555 assert_eq!(harness.screen().lines().next(), Some("groups items detail"), "{}", harness.screen());
556 }
557}