use std::{cell::RefCell, rc::Rc}; use gpui::{ Context, InteractiveElement as _, IntoElement, Modifiers, MouseMoveEvent, ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, TestAppContext, VisualTestContext, Window, WindowHandle, canvas, div, point, px, }; use gpui_native_core::button::Button; use gpui_native_core::tooltip::Tooltip; use gpui_native_theme::Theme; use gpui_native_utils::record_anchor_bounds; /// Hover target at the top-left with a tooltip whose builder records every /// invocation. The tooltip anchors to the target itself, so it hangs below /// the target's bottom edge instead of at the cursor. struct Harness { calls: Rc>>, } impl Render for Harness { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { let calls = self.calls.clone(); let target = div() .id("hover-target") .debug_selector(|| "hover-target".to_string()) .w(px(100.)) .h(px(40.)) .bg(gpui::black()) .child( canvas(record_anchor_bounds("hover-target".into()), |_, _, _, _| {}) .absolute() .inset_0(), ) .tooltip(move |window, cx| { calls.borrow_mut().push("target"); Tooltip::anchored("Target tooltip", "hover-target")(window, cx) }); let button_calls = self.calls.clone(); let button = Button::new("hover-button") .label("Hover me") .tooltip(move |window, cx| { button_calls.borrow_mut().push("button"); Tooltip::anchored("Button tooltip", "hover-button")(window, cx) }); div() .size_full() .child(target) .child(div().mt_12().child(button)) } } fn tooltip_window() -> ( TestAppContext, VisualTestContext, Rc>>, ) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let calls = Rc::new(RefCell::new(Vec::new())); let harness_calls = calls.clone(); let window = cx.add_window(move |_, _| Harness { calls: harness_calls, }); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, calls) } fn hover(visual: &mut VisualTestContext, position: gpui::Point) { visual.simulate_event(MouseMoveEvent { position, pressed_button: None, modifiers: Modifiers::default(), }); } /// gpui's hover delay before deploying a tooltip. const SHOW_DELAY: std::time::Duration = std::time::Duration::from_millis(500); fn draw(visual: &mut VisualTestContext) { visual.update(|window, cx| { window.draw(cx).clear(cx); }); } struct LowHarness; impl Render for LowHarness { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { // The target is pinned to the window's bottom edge: the space below it // is nil, so the tooltip must flip above the parent. div().size_full().flex().flex_col().justify_end().child( div() .id("low-target") .debug_selector(|| "low-target".to_string()) .w(px(100.)) .h(px(40.)) .bg(gpui::black()) .child( canvas(record_anchor_bounds("low-target".into()), |_, _, _, _| {}) .absolute() .inset_0(), ) .tooltip(Tooltip::anchored("Low tooltip", "low-target")), ) } } fn low_tooltip_window() -> (TestAppContext, VisualTestContext, WindowHandle) { let mut cx = TestAppContext::single(); cx.update(Theme::init); let window = cx.add_window(move |_, _| LowHarness); let mut visual = VisualTestContext::from_window(window.into(), &cx); visual.update(|window, cx| { window.draw(cx).clear(cx); }); (cx, visual, window) } #[test] fn hovering_the_target_deploys_the_tooltip_after_the_delay() { let (cx, mut visual, calls) = tooltip_window(); hover(&mut visual, point(px(50.), px(20.))); assert!( calls.borrow().is_empty(), "builder runs only after the delay" ); assert!( calls.borrow().is_empty(), "builder runs only after the delay" ); cx.executor().advance_clock(SHOW_DELAY); visual.run_until_parked(); draw(&mut visual); assert_eq!(*calls.borrow(), vec!["target"]); } #[test] fn hovering_elsewhere_deploys_nothing() { let (cx, mut visual, calls) = tooltip_window(); hover(&mut visual, point(px(800.), px(600.))); cx.executor().advance_clock(SHOW_DELAY); visual.run_until_parked(); draw(&mut visual); assert!(calls.borrow().is_empty()); } #[test] fn the_button_passthrough_deploys_the_tooltip() { let (cx, mut visual, calls) = tooltip_window(); hover(&mut visual, point(px(50.), px(100.))); cx.executor().advance_clock(SHOW_DELAY); visual.run_until_parked(); draw(&mut visual); assert_eq!(*calls.borrow(), vec!["button"]); } /// An anchored tooltip hangs below the hovered parent's bottom edge with the /// spacing gap: it never covers the parent, wherever the cursor is inside it. #[test] fn the_anchored_panel_hangs_below_the_parent_without_overlapping() { let (cx, mut visual, _calls) = tooltip_window(); hover(&mut visual, point(px(50.), px(20.))); cx.executor().advance_clock(SHOW_DELAY); visual.run_until_parked(); draw(&mut visual); let target = visual .debug_bounds("hover-target") .expect("target laid out"); let panel = visual .debug_bounds("tooltip-panel") .expect("panel rendered"); let unit = cx.update(|cx| Theme::global(cx).spacing.unit); assert!( (panel.top() - target.bottom() - unit).abs() <= px(0.5), "the panel hangs {gap:?} below the target", gap = panel.top() - target.bottom() ); assert!((panel.left() - target.left()).abs() <= px(0.5)); assert!( panel.top() >= target.bottom(), "the panel must never overlap the hovered parent" ); } /// A parent pinned to the window's bottom edge leaves no room below: the /// panel flips above the parent's top edge instead. #[test] fn an_anchored_tooltip_flips_above_a_low_parent() { let (mut cx, mut visual, window) = low_tooltip_window(); let window_height = window .update(&mut cx, |_, window, _cx| window.viewport_size().height) .expect("window alive"); hover(&mut visual, point(px(50.), window_height - px(20.))); cx.executor().advance_clock(SHOW_DELAY); visual.run_until_parked(); draw(&mut visual); let target = visual.debug_bounds("low-target").expect("target laid out"); let panel = visual .debug_bounds("tooltip-panel") .expect("panel rendered"); // The flip hangs the panel above the parent's top edge. assert!( panel.bottom() <= target.top(), "the panel must not cover the parent: panel bottom {:?}, parent top {:?}", panel.bottom(), target.top() ); assert!( panel.top() >= px(0.), "the flipped panel must stay inside the window" ); }