gpui_component/plot/
tooltip.rs

1use gpui::{
2    div, prelude::FluentBuilder, px, AnyElement, App, Div, Hsla, IntoElement, ParentElement,
3    Pixels, Point, RenderOnce, StyleRefinement, Styled, Window,
4};
5
6use crate::{v_flex, ActiveTheme};
7
8#[derive(Default)]
9pub enum CrossLineAxis {
10    #[default]
11    Vertical,
12    Horizontal,
13    Both,
14}
15
16impl CrossLineAxis {
17    /// Returns true if the cross line axis is vertical or both.
18    #[inline]
19    pub fn show_vertical(&self) -> bool {
20        matches!(self, CrossLineAxis::Vertical | CrossLineAxis::Both)
21    }
22
23    /// Returns true if the cross line axis is horizontal or both.
24    #[inline]
25    pub fn show_horizontal(&self) -> bool {
26        matches!(self, CrossLineAxis::Horizontal | CrossLineAxis::Both)
27    }
28}
29
30#[derive(IntoElement)]
31pub struct CrossLine {
32    point: Point<Pixels>,
33    height: Option<f32>,
34    direction: CrossLineAxis,
35}
36
37impl CrossLine {
38    pub fn new(point: Point<Pixels>) -> Self {
39        Self {
40            point,
41            height: None,
42            direction: Default::default(),
43        }
44    }
45
46    /// Set the cross line axis to horizontal.
47    pub fn horizontal(mut self) -> Self {
48        self.direction = CrossLineAxis::Horizontal;
49        self
50    }
51
52    /// Set the cross line axis to both.
53    pub fn both(mut self) -> Self {
54        self.direction = CrossLineAxis::Both;
55        self
56    }
57
58    /// Set the height of the cross line.
59    pub fn height(mut self, height: f32) -> Self {
60        self.height = Some(height);
61        self
62    }
63}
64
65impl From<Point<Pixels>> for CrossLine {
66    fn from(value: Point<Pixels>) -> Self {
67        Self::new(value)
68    }
69}
70
71impl RenderOnce for CrossLine {
72    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
73        div()
74            .size_full()
75            .absolute()
76            .top_0()
77            .left_0()
78            .when(self.direction.show_vertical(), |this| {
79                this.child(
80                    div()
81                        .absolute()
82                        .w(px(1.))
83                        .bg(cx.theme().border)
84                        .top_0()
85                        .left(self.point.x)
86                        .map(|this| {
87                            if let Some(height) = self.height {
88                                this.h(px(height))
89                            } else {
90                                this.h_full()
91                            }
92                        }),
93                )
94            })
95            .when(self.direction.show_horizontal(), |this| {
96                this.child(
97                    div()
98                        .absolute()
99                        .w_full()
100                        .h(px(1.))
101                        .bg(cx.theme().border)
102                        .left_0()
103                        .top(self.point.y),
104                )
105            })
106    }
107}
108
109#[derive(IntoElement)]
110pub struct Dot {
111    point: Point<Pixels>,
112    size: Pixels,
113    stroke: Hsla,
114    fill: Hsla,
115}
116
117impl Dot {
118    pub fn new(point: Point<Pixels>) -> Self {
119        Self {
120            point,
121            size: px(6.),
122            stroke: gpui::transparent_black(),
123            fill: gpui::transparent_black(),
124        }
125    }
126
127    /// Set the size of the dot.
128    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
129        self.size = size.into();
130        self
131    }
132
133    /// Set the stroke of the dot.
134    pub fn stroke(mut self, stroke: Hsla) -> Self {
135        self.stroke = stroke;
136        self
137    }
138
139    /// Set the fill of the dot.
140    pub fn fill(mut self, fill: Hsla) -> Self {
141        self.fill = fill;
142        self
143    }
144}
145
146impl RenderOnce for Dot {
147    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
148        let border_width = px(1.);
149        let offset = self.size / 2. - border_width / 2.;
150
151        div()
152            .absolute()
153            .w(self.size)
154            .h(self.size)
155            .rounded_full()
156            .border(border_width)
157            .border_color(self.stroke)
158            .bg(self.fill)
159            .left(self.point.x - offset)
160            .top(self.point.y - offset)
161    }
162}
163
164#[derive(Clone, Copy, Default, PartialEq, Eq)]
165pub enum TooltipPosition {
166    #[default]
167    Left,
168    Right,
169}
170
171#[derive(Clone)]
172pub struct TooltipState {
173    pub index: usize,
174    pub cross_line: Point<Pixels>,
175    pub dots: Vec<Point<Pixels>>,
176    pub position: TooltipPosition,
177}
178
179impl TooltipState {
180    pub fn new(
181        index: usize,
182        cross_line: Point<Pixels>,
183        dots: Vec<Point<Pixels>>,
184        position: TooltipPosition,
185    ) -> Self {
186        Self {
187            index,
188            cross_line,
189            dots,
190            position,
191        }
192    }
193}
194
195#[derive(IntoElement)]
196pub struct Tooltip {
197    base: Div,
198    position: Option<TooltipPosition>,
199    gap: Pixels,
200    cross_line: Option<CrossLine>,
201    dots: Option<Vec<Dot>>,
202    appearance: bool,
203}
204
205impl Tooltip {
206    #[allow(clippy::new_without_default)]
207    pub fn new() -> Self {
208        Self {
209            base: v_flex().top_0(),
210            position: Default::default(),
211            gap: px(0.),
212            cross_line: None,
213            dots: None,
214            appearance: true,
215        }
216    }
217
218    /// Set the position of the tooltip.
219    pub fn position(mut self, position: TooltipPosition) -> Self {
220        self.position = Some(position);
221        self
222    }
223
224    /// Set the gap of the tooltip.
225    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
226        self.gap = gap.into();
227        self
228    }
229
230    /// Set the cross line of the tooltip.
231    pub fn cross_line(mut self, cross_line: CrossLine) -> Self {
232        self.cross_line = Some(cross_line);
233        self
234    }
235
236    /// Set the dots of the tooltip.
237    pub fn dots(mut self, dots: impl IntoIterator<Item = Dot>) -> Self {
238        self.dots = Some(dots.into_iter().collect());
239        self
240    }
241
242    /// Set the appearance of the tooltip.
243    pub fn appearance(mut self, appearance: bool) -> Self {
244        self.appearance = appearance;
245        self
246    }
247}
248
249impl Styled for Tooltip {
250    fn style(&mut self) -> &mut StyleRefinement {
251        self.base.style()
252    }
253}
254
255impl ParentElement for Tooltip {
256    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
257        self.base.extend(elements);
258    }
259}
260
261impl RenderOnce for Tooltip {
262    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
263        div()
264            .size_full()
265            .absolute()
266            .top_0()
267            .left_0()
268            .when_some(self.cross_line, |this, cross_line| this.child(cross_line))
269            .when_some(self.dots, |this, dots| this.children(dots))
270            .child(self.base.map(|this| {
271                if self.appearance {
272                    this.absolute()
273                        .min_w(px(168.))
274                        .p_2()
275                        .border_1()
276                        .border_color(cx.theme().border)
277                        .rounded_sm()
278                        .bg(cx.theme().background.opacity(0.9))
279                        .when_some(self.position, |this, position| {
280                            if position == TooltipPosition::Left {
281                                this.left(self.gap)
282                            } else {
283                                this.right(self.gap)
284                            }
285                        })
286                } else {
287                    this.size_full().relative()
288                }
289            }))
290    }
291}