214 lines
6.9 KiB
Rust
214 lines
6.9 KiB
Rust
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<RefCell<Vec<&'static str>>>,
|
|
}
|
|
|
|
impl Render for Harness {
|
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> 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<RefCell<Vec<&'static str>>>,
|
|
) {
|
|
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<gpui::Pixels>) {
|
|
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<Self>) -> 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<LowHarness>) {
|
|
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());
|
|
}
|
|
|
|
/// 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"
|
|
);
|
|
}
|