use gpui::AppContext as _; use gpui::{ AnyView, App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled as _, Window, canvas, div, point, px, }; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH}; use gpui_native_utils::{anchor_bounds, record_anchor_bounds}; const PANEL_BOUNDS_ID: &str = "tooltip-panel-bounds"; /// A small text panel gpui shows next to a hovered element. pub struct Tooltip { text: SharedString, anchor: Option>, } impl Tooltip { /// Creates the tooltip view. pub fn new(text: L, cx: &mut App) -> Entity where L: Into, { cx.new(|_| Self { text: text.into(), anchor: None, }) } /// Sets the anchor element ID for the tooltip. pub fn anchored(text: L, anchor_id: I) -> impl Fn(&mut Window, &mut App) -> AnyView where L: Into, I: Into, { let text = text.into(); let anchor_id = anchor_id.into(); move |window, cx| { let anchor = anchor_bounds(window.window_handle().window_id(), &anchor_id, cx); cx.new(|_| Self { text: text.clone(), anchor, }) .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 { // Copied out: the mutable reads below need `cx` free of this borrow. let theme = cx.theme(); let text_size = theme.sizes.small.text_size; let spacing_unit = theme.spacing.unit; let surface = theme.colors.surface; let surface_foreground = theme.colors.surface_foreground; let radius_sm = theme.radius.sm; let shadows = theme.elevations.popover.shadows(); let border = if theme.borders.overlays { theme.colors.border } else { theme.colors.surface }; let build_panel = || { div() .debug_selector(|| "tooltip-panel".to_string()) .px_2() .py_1() .max_w(px(360.)) .bg(surface) .border_1() .border_color(border) .rounded(radius_sm) .shadow(shadows.clone()) .text_size(text_size) .text_color(surface_foreground) .child(self.text.clone()) }; let (wrapper, panel) = match self.anchor { Some(parent) => { let origin = window.mouse_position() + point(px(1.), px(1.)); let viewport_height = window.viewport_size().height; let height = anchor_bounds( window.window_handle().window_id(), &ElementId::from(PANEL_BOUNDS_ID), cx, ) .map(|bounds| bounds.size.height) .unwrap_or_else(|| text_size * 1.5 + window.rem_size() * 0.5 + BORDER_WIDTH * 2.0); let below = viewport_height - spacing_unit - parent.bottom(); let above = parent.top() - spacing_unit; let hang_below = below >= height || below >= above; let panel = build_panel() .ml(parent.left() - origin.x) .mt(if hang_below { parent.bottom() + spacing_unit - origin.y } else { parent.top() - spacing_unit - height - origin.y }) .child( canvas( record_anchor_bounds(ElementId::from(PANEL_BOUNDS_ID)), |_, _, _, _| {}, ) .absolute() .inset_0(), ); (div().debug_selector(|| "tooltip-anchor".to_string()), panel) } None => { let wrapper = div() .debug_selector(|| "tooltip-anchor".to_string()) .pl(spacing_unit * 2.0) .pt(spacing_unit * 2.0); (wrapper, build_panel()) } }; wrapper.child(panel) } }