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}