use std::{cell::RefCell, rc::Rc}; use gpui::{ Context, IntoElement, Modifiers, MouseButton, ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, WindowHandle, div, point, px, rgba, }; use gpui_native_core::popover::Popover; use gpui_native_theme::Theme; struct Harness { open: bool, changes: Rc>>, } impl Render for Harness { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let changes = self.changes.clone(); let view = cx.entity(); div().size_full().child( Popover::new("test-popover") .open(self.open) .on_change(move |next, _, cx| { changes.borrow_mut().push(next); view.update(cx, |harness, cx| { harness.open = next; cx.notify(); }); }) .trigger(|_, _| { div() .w(px(100.)) .h(px(32.)) .bg(rgba(0x000000ff)) .into_any_element() }) .content(|_, _| { div() .w(px(200.)) .h(px(80.)) .child("Popover content") .into_any_element() }), ) } } /// A window whose popover trigger sits in the top-left corner: the panel /// hangs below it around `(0..200, 36..116)`, so clicks at `(10, 16)` land /// on the trigger, `(100, 70)` on the panel, and anything past `(300, 300)` /// is outside both. fn popover_window( open: bool, ) -> ( TestAppContext, VisualTestContext, WindowHandle, Rc>>, ) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let changes = Rc::new(RefCell::new(Vec::new())); let harness_changes = changes.clone(); let window = cx.add_window(move |_, _| Harness { open, changes: harness_changes, }); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, window, changes) } fn draw(visual: &mut VisualTestContext) { visual.update(|window, cx| { window.draw(cx).clear(cx); }); } fn click(visual: &mut VisualTestContext, position: gpui::Point) { visual.simulate_mouse_down(position, MouseButton::Left, Modifiers::default()); visual.simulate_mouse_up(position, MouseButton::Left, Modifiers::default()); } #[test] fn clicking_outside_dismisses_and_reports_closed() { let (_cx, mut visual, _window, changes) = popover_window(true); click(&mut visual, point(px(600.), px(600.))); assert_eq!(*changes.borrow(), vec![false]); // The dismissal swallowed the click and the owner closed the popover: // the redraw renders without a panel, and nothing further fires. draw(&mut visual); click(&mut visual, point(px(600.), px(600.))); assert_eq!(*changes.borrow(), vec![false]); } #[test] fn clicking_the_trigger_toggles_and_clicking_the_panel_does_nothing() { let (_cx, mut visual, _window, changes) = popover_window(true); // On the open panel's trigger: toggles closed. click(&mut visual, point(px(10.), px(16.))); assert_eq!(*changes.borrow(), vec![false]); // On the panel itself: nothing. click(&mut visual, point(px(100.), px(70.))); assert_eq!(*changes.borrow(), vec![false]); } #[test] fn clicking_the_trigger_opens_a_closed_popover() { let (_cx, mut visual, _window, changes) = popover_window(false); click(&mut visual, point(px(10.), px(16.))); assert_eq!(*changes.borrow(), vec![true]); } #[test] fn escape_dismisses_while_the_panel_holds_focus() { let (_cx, mut visual, _window, changes) = popover_window(true); draw(&mut visual); // The panel claims focus on open, so escape reaches its key listener. visual.simulate_keystrokes("escape"); assert_eq!(*changes.borrow(), vec![false]); // A closed popover is inert: no further dismissal events. draw(&mut visual); visual.simulate_keystrokes("escape"); assert_eq!(*changes.borrow(), vec![false]); } #[test] fn the_controlled_loop_reopens_from_the_app_side() { let (mut cx, mut visual, window, changes) = popover_window(true); // Dismiss once. click(&mut visual, point(px(600.), px(600.))); assert_eq!(*changes.borrow(), vec![false]); draw(&mut visual); // Reopen through the harness view: the app-driven half of the loop // reports no event of its own. window .update(&mut cx, |harness, _, cx| { harness.open = true; cx.notify(); }) .unwrap(); draw(&mut visual); // The panel is back and holds focus again: escape dismisses once more. visual.simulate_keystrokes("escape"); assert_eq!(*changes.borrow(), vec![false, false]); }