Skip to main content

qframe/widget/
view.rs

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