1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub struct Node {
28 window: WindowId,
29 id: taffy::NodeId,
30}
31
32#[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
46pub struct Ui<B: Backend> {
48 state: Rc<RefCell<UiState<B>>>,
49}
50
51const OPEN_WINDOW: &str = "windows are not used after they are closed";
52const UNSEEN_CHANGE: f32 = 0.5;
54const FULL: i64 = 100;
56
57pub type WindowCloseHandler = Rc<dyn Fn(WindowId)>;
59
60struct UiState<B: Backend> {
61 backend: Rc<B>,
62 layout: Layout<B::Widget>,
63 windows: Vec<Option<WindowState<B>>>,
65 on_close: Option<WindowCloseHandler>,
67}
68
69struct WindowState<B: Backend> {
70 native: Rc<B::Window>,
71 root: taffy::NodeId,
72 viewport: Option<Size>,
74 min_width: f32,
76 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 pub fn set_text(&self, node: Node, text: &str) {
379 self.change_content(node, |backend, widget| backend.set_text(widget, text));
380 }
381
382 pub fn set_badge(&self, node: Node, badge: &str) {
385 self.change_content(node, |backend, widget| backend.set_badge(widget, badge));
386 }
387
388 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 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 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 pub fn set_checked(&self, node: Node, checked: bool) {
425 self.change_state(node, |backend, widget| backend.set_checked(widget, checked));
426 }
427
428 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 pub fn set_value(&self, node: Node, value: i64) {
443 self.change_state(node, |backend, widget| backend.set_value(widget, value));
444 }
445
446 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 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 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 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 pub fn set_size(&self, window: WindowId, size: Size) {
500 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 pub fn set_close_handler(&self, on_close: WindowCloseHandler) {
518 self.state.borrow_mut().on_close = Some(on_close);
519 }
520
521 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 pub fn focus(&self, window: WindowId) {
538 let state = self.state.borrow();
539 state.backend.focus_window(&state.window(window).native);
540 }
541
542 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 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 #[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 #[must_use]
569 pub fn dialogs(&self) -> Rc<dyn ShowDialog> {
570 Rc::clone(&self.state.borrow().backend) as Rc<dyn ShowDialog>
571 }
572
573 pub fn run(&self) {
575 let backend = Rc::clone(&self.state.borrow().backend);
576 backend.run();
577 }
578}
579
580fn 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
596fn image_path(path: &str) -> Option<PathBuf> {
598 if path.is_empty() {
599 return None;
600 }
601 expand_home(path).ok()
602}
603
604fn 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 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 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 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 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}