Skip to main content

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}