Skip to main content

nifty_runtime/
ui.rs

1//! The retained UI that apps drive: create native widgets, arrange them, update them.
2//! Changes are laid out together when [`Ui::flush`] runs, once per event.
3
4use std::cell::RefCell;
5use std::fmt;
6use std::path::PathBuf;
7use std::rc::Rc;
8
9use crate::backend::{
10    Backend, ClickHandler, DropHandler, MenuHandler, NumberHandler, Parent, TextHandler,
11    ToggleHandler, WakeHandler,
12};
13use crate::dialog::ShowDialog;
14use crate::geometry::Size;
15use crate::layout::{Layout, Placed};
16use crate::menu::{Menu, MenuEntry};
17use crate::paths::expand_home;
18use crate::style::{
19    Backdrop, ButtonStyle, Icon, ImageSize, LabelStyle, StackStyle, Surface, ToggleStyle,
20};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct WindowId(usize);
24
25/// A widget, stack or spacer inside one window.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub struct Node {
28    window: WindowId,
29    id: taffy::NodeId,
30}
31
32/// Where a new node goes: at `index` among the children of `parent`.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct Position {
35    pub parent: Node,
36    pub index: usize,
37}
38
39impl Node {
40    #[must_use]
41    pub fn window(self) -> WindowId {
42        self.window
43    }
44}
45
46/// A shared handle to the app's windows; clones refer to the same UI.
47pub struct Ui<B: Backend> {
48    state: Rc<RefCell<UiState<B>>>,
49}
50
51const OPEN_WINDOW: &str = "windows are not used after they are closed";
52/// Sizes closer than this, in points, look the same on screen.
53const UNSEEN_CHANGE: f32 = 0.5;
54/// The percentage at which a progress bar is full.
55const FULL: i64 = 100;
56
57/// Decides what happens when the user asks to close a window.
58pub type WindowCloseHandler = Rc<dyn Fn(WindowId)>;
59
60struct UiState<B: Backend> {
61    backend: Rc<B>,
62    layout: Layout<B::Widget>,
63    /// Indexed by [`WindowId`]; `None` once closed, so the ids of other windows stay valid.
64    windows: Vec<Option<WindowState<B>>>,
65    /// What runs when the user asks to close a window; without one, it closes.
66    on_close: Option<WindowCloseHandler>,
67}
68
69struct WindowState<B: Backend> {
70    native: Rc<B::Window>,
71    root: taffy::NodeId,
72    /// `None` until the window is shown and reports its size.
73    viewport: Option<Size>,
74    /// The narrowest content width last given to the backend.
75    min_width: f32,
76    /// The width the window's scroll areas last left for their scroll bars.
77    scroll_bar_width: f32,
78    needs_layout: bool,
79}
80
81impl<B: Backend> Ui<B> {
82    pub fn new(backend: B) -> Self {
83        Self {
84            state: Rc::new(RefCell::new(UiState {
85                backend: Rc::new(backend),
86                layout: Layout::new(),
87                windows: Vec::new(),
88                on_close: None,
89            })),
90        }
91    }
92
93    /// Opens a hidden window; fill it with [`Ui::set_content`], then [`Ui::show`] it.
94    #[must_use = "a window must be filled with `set_content` and shown"]
95    pub fn create_window(&self, title: &str, size: Size) -> WindowId {
96        let mut state = self.state.borrow_mut();
97        let window = WindowId(state.windows.len());
98        let weak_state = Rc::downgrade(&self.state);
99        let on_resize = Box::new(move |size| {
100            if let Some(state) = weak_state.upgrade() {
101                state.borrow_mut().resize(window, size);
102            }
103        });
104        let weak_state = Rc::downgrade(&self.state);
105        let on_close = Box::new(move || {
106            if let Some(state) = weak_state.upgrade() {
107                Self { state }.close_requested(window);
108            }
109        });
110        let native = state.backend.open_window(title, size, on_resize, on_close);
111        let scroll_bar_width = state.backend.scroll_bar_width(&native);
112        let root = state.layout.add_root();
113        state.windows.push(Some(WindowState {
114            native: Rc::new(native),
115            root,
116            viewport: None,
117            min_width: 0.0,
118            scroll_bar_width,
119            needs_layout: true,
120        }));
121        window
122    }
123
124    /// A stack to fill the window with, through [`Ui::set_content`].
125    #[must_use = "the content must be shown with `set_content`"]
126    pub fn content(&self, window: WindowId, style: &StackStyle) -> Node {
127        let mut state = self.state.borrow_mut();
128        let UiState {
129            backend,
130            layout,
131            windows,
132            ..
133        } = &mut *state;
134        let native = &*live_window(windows, window).native;
135        let surface = surface(&**backend, Parent::Window(native), style);
136        let id = layout.add_stack(style, surface);
137        Node { window, id }
138    }
139
140    #[must_use = "the node is needed to change or remove it"]
141    pub fn stack(&self, at: Position, style: &StackStyle) -> Node {
142        self.add(
143            at,
144            |backend, parent, _| surface(backend, parent, style),
145            |layout, surface| layout.add_stack(style, surface),
146        )
147    }
148
149    /// A stack whose children scroll up and down when they are taller than it.
150    #[must_use = "the node is needed to change or remove it"]
151    pub fn scroll(&self, at: Position, style: &StackStyle) -> Node {
152        self.add(
153            at,
154            |backend, parent, window| {
155                (
156                    surface(backend, parent, style),
157                    backend.create_scroll(parent),
158                    backend.scroll_bar_width(window),
159                )
160            },
161            |layout, (surface, scroll, bar_width)| {
162                layout.add_scroll(style, surface, scroll, bar_width)
163            },
164        )
165    }
166
167    #[must_use = "the node is needed to change or remove it"]
168    pub fn spacer(&self, at: Position) -> Node {
169        self.add(at, |_, _, _| (), |layout, ()| layout.add_spacer())
170    }
171
172    #[must_use = "the node is needed to change or remove it"]
173    pub fn label(&self, at: Position, text: &str, style: LabelStyle) -> Node {
174        self.add_widget(at, Layout::add_widget, |backend, parent| {
175            backend.create_label(parent, text, style)
176        })
177    }
178
179    #[must_use = "the node is needed to change or remove it"]
180    pub fn button(
181        &self,
182        at: Position,
183        title: &str,
184        look: (ButtonStyle, Icon),
185        selected: Option<bool>,
186        on_click: ClickHandler,
187    ) -> Node {
188        let (style, icon) = look;
189        self.add_widget(at, Layout::add_widget, |backend, parent| {
190            backend.create_button(parent, title, style, icon, selected, on_click)
191        })
192    }
193
194    #[must_use = "the node is needed to change or remove it"]
195    pub fn nav_item(
196        &self,
197        at: Position,
198        title: &str,
199        icon: Icon,
200        badge: &str,
201        selected: bool,
202        on_click: ClickHandler,
203    ) -> Node {
204        self.add_widget(at, Layout::add_widget, |backend, parent| {
205            backend.create_nav_item(parent, title, icon, badge, selected, on_click)
206        })
207    }
208
209    #[must_use = "the node is needed to change or remove it"]
210    pub fn text_field(
211        &self,
212        at: Position,
213        text: &str,
214        (placeholder, lines): (&str, u32),
215        on_change: TextHandler,
216        on_submit: Option<ClickHandler>,
217    ) -> Node {
218        self.add_widget(at, Layout::add_widget, |backend, parent| {
219            backend.create_text_field(parent, text, placeholder, lines, on_change, on_submit)
220        })
221    }
222
223    #[must_use = "the node is needed to change or remove it"]
224    pub fn checkbox(
225        &self,
226        at: Position,
227        title: &str,
228        checked: bool,
229        style: ToggleStyle,
230        on_toggle: ToggleHandler,
231    ) -> Node {
232        self.add_widget(at, Layout::add_widget, |backend, parent| {
233            backend.create_checkbox(parent, title, checked, style, on_toggle)
234        })
235    }
236
237    #[must_use = "the node is needed to change or remove it"]
238    pub fn slider(
239        &self,
240        at: Position,
241        value: i64,
242        range: (i64, i64),
243        on_change: NumberHandler,
244    ) -> Node {
245        let (min, max) = range;
246        self.add_widget(at, Layout::add_widget, |backend, parent| {
247            backend.create_slider(parent, value, min, max, on_change)
248        })
249    }
250
251    #[must_use = "the node is needed to change or remove it"]
252    pub fn picker(
253        &self,
254        at: Position,
255        options: &[String],
256        selected: &str,
257        on_pick: TextHandler,
258    ) -> Node {
259        self.add_widget(at, Layout::add_widget, |backend, parent| {
260            backend.create_picker(parent, options, selected, on_pick)
261        })
262    }
263
264    /// A progress bar at `percent`, kept between 0 and 100, or busy without one.
265    #[must_use = "the node is needed to change or remove it"]
266    pub fn progress(&self, at: Position, percent: Option<i64>) -> Node {
267        let percent = percent.map(as_percent);
268        self.add_widget(at, Layout::add_widget, |backend, parent| {
269            backend.create_progress(parent, percent)
270        })
271    }
272
273    /// The image in the file at `path`, where `~` stands for the home folder, shown at
274    /// `size`. An empty path, or one whose home folder can't be found, shows nothing, as a
275    /// file that can't be read as an image does.
276    #[must_use = "the node is needed to change or remove it"]
277    pub fn image(&self, at: Position, path: &str, size: ImageSize) -> Node {
278        let path = image_path(path);
279        self.add_widget(at, Layout::add_widget, |backend, parent| {
280            backend.create_image(parent, path.as_deref(), size)
281        })
282    }
283
284    /// An area covering the stack `at` is in, which shows a menu of `entries` on a right
285    /// click.
286    #[must_use = "the node is needed to change or remove it"]
287    pub fn context_menu(
288        &self,
289        at: Position,
290        entries: &[MenuEntry<usize>],
291        on_select: MenuHandler,
292    ) -> Node {
293        self.add_widget(at, Layout::add_overlay, |backend, parent| {
294            backend.create_context_menu(parent, entries, on_select)
295        })
296    }
297
298    /// An area covering the stack `at` is in, which takes files and links dropped on it.
299    #[must_use = "the node is needed to change or remove it"]
300    pub fn drop_target(&self, at: Position, on_drop: DropHandler) -> Node {
301        self.add_widget(at, Layout::add_overlay, |backend, parent| {
302            backend.create_drop_target(parent, on_drop)
303        })
304    }
305
306    #[must_use = "the node is needed to change or remove it"]
307    pub fn divider(&self, at: Position) -> Node {
308        self.add_widget(at, Layout::add_divider, |backend, parent| {
309            backend.create_divider(parent)
310        })
311    }
312
313    /// Creates a widget in the parent `at` names, and places it there.
314    fn add_widget(
315        &self,
316        at: Position,
317        place: fn(&mut Layout<B::Widget>, B::Widget, Size) -> taffy::NodeId,
318        create: impl FnOnce(&B, Parent<'_, B>) -> B::Widget,
319    ) -> Node {
320        self.add(
321            at,
322            |backend, parent, _| {
323                let widget = create(backend, parent);
324                let natural = backend.measure(&widget);
325                (widget, natural)
326            },
327            |layout, (widget, natural)| place(layout, widget, natural),
328        )
329    }
330
331    /// Makes a node in two steps, `create` making its native parts in the parent `at`
332    /// names, and `place` adding them to the layout, then places it there.
333    fn add<Made>(
334        &self,
335        at: Position,
336        create: impl FnOnce(&B, Parent<'_, B>, &B::Window) -> Made,
337        place: impl FnOnce(&mut Layout<B::Widget>, Made) -> taffy::NodeId,
338    ) -> Node {
339        let window = at.parent.window;
340        let mut state = self.state.borrow_mut();
341        let UiState {
342            backend,
343            layout,
344            windows,
345            ..
346        } = &mut *state;
347        let native = &*live_window(windows, window).native;
348        let parent = layout
349            .scroll_around(at.parent.id)
350            .map_or(Parent::Window(native), Parent::Scroll);
351        let made = create(&**backend, parent, native);
352        let id = place(layout, made);
353        layout.insert(at.parent.id, at.index, id);
354        state.invalidate(window);
355        Node { window, id }
356    }
357
358    /// Makes `content` fill the window, removing whatever it showed before.
359    pub fn set_content(&self, window: WindowId, content: Node) {
360        let mut state = self.state.borrow_mut();
361        let root = state.window(window).root;
362        let previous = state.layout.set_content(root, content.id);
363        state.invalidate(window);
364        if let Some(previous) = previous {
365            state.remove_tree(previous);
366        }
367    }
368
369    /// Removes `node` and everything inside it from the window.
370    pub fn remove(&self, node: Node) {
371        let mut state = self.state.borrow_mut();
372        state.remove_tree(node.id);
373        state.invalidate(node.window);
374    }
375
376    /// # Panics
377    /// When `node` is a stack or spacer rather than a widget.
378    pub fn set_text(&self, node: Node, text: &str) {
379        self.change_content(node, |backend, widget| backend.set_text(widget, text));
380    }
381
382    /// # Panics
383    /// When `node` is a stack or spacer rather than a widget.
384    pub fn set_badge(&self, node: Node, badge: &str) {
385        self.change_content(node, |backend, widget| backend.set_badge(widget, badge));
386    }
387
388    /// Shows the image at `path` instead, as [`Ui::image`] does.
389    ///
390    /// # Panics
391    /// When `node` is a stack or spacer rather than a widget.
392    pub fn set_image(&self, node: Node, path: &str) {
393        let path = image_path(path);
394        self.change_content(node, |backend, widget| {
395            backend.set_image(widget, path.as_deref());
396        });
397    }
398
399    /// # Panics
400    /// When `node` is a stack or spacer rather than a widget.
401    pub fn set_options(&self, node: Node, options: &[String]) {
402        self.change_content(node, |backend, widget| {
403            backend.set_options(widget, options);
404        });
405    }
406
407    /// Changes what a widget shows, then lays its window out again if that changed its size.
408    fn change_content(&self, node: Node, change: impl FnOnce(&B, &B::Widget)) {
409        let mut state = self.state.borrow_mut();
410        let state = &mut *state;
411        let widget = state
412            .layout
413            .widget(node.id)
414            .expect("only widgets show content");
415        change(&state.backend, widget);
416        let natural = state.backend.measure(widget);
417        if state.layout.set_natural(node.id, natural) {
418            state.invalidate(node.window);
419        }
420    }
421
422    /// # Panics
423    /// When `node` is a stack or spacer rather than a widget.
424    pub fn set_checked(&self, node: Node, checked: bool) {
425        self.change_state(node, |backend, widget| backend.set_checked(widget, checked));
426    }
427
428    /// Fills a progress bar to `percent`, kept between 0 and 100.
429    ///
430    /// # Panics
431    /// When `node` is a stack or spacer rather than a widget.
432    pub fn set_progress(&self, node: Node, percent: i64) {
433        self.change_state(node, |backend, widget| {
434            backend.set_value(widget, as_percent(percent));
435        });
436    }
437
438    /// Moves a slider to `value`.
439    ///
440    /// # Panics
441    /// When `node` is a stack or spacer rather than a widget.
442    pub fn set_value(&self, node: Node, value: i64) {
443        self.change_state(node, |backend, widget| backend.set_value(widget, value));
444    }
445
446    /// Changes a widget's state, such as whether it's checked, which leaves its size alone.
447    fn change_state(&self, node: Node, change: impl FnOnce(&B, &B::Widget)) {
448        let state = self.state.borrow();
449        let widget = state
450            .layout
451            .widget(node.id)
452            .expect("only widgets have a state");
453        change(&state.backend, widget);
454    }
455
456    /// Lays out every window changed since the last flush.
457    pub fn flush(&self) {
458        let mut state = self.state.borrow_mut();
459        for index in 0..state.windows.len() {
460            if state.windows[index]
461                .as_ref()
462                .is_some_and(|window| window.needs_layout)
463            {
464                state.layout_window(WindowId(index));
465            }
466        }
467    }
468
469    /// Shows the window, first widening it if its content doesn't fit.
470    pub fn show(&self, window: WindowId) {
471        let (backend, native, size, min_width) = {
472            let mut state = self.state.borrow_mut();
473            let native = Rc::clone(&state.window(window).native);
474            let size = state.backend.content_size(&native);
475            state.resize(window, size);
476            let min_width = state.window(window).min_width;
477            (Rc::clone(&state.backend), native, size, min_width)
478        };
479        if size.width < min_width {
480            // The backend reports the resize through `on_resize`, which borrows the state.
481            backend.set_content_size(&native, Size::new(min_width, size.height));
482        }
483        backend.show_window(&native);
484    }
485
486    pub fn set_title(&self, window: WindowId, title: &str) {
487        let state = self.state.borrow();
488        state.backend.set_title(&state.window(window).native, title);
489    }
490
491    pub fn set_backdrop(&self, window: WindowId, backdrop: Backdrop) {
492        let state = self.state.borrow();
493        state
494            .backend
495            .set_backdrop(&state.window(window).native, backdrop);
496    }
497
498    /// Resizes the window's content area and lays it out at the new size.
499    pub fn set_size(&self, window: WindowId, size: Size) {
500        // The backend reports the resize through `on_resize`, which borrows the state again.
501        let (backend, native) = {
502            let state = self.state.borrow();
503            (
504                Rc::clone(&state.backend),
505                Rc::clone(&state.window(window).native),
506            )
507        };
508        backend.set_content_size(&native, size);
509    }
510
511    /// Removes the window's widgets and closes it. Its id must not be used again.
512    ///
513    /// # Panics
514    /// When the window is already closed.
515    /// Has `on_close` decide what happens when the user asks to close a window, in place
516    /// of closing it.
517    pub fn set_close_handler(&self, on_close: WindowCloseHandler) {
518        self.state.borrow_mut().on_close = Some(on_close);
519    }
520
521    /// The user asked to close `window`, which may have closed since the request set out.
522    fn close_requested(&self, window: WindowId) {
523        let on_close = {
524            let state = self.state.borrow();
525            if state.windows[window.0].is_none() {
526                return;
527            }
528            state.on_close.clone()
529        };
530        match on_close {
531            Some(on_close) => on_close(window),
532            None => self.close(window),
533        }
534    }
535
536    /// Brings a window already shown to the front.
537    pub fn focus(&self, window: WindowId) {
538        let state = self.state.borrow();
539        state.backend.focus_window(&state.window(window).native);
540    }
541
542    /// Closes the window and removes everything in it.
543    ///
544    /// # Panics
545    /// When the window is closed already.
546    pub fn close(&self, window: WindowId) {
547        let mut state = self.state.borrow_mut();
548        let closed = state.windows[window.0].take().expect(OPEN_WINDOW);
549        state.remove_tree(closed.root);
550        let native = Rc::into_inner(closed.native)
551            .expect("`show` and `set_size` hold a window only while they run");
552        state.backend.close_window(native);
553    }
554
555    /// Gives the app `menus`; see [`Backend::set_menu_bar`].
556    pub fn set_menu_bar(&self, menus: &[Menu<usize>], on_select: MenuHandler) {
557        let backend = Rc::clone(&self.state.borrow().backend);
558        backend.set_menu_bar(menus, on_select);
559    }
560
561    /// Returns something any thread can use to have `on_wake` run on the UI thread.
562    #[must_use = "dropping the waker means `on_wake` never runs"]
563    pub fn waker(&self, on_wake: WakeHandler) -> B::Waker {
564        self.state.borrow().backend.waker(on_wake)
565    }
566
567    /// The platform's file dialogs, for code that isn't generic over the backend.
568    #[must_use]
569    pub fn dialogs(&self) -> Rc<dyn ShowDialog> {
570        Rc::clone(&self.state.borrow().backend) as Rc<dyn ShowDialog>
571    }
572
573    /// Runs the event loop until the app quits.
574    pub fn run(&self) {
575        let backend = Rc::clone(&self.state.borrow().backend);
576        backend.run();
577    }
578}
579
580/// The surface drawn behind a stack's children, if it has one.
581fn surface<B: Backend>(
582    backend: &B,
583    parent: Parent<'_, B>,
584    style: &StackStyle,
585) -> Option<B::Widget> {
586    (style.surface != Surface::None).then(|| backend.create_surface(parent, style.surface))
587}
588
589fn live_window<B: Backend>(
590    windows: &[Option<WindowState<B>>],
591    window: WindowId,
592) -> &WindowState<B> {
593    windows[window.0].as_ref().expect(OPEN_WINDOW)
594}
595
596/// Where an image's file is, with `~` standing for the home folder; `None` for no file.
597fn image_path(path: &str) -> Option<PathBuf> {
598    if path.is_empty() {
599        return None;
600    }
601    expand_home(path).ok()
602}
603
604/// Progress bars show a share of their width, so a percentage out of range shows as full or
605/// empty.
606fn as_percent(value: i64) -> i64 {
607    value.clamp(0, FULL)
608}
609
610impl<B: Backend> UiState<B> {
611    fn window(&self, window: WindowId) -> &WindowState<B> {
612        live_window(&self.windows, window)
613    }
614
615    fn invalidate(&mut self, window: WindowId) {
616        if let Some(window) = &mut self.windows[window.0] {
617            window.needs_layout = true;
618        }
619    }
620
621    /// A closed window can still report a resize that was already on its way, so that is
622    /// ignored rather than treated as a mistake.
623    fn resize(&mut self, window: WindowId, size: Size) {
624        let Some(open) = &mut self.windows[window.0] else {
625            return;
626        };
627        open.viewport = Some(size);
628        self.layout_window(window);
629    }
630
631    /// Removes `node` and everything under it, taking their widgets off screen.
632    fn remove_tree(&mut self, node: taffy::NodeId) {
633        for widget in self.layout.remove(node) {
634            self.backend.remove_widget(widget);
635        }
636    }
637
638    fn layout_window(&mut self, window: WindowId) {
639        let Self {
640            backend,
641            layout,
642            windows,
643            ..
644        } = self;
645        let window = windows[window.0].as_mut().expect(OPEN_WINDOW);
646        let Some(viewport) = window.viewport else {
647            return;
648        };
649        window.needs_layout = false;
650        let scroll_bar_width = backend.scroll_bar_width(&window.native);
651        if (scroll_bar_width - window.scroll_bar_width).abs() >= UNSEEN_CHANGE {
652            window.scroll_bar_width = scroll_bar_width;
653            layout.set_scroll_bar_width(window.root, scroll_bar_width);
654        }
655        let title_bar_inset = backend.title_bar_inset(&window.native);
656        let min_width = layout.min_width(window.root, title_bar_inset);
657        if (min_width - window.min_width).abs() >= UNSEEN_CHANGE {
658            window.min_width = min_width;
659            backend.set_min_content_width(&window.native, min_width);
660        }
661        layout.layout_window(
662            window.root,
663            viewport,
664            title_bar_inset,
665            |placed| match placed {
666                Placed::Frame(widget, frame) => backend.set_frame(widget, frame),
667                Placed::ScrollContent(scroll, size) => backend.set_scroll_content(scroll, size),
668            },
669        );
670    }
671}
672
673impl<B: Backend> Clone for Ui<B> {
674    fn clone(&self) -> Self {
675        Self {
676            state: Rc::clone(&self.state),
677        }
678    }
679}
680
681impl<B: Backend> fmt::Debug for Ui<B> {
682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
683        f.debug_struct("Ui").finish_non_exhaustive()
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use crate::geometry::{Frame, Point};
691    use crate::style::{Align, Axis, Justify};
692    use crate::testing::{FakeBackend, FakeKind, SCROLL_BAR_WIDTH};
693
694    fn frame(x: f32, y: f32, width: f32, height: f32) -> Frame {
695        Frame {
696            origin: Point { x, y },
697            size: Size::new(width, height),
698        }
699    }
700
701    fn at(parent: Node, index: usize) -> Position {
702        Position { parent, index }
703    }
704
705    fn open(ui: &Ui<FakeBackend>, width: f32, height: f32) -> WindowId {
706        ui.create_window("test", Size::new(width, height))
707    }
708
709    /// Fills the window with a stack of `style`.
710    fn fill(ui: &Ui<FakeBackend>, window: WindowId, style: &StackStyle) -> Node {
711        let content = ui.content(window, style);
712        ui.set_content(window, content);
713        content
714    }
715
716    /// A centred column with some spacing, filling the window.
717    fn column(ui: &Ui<FakeBackend>, window: WindowId) -> Node {
718        let style = StackStyle {
719            spacing: 10.0,
720            align: Align::Center,
721            justify: Justify::Center,
722            ..StackStyle::new(Axis::Vertical)
723        };
724        fill(ui, window, &style)
725    }
726
727    fn label(ui: &Ui<FakeBackend>, at: Position, text: &str) -> Node {
728        ui.label(at, text, LabelStyle::default())
729    }
730
731    #[test]
732    fn centers_children_with_spacing() {
733        let fake = FakeBackend::default();
734        let ui = Ui::new(fake.clone());
735        let window = open(&ui, 200.0, 100.0);
736        let content = column(&ui, window);
737        label(&ui, at(content, 0), "abc");
738        label(&ui, at(content, 1), "hello");
739        ui.show(window);
740
741        assert_eq!(fake.frame(0), Some(frame(85.0, 25.0, 30.0, 20.0)));
742        assert_eq!(fake.frame(1), Some(frame(75.0, 55.0, 50.0, 20.0)));
743    }
744
745    #[test]
746    fn align_places_children_across_the_stack() {
747        let cases = [
748            (Align::Stretch, frame(0.0, 0.0, 200.0, 20.0)),
749            (Align::Start, frame(0.0, 0.0, 30.0, 20.0)),
750            (Align::Center, frame(85.0, 0.0, 30.0, 20.0)),
751            (Align::End, frame(170.0, 0.0, 30.0, 20.0)),
752        ];
753        for (align, expected) in cases {
754            let fake = FakeBackend::default();
755            let ui = Ui::new(fake.clone());
756            let window = open(&ui, 200.0, 100.0);
757            let style = StackStyle {
758                align,
759                ..StackStyle::new(Axis::Vertical)
760            };
761            let content = fill(&ui, window, &style);
762            label(&ui, at(content, 0), "abc");
763            ui.show(window);
764
765            assert_eq!(fake.frame(0), Some(expected), "{align:?}");
766        }
767    }
768
769    #[test]
770    fn surfaces_reach_under_the_title_bar_only_from_the_top() {
771        let fake = FakeBackend::default();
772        let ui = Ui::new(fake.clone());
773        let window = open(&ui, 300.0, 200.0);
774        fake.set_title_bar_inset(0, 28.0);
775        let row = fill(&ui, window, &StackStyle::new(Axis::Horizontal));
776        let sidebar = ui.stack(
777            at(row, 0),
778            &StackStyle {
779                surface: Surface::Sidebar,
780                width: 100.0,
781                padding: 10.0,
782                ..StackStyle::new(Axis::Vertical)
783            },
784        );
785        label(&ui, at(sidebar, 0), "abc");
786        let main = ui.stack(
787            at(row, 1),
788            &StackStyle {
789                surface: Surface::Content,
790                grow: true,
791                padding: 20.0,
792                ..StackStyle::new(Axis::Vertical)
793            },
794        );
795        let card = ui.stack(
796            at(main, 0),
797            &StackStyle {
798                surface: Surface::Card,
799                padding: 5.0,
800                ..StackStyle::new(Axis::Vertical)
801            },
802        );
803        label(&ui, at(card, 0), "x");
804        ui.show(window);
805
806        assert_eq!(fake.style(0).as_deref(), Some("Sidebar"));
807        assert_eq!(fake.frame(0), Some(frame(0.0, 0.0, 100.0, 200.0)));
808        assert_eq!(fake.frame(1), Some(frame(10.0, 38.0, 80.0, 20.0)));
809        assert_eq!(fake.frame(2), Some(frame(100.0, 0.0, 200.0, 200.0)));
810        assert_eq!(fake.frame(3), Some(frame(120.0, 48.0, 160.0, 30.0)));
811        assert_eq!(fake.frame(4), Some(frame(125.0, 53.0, 150.0, 20.0)));
812    }
813
814    #[test]
815    fn stacks_take_a_fixed_width_or_fill_up_to_a_limit() {
816        let fake = FakeBackend::default();
817        let ui = Ui::new(fake.clone());
818        let window = open(&ui, 400.0, 100.0);
819        let centered = fill(
820            &ui,
821            window,
822            &StackStyle {
823                align: Align::Center,
824                ..StackStyle::new(Axis::Vertical)
825            },
826        );
827        let _limited = ui.stack(
828            at(centered, 0),
829            &StackStyle {
830                surface: Surface::Card,
831                max_width: 200.0,
832                ..StackStyle::new(Axis::Vertical)
833            },
834        );
835        let _fixed = ui.stack(
836            at(centered, 1),
837            &StackStyle {
838                surface: Surface::Card,
839                width: 50.0,
840                ..StackStyle::new(Axis::Vertical)
841            },
842        );
843        ui.show(window);
844
845        assert_eq!(fake.frame(0), Some(frame(100.0, 0.0, 200.0, 0.0)));
846        assert_eq!(fake.frame(1), Some(frame(175.0, 0.0, 50.0, 0.0)));
847
848        fake.resize(0, Size::new(150.0, 100.0));
849        assert_eq!(fake.frame(0), Some(frame(0.0, 0.0, 150.0, 0.0)));
850    }
851
852    #[test]
853    fn scroll_areas_take_the_space_left_and_scroll_their_content() {
854        let fake = FakeBackend::default();
855        let ui = Ui::new(fake.clone());
856        let window = open(&ui, 200.0, 100.0);
857        let content = fill(&ui, window, &StackStyle::new(Axis::Vertical));
858        label(&ui, at(content, 0), "title");
859        let list = ui.scroll(
860            at(content, 1),
861            &StackStyle {
862                padding: 5.0,
863                spacing: 10.0,
864                ..StackStyle::new(Axis::Vertical)
865            },
866        );
867        for index in 0..4 {
868            label(&ui, at(list, index), "row");
869        }
870        label(&ui, at(content, 2), "footer");
871        ui.show(window);
872
873        let scroll = fake.find(FakeKind::Scroll)[0];
874        assert_eq!(fake.frame(scroll), Some(frame(0.0, 20.0, 200.0, 60.0)));
875        let rows: Vec<usize> = (0..fake.widget_count())
876            .filter(|&widget| fake.host(widget) == Some(scroll))
877            .collect();
878        assert_eq!(rows.len(), 4);
879        assert_eq!(
880            fake.frame(rows[1]),
881            Some(frame(5.0, 35.0, 200.0 - SCROLL_BAR_WIDTH - 10.0, 20.0)),
882            "rows are placed in the scroll area's content, beside its scroll bar"
883        );
884        assert_eq!(
885            fake.scroll_content(scroll),
886            Some(Size::new(
887                200.0 - SCROLL_BAR_WIDTH,
888                5.0 + 4.0 * 20.0 + 3.0 * 10.0 + 5.0
889            ))
890        );
891        assert_eq!(
892            fake.window_frame(rows[3]).origin.y,
893            20.0 + 95.0,
894            "past the bottom of the area until it scrolls"
895        );
896
897        fake.set_scroll_bar_width(0.0);
898        fake.resize(0, Size::new(200.0, 300.0));
899        assert_eq!(fake.frame(scroll), Some(frame(0.0, 20.0, 200.0, 260.0)));
900        assert_eq!(
901            fake.scroll_content(scroll),
902            Some(Size::new(200.0, 260.0)),
903            "short content still fills the area, and overlay scroll bars leave it all"
904        );
905    }
906
907    #[test]
908    fn windows_stay_wide_enough_for_their_content() {
909        let fake = FakeBackend::default();
910        let ui = Ui::new(fake.clone());
911        let window = open(&ui, 50.0, 100.0);
912        let content = column(&ui, window);
913        let text = label(&ui, at(content, 0), "abcdefghij");
914        ui.show(window);
915
916        assert_eq!(fake.min_widths(0), [100.0]);
917        assert_eq!(fake.window_size(0), Size::new(100.0, 100.0));
918
919        ui.set_text(text, "abcdefghijk");
920        ui.flush();
921        ui.set_text(text, "abcdefghijk");
922        ui.flush();
923        assert_eq!(fake.min_widths(0), [100.0, 110.0]);
924    }
925
926    #[test]
927    fn text_changes_are_laid_out_on_flush() {
928        let fake = FakeBackend::default();
929        let ui = Ui::new(fake.clone());
930        let window = open(&ui, 200.0, 100.0);
931        let content = column(&ui, window);
932        let text = label(&ui, at(content, 0), "n=9");
933        ui.show(window);
934
935        ui.set_text(text, "n=10");
936        assert_eq!(fake.text(0), "n=10");
937        assert_eq!(fake.frame(0).map(|f| f.size.width), Some(30.0));
938
939        ui.flush();
940        assert_eq!(fake.frame(0).map(|f| f.size.width), Some(40.0));
941    }
942
943    #[test]
944    fn inserts_and_removes_children() {
945        let fake = FakeBackend::default();
946        let ui = Ui::new(fake.clone());
947        let window = open(&ui, 200.0, 200.0);
948        let content = column(&ui, window);
949        label(&ui, at(content, 0), "a");
950        label(&ui, at(content, 1), "c");
951        ui.show(window);
952
953        let middle = label(&ui, at(content, 1), "b");
954        ui.flush();
955        assert_eq!(fake.visible_texts(FakeKind::Label), ["a", "b", "c"]);
956
957        ui.remove(middle);
958        ui.flush();
959        assert_eq!(fake.visible_texts(FakeKind::Label), ["a", "c"]);
960        assert!(fake.is_removed(2));
961    }
962
963    #[test]
964    fn removing_a_stack_removes_its_widgets() {
965        let fake = FakeBackend::default();
966        let ui = Ui::new(fake.clone());
967        let window = open(&ui, 200.0, 200.0);
968        let content = column(&ui, window);
969        let inner = ui.stack(at(content, 0), &StackStyle::new(Axis::Vertical));
970        label(&ui, at(inner, 0), "a");
971        ui.show(window);
972
973        ui.remove(inner);
974        assert!(fake.is_removed(0));
975    }
976
977    #[test]
978    fn spacer_pushes_siblings_apart_and_divider_stretches() {
979        let fake = FakeBackend::default();
980        let ui = Ui::new(fake.clone());
981        let window = open(&ui, 100.0, 100.0);
982        let content = fill(&ui, window, &StackStyle::new(Axis::Vertical));
983        label(&ui, at(content, 0), "a");
984        let _spacer = ui.spacer(at(content, 1));
985        let _divider = ui.divider(at(content, 2));
986        ui.show(window);
987
988        assert_eq!(fake.frame(1), Some(frame(0.0, 99.0, 100.0, 1.0)));
989    }
990
991    #[test]
992    fn resizing_lays_out_immediately() {
993        let fake = FakeBackend::default();
994        let ui = Ui::new(fake.clone());
995        let window = open(&ui, 100.0, 100.0);
996        let style = StackStyle {
997            justify: Justify::End,
998            ..StackStyle::new(Axis::Vertical)
999        };
1000        let content = fill(&ui, window, &style);
1001        label(&ui, at(content, 0), "a");
1002        ui.show(window);
1003        assert_eq!(fake.frame(0).map(|f| f.origin.y), Some(80.0));
1004
1005        fake.resize(0, Size::new(100.0, 300.0));
1006        assert_eq!(fake.frame(0).map(|f| f.origin.y), Some(280.0));
1007    }
1008
1009    #[test]
1010    fn new_content_replaces_and_removes_the_old() {
1011        let fake = FakeBackend::default();
1012        let ui = Ui::new(fake.clone());
1013        let window = open(&ui, 200.0, 100.0);
1014        let old_content = column(&ui, window);
1015        label(&ui, at(old_content, 0), "old");
1016        ui.show(window);
1017
1018        let new_content = column(&ui, window);
1019        label(&ui, at(new_content, 0), "new");
1020        ui.flush();
1021
1022        assert!(fake.is_removed(0));
1023        assert_eq!(fake.visible_texts(FakeKind::Label), ["new"]);
1024        assert_eq!(fake.frame(1), Some(frame(85.0, 40.0, 30.0, 20.0)));
1025    }
1026
1027    #[test]
1028    fn windows_can_be_retitled_resized_and_closed() {
1029        let fake = FakeBackend::default();
1030        let ui = Ui::new(fake.clone());
1031        let first = open(&ui, 100.0, 100.0);
1032        let second = open(&ui, 100.0, 100.0);
1033        let content = column(&ui, second);
1034        label(&ui, at(content, 0), "a");
1035        ui.show(second);
1036
1037        ui.set_title(first, "Renamed");
1038        ui.set_size(second, Size::new(300.0, 100.0));
1039        assert_eq!(fake.window_title(0), "Renamed");
1040        assert_eq!(fake.frame(0).map(|f| f.origin.x), Some(145.0));
1041
1042        ui.close(second);
1043        assert!(fake.is_closed(1));
1044        assert!(fake.is_removed(0));
1045        fake.resize(1, Size::new(50.0, 50.0));
1046        assert!(!fake.is_closed(0));
1047    }
1048
1049    #[test]
1050    fn wakers_run_their_handler_on_the_ui_thread() {
1051        use crate::backend::Waker as _;
1052
1053        let fake = FakeBackend::default();
1054        let ui = Ui::new(fake.clone());
1055        let wake_count = Rc::new(RefCell::new(0));
1056        let waker = ui.waker({
1057            let wake_count = Rc::clone(&wake_count);
1058            Box::new(move || *wake_count.borrow_mut() += 1)
1059        });
1060
1061        std::thread::scope(|scope| {
1062            scope.spawn(|| waker.wake());
1063        });
1064        assert_eq!(
1065            *wake_count.borrow(),
1066            0,
1067            "the handler waits for the UI thread"
1068        );
1069        fake.run_pending_wakes();
1070        fake.run_pending_wakes();
1071        assert_eq!(*wake_count.borrow(), 1);
1072    }
1073
1074    #[test]
1075    fn progress_stays_between_empty_and_full() {
1076        let fake = FakeBackend::default();
1077        let ui = Ui::new(fake.clone());
1078        let window = open(&ui, 100.0, 100.0);
1079        let content = column(&ui, window);
1080        let bar = ui.progress(at(content, 0), Some(150));
1081        let _busy = ui.progress(at(content, 1), None);
1082        assert_eq!(fake.value(0), Some(100));
1083        assert_eq!(fake.value(1), None);
1084
1085        ui.set_progress(bar, -5);
1086        assert_eq!(fake.value(0), Some(0));
1087        ui.set_progress(bar, 42);
1088        assert_eq!(fake.value(0), Some(42));
1089    }
1090
1091    #[test]
1092    fn context_menus_cover_their_stack_and_take_no_room() {
1093        let fake = FakeBackend::default();
1094        let ui = Ui::new(fake.clone());
1095        let window = open(&ui, 200.0, 100.0);
1096        let content = fill(&ui, window, &StackStyle::new(Axis::Vertical));
1097        let card = ui.stack(
1098            at(content, 0),
1099            &StackStyle {
1100                padding: 10.0,
1101                spacing: 5.0,
1102                ..StackStyle::new(Axis::Vertical)
1103            },
1104        );
1105        let _menu = ui.context_menu(at(card, 0), &[], Box::new(|_| {}));
1106        label(&ui, at(card, 1), "a");
1107        label(&ui, at(card, 2), "b");
1108        ui.show(window);
1109
1110        let menu = fake.find(FakeKind::ContextMenu)[0];
1111        assert_eq!(fake.frame(menu), Some(frame(0.0, 0.0, 200.0, 65.0)));
1112        let labels = fake.find(FakeKind::Label);
1113        assert_eq!(fake.frame(labels[0]).map(|f| f.origin.y), Some(10.0));
1114        assert_eq!(fake.frame(labels[1]).map(|f| f.origin.y), Some(35.0));
1115    }
1116
1117    #[test]
1118    fn controls_report_user_input() {
1119        let fake = FakeBackend::default();
1120        let ui = Ui::new(fake.clone());
1121        let window = open(&ui, 100.0, 100.0);
1122        let content = column(&ui, window);
1123        let typed = Rc::new(RefCell::new(String::new()));
1124        let toggled = Rc::new(RefCell::new(None));
1125        let _field = ui.text_field(
1126            at(content, 0),
1127            "",
1128            ("Name", 1),
1129            {
1130                let typed = Rc::clone(&typed);
1131                Box::new(move |text| text.clone_into(&mut typed.borrow_mut()))
1132            },
1133            None,
1134        );
1135        let _checkbox = ui.checkbox(at(content, 1), "Done", false, ToggleStyle::Checkbox, {
1136            let toggled = Rc::clone(&toggled);
1137            Box::new(move |checked| *toggled.borrow_mut() = Some(checked))
1138        });
1139
1140        fake.type_text(0, "Ada");
1141        fake.toggle(1);
1142
1143        assert_eq!(*typed.borrow(), "Ada");
1144        assert_eq!(*toggled.borrow(), Some(true));
1145    }
1146}