136 lines
4.6 KiB
Rust
136 lines
4.6 KiB
Rust
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,
|
|
};
|
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, TOOLTIP_MAX_WIDTH, TOOLTIP_OFFSET};
|
|
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<Bounds<Pixels>>,
|
|
}
|
|
|
|
impl Tooltip {
|
|
/// Creates the tooltip view.
|
|
pub fn new<L>(text: L, cx: &mut App) -> Entity<Self>
|
|
where
|
|
L: Into<SharedString>,
|
|
{
|
|
cx.new(|_| Self {
|
|
text: text.into(),
|
|
anchor: None,
|
|
})
|
|
}
|
|
|
|
/// Sets the anchor element ID for the tooltip.
|
|
pub fn anchored<L, I>(text: L, anchor_id: I) -> impl Fn(&mut Window, &mut App) -> AnyView
|
|
where
|
|
L: Into<SharedString>,
|
|
I: Into<ElementId>,
|
|
{
|
|
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<L>(text: L) -> impl Fn(&mut Window, &mut App) -> AnyView + 'static
|
|
where
|
|
L: Into<SharedString>,
|
|
{
|
|
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<Self>) -> 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(TOOLTIP_MAX_WIDTH)
|
|
.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(TOOLTIP_OFFSET, TOOLTIP_OFFSET);
|
|
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)
|
|
}
|
|
}
|