Skip to main content

liora_components/
tooltip.rs

1use gpui::{
2    AnyElement, App, Bounds, Component, ElementId, GlobalElementId, InspectorElementId,
3    IntoElement, LayoutId, Pixels, RenderOnce, SharedString, Window, div, prelude::*, px,
4};
5use liora_core::{Placement, TooltipData, clear_tooltip, set_active_tooltip, stable_unique_id};
6use std::cell::Cell;
7use std::rc::Rc;
8
9pub struct Tooltip {
10    trigger: AnyElement,
11    content: SharedString,
12    placement: Placement,
13    offset: Pixels,
14    id: Option<SharedString>,
15}
16
17impl Tooltip {
18    pub fn new(trigger: impl IntoElement) -> Self {
19        Self {
20            trigger: trigger.into_any_element(),
21            content: SharedString::default(),
22            placement: Placement::Top,
23            offset: px(8.0),
24            id: None,
25        }
26    }
27
28    pub fn content(mut self, content: impl Into<SharedString>) -> Self {
29        self.content = content.into();
30        self
31    }
32
33    pub fn placement(mut self, placement: Placement) -> Self {
34        self.placement = placement;
35        self
36    }
37
38    pub fn offset(mut self, offset: impl Into<Pixels>) -> Self {
39        self.offset = offset.into();
40        self
41    }
42
43    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
44        self.id = Some(id.into());
45        self
46    }
47}
48
49impl RenderOnce for Tooltip {
50    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
51        let content = self.content.clone();
52        let placement = self.placement;
53        let offset = self.offset;
54        let id = self.id.clone().unwrap_or_else(|| {
55            stable_unique_id(
56                format!("tooltip:{}:{:?}", self.content, self.placement),
57                "tooltip",
58                _window,
59                _cx,
60            )
61        });
62
63        let bounds_cell = Rc::new(Cell::new(Bounds::default()));
64        let bounds_cell_clone = bounds_cell.clone();
65        let hover_id = id.clone();
66        let move_id = id.clone();
67
68        div()
69            .id(id.clone())
70            .child(TooltipBoundsTracker {
71                trigger: self.trigger,
72                bounds: bounds_cell,
73            })
74            .on_hover(move |hovered, _window, cx| {
75                if !hovered {
76                    clear_tooltip(&hover_id, cx);
77                }
78            })
79            .on_mouse_move(move |_event, _window, cx| {
80                let anchor_bounds = bounds_cell_clone.get();
81                if anchor_bounds.size.width > px(0.0) {
82                    set_active_tooltip(
83                        TooltipData {
84                            id: move_id.clone(),
85                            content: content.clone(),
86                            anchor_bounds,
87                            placement,
88                            offset,
89                        },
90                        cx,
91                    );
92                }
93            })
94    }
95}
96
97impl IntoElement for Tooltip {
98    type Element = Component<Self>;
99    fn into_element(self) -> Self::Element {
100        Component::new(self)
101    }
102}
103
104struct TooltipBoundsTracker {
105    trigger: AnyElement,
106    bounds: Rc<Cell<Bounds<Pixels>>>,
107}
108
109impl IntoElement for TooltipBoundsTracker {
110    type Element = Self;
111    fn into_element(self) -> Self::Element {
112        self
113    }
114}
115
116impl gpui::Element for TooltipBoundsTracker {
117    type RequestLayoutState = ();
118    type PrepaintState = ();
119
120    fn id(&self) -> Option<ElementId> {
121        None
122    }
123    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
124        None
125    }
126
127    fn request_layout(
128        &mut self,
129        _id: Option<&GlobalElementId>,
130        _id2: Option<&InspectorElementId>,
131        window: &mut Window,
132        cx: &mut App,
133    ) -> (LayoutId, ()) {
134        (self.trigger.request_layout(window, cx), ())
135    }
136
137    fn prepaint(
138        &mut self,
139        _id: Option<&GlobalElementId>,
140        _id2: Option<&InspectorElementId>,
141        _bounds: Bounds<Pixels>,
142        _rl: &mut (),
143        window: &mut Window,
144        cx: &mut App,
145    ) -> () {
146        self.trigger.prepaint(window, cx);
147    }
148
149    fn paint(
150        &mut self,
151        _id: Option<&GlobalElementId>,
152        _id2: Option<&InspectorElementId>,
153        bounds: Bounds<Pixels>,
154        _rl: &mut (),
155        _ps: &mut (),
156        window: &mut Window,
157        cx: &mut App,
158    ) {
159        self.bounds.set(bounds);
160        self.trigger.paint(window, cx);
161    }
162}