use std::{cell::Cell, rc::Rc};
use gpui_kit::{
AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div, ElementId,
InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Pixels, Render,
SharedString, StatefulInteractiveElement, Styled as _, Window,
base::{
ElementExt as _, Keyframe, Keyframes, Placement, TestSupportExt as _, Timing,
Tooltip as BaseTooltip, TooltipRequest, TooltipTransition, animate_keyframes,
},
div,
};
use crate::{ActiveTheme as _, Theme, TooltipHost};
thread_local! {
static RAMP: Keyframes<f32> =
Keyframes::try_new([Keyframe::new(0., 0f32), Keyframe::new(1., 1f32)])
.expect("two ordered stops");
}
pub struct Tooltip {
text: SharedString,
}
impl Tooltip {
pub fn new(text: impl Into<SharedString>) -> Self {
Self { text: text.into() }
}
pub fn build(text: impl Into<SharedString>, cx: &mut App) -> AnyView {
let text = text.into();
cx.new(|_| Self::new(text)).into()
}
}
impl Render for Tooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("gpui-cn-tooltip")
.test_support()
.child(surface(cx).child(self.text.clone()))
}
}
pub fn surface(cx: &App) -> Div {
let theme = cx.theme();
div()
.flex()
.items_center()
.h(theme.metrics.row)
.px(theme.text_control.size)
.rounded(theme.radius_lg())
.bg(theme.tooltip)
.text_color(theme.tooltip_foreground)
.border_1()
.border_color(theme.border())
.text_size(theme.text_control.size)
.whitespace_nowrap()
}
pub(crate) fn render_surface(
view: AnyView,
transition: TooltipTransition,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let normal = Theme::global(cx).motion.normal;
let opacity = match transition {
TooltipTransition::Enter { epoch } if !normal.is_zero() => RAMP.with(|ramp| {
animate_keyframes(
ElementId::from(("gpui-cn-tooltip-enter", epoch)),
ramp,
Timing::new(normal),
window,
cx,
)
.value
}),
_ => 1.,
};
BaseTooltip::new("gpui-cn-tooltip-positioner")
.opacity(opacity)
.child(view)
.into_any_element()
}
pub struct TooltipTrigger {
owner: ElementId,
build: Rc<dyn Fn(&mut App) -> AnyView>,
placement: Option<Placement>,
bounds: Rc<Cell<Bounds<Pixels>>>,
}
impl TooltipTrigger {
pub fn new(
owner: ElementId,
placement: Option<Placement>,
build: impl Fn(&mut App) -> AnyView + 'static,
) -> Self {
Self {
owner,
build: Rc::new(build),
placement,
bounds: Rc::new(Cell::new(Bounds::default())),
}
}
pub fn text(owner: ElementId, placement: Option<Placement>, text: SharedString) -> Self {
Self::new(owner, placement, move |cx| Tooltip::build(text.clone(), cx))
}
pub fn track_bounds(&self) -> impl Fn(Bounds<Pixels>, &mut Window, &mut App) + 'static {
let bounds = self.bounds.clone();
move |resolved, _, _| bounds.set(resolved)
}
pub fn hovered(&self, hovered: bool, window: &mut Window, cx: &mut App) {
let Some(overlay) = TooltipHost::overlay(window, cx) else {
return;
};
if hovered {
let build = self.build.clone();
let request = TooltipRequest::new(self.bounds.get(), move |_, cx| build(cx));
let request = match self.placement {
Some(placement) => request.placement(placement),
None => request,
};
TooltipHost::note_requester(window, &self.owner, cx);
overlay.update(cx, |overlay, cx| overlay.request_show(request, window, cx));
} else if TooltipHost::is_requester(window, &self.owner, cx) {
overlay.update(cx, |overlay, cx| overlay.request_hide(window, cx));
}
}
pub fn pressed(window: &mut Window, cx: &mut App) {
if let Some(overlay) = TooltipHost::overlay(window, cx) {
overlay.update(cx, |overlay, cx| overlay.hide(cx));
}
}
pub fn native(&self) -> impl Fn(&mut Window, &mut App) -> AnyView + 'static {
let build = self.build.clone();
move |_, cx| build(cx)
}
}
pub trait TooltipExt: StatefulInteractiveElement + Sized {
fn managed_tooltip(self, text: impl Into<SharedString>, window: &Window, cx: &App) -> Self {
let text = text.into();
self.managed_tooltip_with(None, move |cx| Tooltip::build(text.clone(), cx), window, cx)
}
fn managed_tooltip_at(
self,
placement: Placement,
text: impl Into<SharedString>,
window: &Window,
cx: &App,
) -> Self {
let text = text.into();
self.managed_tooltip_with(
Some(placement),
move |cx| Tooltip::build(text.clone(), cx),
window,
cx,
)
}
fn managed_tooltip_with(
self,
placement: Option<Placement>,
build: impl Fn(&mut App) -> AnyView + 'static,
window: &Window,
cx: &App,
) -> Self;
}
impl<E: StatefulInteractiveElement + gpui_kit::ParentElement> TooltipExt for E {
fn managed_tooltip_with(
mut self,
placement: Option<Placement>,
build: impl Fn(&mut App) -> AnyView + 'static,
window: &Window,
cx: &App,
) -> Self {
let owner = self
.interactivity()
.element_id
.clone()
.expect("a stateful element has an id");
let trigger = TooltipTrigger::new(owner, placement, build);
if TooltipHost::overlay(window, cx).is_none() {
return self.tooltip(trigger.native());
}
self.on_prepaint(trigger.track_bounds())
.on_hover(move |hovered, window, cx| trigger.hovered(*hovered, window, cx))
.on_mouse_down(MouseButton::Left, |_, window, cx| {
TooltipTrigger::pressed(window, cx);
})
}
}