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::{Align, Container, Key, Length, Node, Widget};
9use crate::env::Env;
10use crate::geometry::{Padding, Size};
11
12/// Collects the nodes of one container while an application's `view` runs.
13pub struct View<'a, Msg> {
14    nodes: &'a mut Vec<Node<Msg>>,
15    env: &'a Env,
16    size: Size,
17    idle: &'a IdleScope<Msg>,
18}
19
20impl<'a, Msg: 'static> View<'a, Msg> {
21    pub(crate) fn new(nodes: &'a mut Vec<Node<Msg>>, env: &'a Env, size: Size, idle: &'a IdleScope<Msg>) -> Self {
22        Self { nodes, env, size, idle }
23    }
24
25    /// A builder for the children of a container inside this one: the same environment and size.
26    /// The idleness this view reads and declares watches in, for builders that make views of
27    /// their own.
28    pub(crate) fn idle_scope(&self) -> &'a IdleScope<Msg> {
29        self.idle
30    }
31
32    pub(crate) fn nested<'b>(&self, nodes: &'b mut Vec<Node<Msg>>) -> View<'b, Msg>
33    where
34        'a: 'b,
35    {
36        View::new(nodes, self.env, self.size, self.idle)
37    }
38
39    /// The environment: theme, icons, language and keymap.
40    #[must_use]
41    pub fn env(&self) -> &Env {
42        self.env
43    }
44
45    /// The room the application is drawing into: the whole terminal, in columns and rows.
46    ///
47    /// This is the value for an application's own layout decision, such as "below 48 columns,
48    /// fold the three columns into one": `if ui.size().width < 48 { .. } else { .. }` in `view`.
49    ///
50    /// The application's view fills the screen, so at the top of `view` this is exactly the
51    /// area it lays out. Every nested builder reports the same value: the children of `column`,
52    /// `row`, `stack`, `page` and `add_with`, the parts of an `AppShell`, `SidePanel`,
53    /// `Splitter` or `Popover`, the content of a `Modal` or other layer. The view is built
54    /// before layout divides the screen, so a container's own share is not known yet while its
55    /// children are being built; the number never pretends to be that share. A widget that
56    /// adapts to its own rectangle (a column that shortens its labels) does so in `measure` and
57    /// `paint`, which receive it.
58    ///
59    /// Reading it performs no I/O: it is the size of the frame the framework is about to draw,
60    /// which it already holds. After a terminal resize the next frame reports the new size, and
61    /// [`Harness::resize`](crate::runtime::Harness::resize) does the same in tests.
62    #[must_use]
63    pub fn size(&self) -> Size {
64        self.size
65    }
66
67    /// How long no input has reached this terminal: the time since the last key, mouse event or
68    /// paste the runtime received, or since the application started when none came yet.
69    ///
70    /// Everything the user does in this terminal counts: a key going down, repeating or coming
71    /// up, a mouse button, the wheel, the pointer moving over the window, a paste, and the end of
72    /// a [`Handoff`](crate::runtime::Handoff), because the program that had the terminal was
73    /// being used meanwhile. A terminal resize does not count: a window manager or a monitor
74    /// change resizes a window nobody is sitting at. Messages, background work and timers do not
75    /// count either; they are the application, not the user. Other programs and other terminals
76    /// are out of reach: this is idleness *here*, not idleness of the machine.
77    ///
78    /// Reading the value keeps it current on screen: while `view` reads it, the runtime draws
79    /// again each time it passes a whole second, and stops once `view` no longer reads it. A
80    /// view that shows minutes therefore redraws once a second while it shows them; one that
81    /// only needs to act after a silence uses [`View::on_idle`], which wakes the application
82    /// once, at that moment, without drawing in between.
83    ///
84    /// [`Harness::advance`](crate::runtime::Harness::advance) moves it forward in tests, and
85    /// every simulated input starts it again from zero.
86    #[must_use]
87    pub fn idle_for(&self) -> Duration {
88        self.idle.read.set(true);
89        self.idle.silent
90    }
91
92    /// Tells the application when no input has arrived for `after`, and when input comes back.
93    ///
94    /// `message(true)` is delivered once, at the moment the silence reaches `after`: the runtime
95    /// wakes for it even when nothing else happens, and does not draw in between. The first
96    /// input afterwards delivers `message(false)`, before that input reaches any widget, and
97    /// starts the next wait. What counts as input is listed at [`View::idle_for`].
98    ///
99    /// ```
100    /// use std::time::Duration;
101    ///
102    /// use qframe::prelude::*;
103    ///
104    /// #[derive(Default)]
105    /// struct Focus {
106    ///     away: bool,
107    /// }
108    ///
109    /// impl App for Focus {
110    ///     type Msg = bool;
111    ///     fn update(&mut self, away: bool) -> Command<bool> {
112    ///         self.away = away;
113    ///         Command::none()
114    ///     }
115    ///     fn view(&self, ui: &mut View<'_, bool>) {
116    ///         ui.on_idle(Duration::from_secs(300), |away| away);
117    ///         ui.add(Text::new(if self.away { "away" } else { "working" }));
118    ///     }
119    /// }
120    ///
121    /// let mut app = Harness::new(Focus::default(), 20, 1);
122    /// app.advance(Duration::from_secs(299));
123    /// assert!(app.screen().contains("working"));
124    /// app.advance(Duration::from_secs(1));
125    /// assert!(app.screen().contains("away"));
126    /// app.press("x");
127    /// assert!(app.screen().contains("working"));
128    /// ```
129    ///
130    /// Declare the watch in every frame it should stay active, like a widget: the runtime
131    /// answers the watches of the latest frame. One that is no longer declared is not told the
132    /// silence ended. A watch declared when the silence has already lasted `after` is told at
133    /// once. Watches with different `after` are independent, so an application can dim the
134    /// screen after one minute and pause a timer after five.
135    pub fn on_idle(&mut self, after: Duration, message: impl Fn(bool) -> Msg + 'static) {
136        self.idle.watches.borrow_mut().push(IdleWatch { after, message: Box::new(message) });
137    }
138
139    /// Adds a widget.
140    pub fn add<W: Widget<Msg>>(&mut self, widget: W) -> NodeMut<'_, Msg> {
141        let index = self.nodes.len();
142        self.nodes.push(Node::new(widget, index));
143        NodeMut { node: self.nodes.last_mut().expect("a node was just pushed") }
144    }
145
146    /// Adds a widget that contains other widgets, built by `build`.
147    pub fn add_with<W: Container<Msg>>(
148        &mut self,
149        mut widget: W,
150        build: impl FnOnce(&mut View<'_, Msg>),
151    ) -> NodeMut<'_, Msg> {
152        let mut children = Vec::new();
153        build(&mut self.nested(&mut children));
154        widget.set_children(children);
155        self.add(widget)
156    }
157
158    /// Adds a column whose children are built by `build`.
159    pub fn column(&mut self, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
160        self.container(Axis::Column, build)
161    }
162
163    /// Adds a row whose children are built by `build`.
164    pub fn row(&mut self, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
165        self.container(Axis::Row, build)
166    }
167
168    /// Adds a stack: children are drawn on top of each other in the same area, later ones on top.
169    pub fn stack(&mut self, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
170        self.container(Axis::Stack, build)
171    }
172
173    /// Adds a column that remembers its widgets' state (focus, scroll, cursors) while it is not
174    /// shown. Give every page of a router its own `key`.
175    pub fn page(&mut self, key: impl Into<String>, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
176        let node = self.container(Axis::Column, build);
177        node.node.persistent = true;
178        node.node.key = Key::Named(key.into());
179        node.fill()
180    }
181
182    /// Adds a column whose children are built by `build` with messages of their own type
183    /// `Inner`, each converted by `map` on its way to the application. A screen with its own
184    /// messages writes its view for them, and the application places it in one line:
185    ///
186    /// ```
187    /// use qframe::prelude::*;
188    ///
189    /// mod search {
190    ///     use qframe::prelude::*;
191    ///
192    ///     #[derive(Clone)]
193    ///     pub enum Msg {
194    ///         Run,
195    ///     }
196    ///
197    ///     pub fn view(ui: &mut View<'_, Msg>) {
198    ///         ui.add(Button::new("Search").on_press(Msg::Run));
199    ///     }
200    /// }
201    ///
202    /// enum Msg {
203    ///     Search(search::Msg),
204    /// }
205    ///
206    /// fn view(ui: &mut View<'_, Msg>) {
207    ///     ui.map(Msg::Search, search::view).fill();
208    /// }
209    /// ```
210    ///
211    /// Everything the screen does inside arrives converted: the messages of its widgets and
212    /// handlers, the children of [`add_with`](Self::add_with) and nested containers, layers such
213    /// as a `Modal` and the widgets in them, overlays such as an open dropdown. Focus, memory and
214    /// ids work as for any column; [`Command::map`](crate::runtime::Command::map) converts the
215    /// commands the screen's `update` returns the same way.
216    pub fn map<Inner: 'static>(
217        &mut self,
218        map: impl Fn(Inner) -> Msg + 'static,
219        build: impl FnOnce(&mut View<'_, Inner>),
220    ) -> NodeMut<'_, Msg> {
221        let map = std::rc::Rc::new(map);
222        let mut children = Vec::new();
223        // The screen reads and watches the same silence as the application; what it read and the
224        // watches it declared are handed up, their messages converted like any other.
225        let idle = IdleScope::new(self.idle.silent);
226        build(&mut View::new(&mut children, self.env, self.size, &idle));
227        if idle.read.get() {
228            self.idle.read.set(true);
229        }
230        for watch in idle.watches.into_inner() {
231            let map = std::rc::Rc::clone(&map);
232            let message = watch.message;
233            self.idle
234                .watches
235                .borrow_mut()
236                .push(IdleWatch { after: watch.after, message: Box::new(move |away| map(message(away))) });
237        }
238        self.add(Mapped::new(children, move |inner| map(inner)))
239    }
240
241    /// Adds empty space that takes the room left in a row or column.
242    pub fn spacer(&mut self) -> NodeMut<'_, Msg> {
243        self.container(Axis::Stack, |_| {}).fill()
244    }
245
246    fn container(&mut self, axis: Axis, build: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'_, Msg> {
247        let mut children = Vec::new();
248        build(&mut self.nested(&mut children));
249        self.add(Flex::new(axis, children))
250    }
251}
252
253/// Adjusts the node just added. Every method changes the node in place, so the result can be
254/// ignored or chained.
255pub struct NodeMut<'a, Msg> {
256    node: &'a mut Node<Msg>,
257}
258
259impl<'a, Msg> NodeMut<'a, Msg> {
260    /// Names the node. Name widgets whose position among their siblings can change (list rows,
261    /// optional widgets) so their state and focus follow them.
262    pub fn id(self, name: impl Into<String>) -> Self {
263        self.node.key = Key::Named(name.into());
264        self
265    }
266
267    /// Sets the width.
268    pub fn width(self, width: Length) -> Self {
269        self.node.layout.width = width;
270        self
271    }
272
273    /// Sets the height.
274    pub fn height(self, height: Length) -> Self {
275        self.node.layout.height = height;
276        self
277    }
278
279    /// Takes all space left in both directions.
280    pub fn fill(self) -> Self {
281        self.width(Length::Fill(1)).height(Length::Fill(1))
282    }
283
284    /// Takes all width left.
285    pub fn fill_width(self) -> Self {
286        self.width(Length::Fill(1))
287    }
288
289    /// Takes all height left.
290    pub fn fill_height(self) -> Self {
291        self.height(Length::Fill(1))
292    }
293
294    /// Keeps `padding` free inside the node.
295    pub fn padding(self, padding: Padding) -> Self {
296        self.node.layout.padding = padding;
297        self
298    }
299
300    /// Leaves `cells` between the children of a row or column.
301    pub fn gap(self, cells: u16) -> Self {
302        self.node.layout.gap = cells;
303        self
304    }
305
306    /// Places children along the main axis of a row or column (both axes of a stack).
307    pub fn justify(self, align: Align) -> Self {
308        self.node.layout.justify = align;
309        self
310    }
311
312    /// Whether a mouse drag may select text in this node. Nothing is selectable unless asked:
313    /// `true` makes the node a selection region, so a drag that starts inside it selects text
314    /// within the node only (widgets such as `CodeView` and `Markdown` are regions by
315    /// themselves). `false` keeps selection out of the node and everything inside it, also out
316    /// of regions within it, e.g. for a secret shown inside a selectable log.
317    pub fn selectable(self, selectable: bool) -> Self {
318        self.node.selectable = Some(selectable);
319        self
320    }
321
322    /// Places children across the main axis of a row or column.
323    pub fn align(self, align: Align) -> Self {
324        self.node.layout.align = align;
325        self
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use std::cell::RefCell;
332
333    use crate::geometry::Size;
334    use crate::runtime::{App, Command, Harness};
335    use crate::widget::{Length, View};
336    use crate::widgets::{Modal, Text};
337
338    /// Records the size every builder of its view saw: the root, a nested column and a row with
339    /// a fixed width inside it, and the content of a modal layer.
340    #[derive(Default)]
341    struct Probe {
342        seen: RefCell<Vec<Size>>,
343    }
344
345    impl Probe {
346        fn take(&self) -> Vec<Size> {
347            std::mem::take(&mut *self.seen.borrow_mut())
348        }
349    }
350
351    impl App for Probe {
352        type Msg = ();
353
354        fn update(&mut self, (): ()) -> Command<()> {
355            Command::none()
356        }
357
358        fn view(&self, ui: &mut View<'_, ()>) {
359            self.seen.borrow_mut().push(ui.size());
360            ui.column(|ui| {
361                self.seen.borrow_mut().push(ui.size());
362                ui.row(|ui| {
363                    self.seen.borrow_mut().push(ui.size());
364                    ui.add(Text::new("probe"));
365                })
366                .width(Length::Cells(10));
367            });
368            ui.add_with(Modal::new(), |ui| {
369                self.seen.borrow_mut().push(ui.size());
370                ui.add(Text::new("layer"));
371            });
372        }
373    }
374
375    #[test]
376    fn every_builder_of_the_view_sees_the_terminal_size() {
377        let mut harness = Harness::new(Probe::default(), 83, 27);
378        let seen = harness.app().take();
379        assert!(seen.len() >= 4, "{seen:?}");
380        assert!(seen.iter().all(|size| *size == Size::new(83, 27)), "{seen:?}");
381
382        harness.resize(31, 9);
383        let seen = harness.app().take();
384        assert!(seen.len() >= 4, "{seen:?}");
385        assert!(seen.iter().all(|size| *size == Size::new(31, 9)), "{seen:?}");
386
387        harness.resize(0, 0);
388        let seen = harness.app().take();
389        assert!(seen.len() >= 4, "{seen:?}");
390        assert!(seen.iter().all(|size| *size == Size::new(0, 0)), "{seen:?}");
391    }
392
393    /// Three columns side by side from 48 columns up; below that the groups fold into a strip
394    /// above the list and the detail is left out.
395    struct Folding;
396
397    impl App for Folding {
398        type Msg = ();
399
400        fn update(&mut self, (): ()) -> Command<()> {
401            Command::none()
402        }
403
404        fn view(&self, ui: &mut View<'_, ()>) {
405            if ui.size().width < 48 {
406                ui.column(|ui| {
407                    ui.add(Text::new("groups"));
408                    ui.add(Text::new("items"));
409                })
410                .fill();
411            } else {
412                ui.row(|ui| {
413                    ui.add(Text::new("groups"));
414                    ui.add(Text::new("items"));
415                    ui.add(Text::new("detail"));
416                })
417                .gap(2)
418                .fill();
419            }
420        }
421    }
422
423    #[test]
424    fn an_application_folds_its_layout_below_a_width() {
425        let mut harness = Harness::new(Folding, 120, 10);
426        assert_eq!(harness.screen().lines().next(), Some("groups  items  detail"), "{}", harness.screen());
427
428        harness.resize(40, 10);
429        let screen = harness.screen();
430        let lines: Vec<&str> = screen.lines().collect();
431        assert_eq!(lines.get(..2), Some(&["groups", "items"][..]), "{screen}");
432        assert!(!screen.contains("detail"), "{screen}");
433
434        harness.resize(120, 10);
435        assert_eq!(harness.screen().lines().next(), Some("groups  items  detail"), "{}", harness.screen());
436    }
437}