diff --git a/crates/core/src/tooltip.rs b/crates/core/src/tooltip.rs new file mode 100644 index 0000000..321081f --- /dev/null +++ b/crates/core/src/tooltip.rs @@ -0,0 +1,49 @@ +use gpui::AppContext as _; +use gpui::{ + AnyView, App, Context, Entity, IntoElement, ParentElement as _, Render, SharedString, + Styled as _, Window, div, px, +}; +use gpui_native_theme::ActiveTheme as _; + +/// A small text panel gpui shows next to a hovered element. +pub struct Tooltip { + text: SharedString, +} + +impl Tooltip { + /// Creates the tooltip view. + pub fn new(text: L, cx: &mut App) -> Entity + where + L: Into, + { + cx.new(|_| Self { text: text.into() }) + } + + /// The `.tooltip(...)` closure for a plain text tooltip. + pub fn text(text: L) -> impl Fn(&mut Window, &mut App) -> AnyView + 'static + where + L: Into, + { + let text = text.into(); + move |_, cx| Tooltip::new(text.clone(), cx).into() + } +} + +impl Render for Tooltip { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme(); + let text_size = theme.sizes.small.text_size; + + div() + .px_2() + .py_1() + .max_w(px(360.)) + .bg(theme.colors.surface) + .border_1() + .border_color(theme.colors.border) + .rounded(theme.radius.sm) + .text_size(text_size) + .text_color(theme.colors.surface_foreground) + .child(self.text.clone()) + } +} diff --git a/crates/core/tests/tooltip.rs b/crates/core/tests/tooltip.rs new file mode 100644 index 0000000..036647b --- /dev/null +++ b/crates/core/tests/tooltip.rs @@ -0,0 +1,120 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use gpui::{ + Context, InteractiveElement as _, IntoElement, Modifiers, MouseMoveEvent, ParentElement as _, + Render, StatefulInteractiveElement as _, Styled as _, TestAppContext, VisualTestContext, + Window, div, point, px, +}; +use gpui_native_core::button::Button; +use gpui_native_core::tooltip::Tooltip; +use gpui_native_theme::Theme; + +/// Hover target at the top-left with a tooltip whose builder records every +/// invocation. +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") + .w(px(100.)) + .h(px(40.)) + .bg(gpui::black()) + .tooltip(move |_, cx| { + calls.borrow_mut().push("target"); + Tooltip::new("Target tooltip", cx).into() + }); + + let button_calls = self.calls.clone(); + let button = Button::new("hover-button") + .label("Hover me") + .tooltip(move |_, cx| { + button_calls.borrow_mut().push("button"); + Tooltip::new("Button tooltip", cx).into() + }); + + 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); + }); +} + +#[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"]); +}