use super::*; use crate::clipboard::NoClipboard; use crate::event::{Key, KeyInput, Modifiers}; use crate::text::TextContext; const VIEWPORT: Size = Size::new(300.0, 200.0); fn sample_font() -> guiduck_scene::paint::Blob { static FONT: &[u8] = include_bytes!("../../../../guiduck-samples/fonts/DejaVuSans.ttf"); guiduck_scene::paint::Blob::new(std::sync::Arc::new(FONT)) } fn fixture() -> (WidgetTree, WidgetId) { let mut tree = WidgetTree::with_text_context(TextContext::hermetic([sample_font()])); let root = tree.insert( super::super::Container::new(), taffy::Style::default(), None, ); let dialog = tree.insert( Dialog::new() .title("Delete draft?") .content(ContentBuilder::new(|tree, parent| { tree.insert( super::super::Button::new("Cancel"), taffy::Style::default(), Some(parent), ); })), taffy::Style::default(), Some(root), ); tree.render_frame(VIEWPORT); (tree, dialog) } /// Search, rather than assume a depth: the sheet sits inside the scrim. fn sheet_showing(tree: &WidgetTree) -> bool { let mut stack: Vec = tree.overlay_roots(); while let Some(id) = stack.pop() { if tree.widget::(id).is_some() { return true; } stack.extend(tree.children(id).iter().copied()); } false } #[test] fn open_is_controlled_by_the_binding() { let (mut tree, dialog) = fixture(); assert!(!sheet_showing(&tree), "a closed dialog has no sheet at all"); // The app owns `:open`; the tree reconciles to it. tree.widget_mut::(dialog).unwrap().set_open(true); tree.render_frame(VIEWPORT); assert!(sheet_showing(&tree)); tree.widget_mut::(dialog).unwrap().set_open(false); tree.render_frame(VIEWPORT); assert!(!sheet_showing(&tree), "the body is gone, not hidden"); } #[test] fn the_body_is_lazy_like_every_other_deferred_content() { /// Is the body's Cancel button anywhere in the tree? fn body_built(tree: &WidgetTree) -> bool { let mut stack: Vec = tree.overlay_roots(); stack.extend(tree.root()); while let Some(id) = stack.pop() { if tree.widget::(id).is_some() { return true; } stack.extend(tree.children(id).iter().copied()); } false } let (mut tree, dialog) = fixture(); assert!(!body_built(&tree), "a closed dialog's body does not exist"); tree.widget_mut::(dialog).unwrap().set_open(true); tree.render_frame(VIEWPORT); assert!(body_built(&tree), "opening built it"); tree.widget_mut::(dialog).unwrap().set_open(false); tree.render_frame(VIEWPORT); assert!(!body_built(&tree), "and closing took it away, not hid it"); } #[test] fn escape_reports_rather_than_closing() { // The heart of the controlled contract. If Escape dismissed the sheet, // `:open` would still say true and the app's state would have quietly // diverged from the screen. let (mut tree, dialog) = fixture(); let closed = std::rc::Rc::new(std::cell::Cell::new(0)); let seen = std::rc::Rc::clone(&closed); tree.on_event(dialog, crate::event::EventKind::Close, move |_, _| { seen.set(seen.get() + 1); }); tree.widget_mut::(dialog).unwrap().set_open(true); tree.render_frame(VIEWPORT); tree.dispatch_key( &KeyInput { key: Key::Escape, modifiers: Modifiers::default(), pressed: true, }, &mut NoClipboard, ); tree.render_frame(VIEWPORT); assert_eq!(closed.get(), 1, "the request reached the app"); assert!( sheet_showing(&tree), "and the sheet is still up: only the app may close it" ); assert!(tree.widget::(dialog).unwrap().is_open()); // The app answers, and now it goes. tree.widget_mut::(dialog).unwrap().set_open(false); tree.render_frame(VIEWPORT); assert!(!sheet_showing(&tree)); } #[test] fn focus_lands_inside_and_comes_back() { let (mut tree, dialog) = fixture(); let outside = tree.insert( super::super::Button::new("Behind"), taffy::Style::default(), tree.root(), ); tree.render_frame(VIEWPORT); tree.set_focus(Some(outside)); tree.widget_mut::(dialog).unwrap().set_open(true); tree.render_frame(VIEWPORT); // The overlay's focus trap and the focus invariant do this between them; // the dialog adds nothing. assert_ne!(tree.focus(), Some(outside), "focus moved into the sheet"); tree.widget_mut::(dialog).unwrap().set_open(false); tree.render_frame(VIEWPORT); assert_eq!(tree.focus(), Some(outside), "and came back on close"); }