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}