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 #[inline]
19 pub fn show_vertical(&self) -> bool {
20 matches!(self, CrossLineAxis::Vertical | CrossLineAxis::Both)
21 }
22
23 #[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 pub fn horizontal(mut self) -> Self {
48 self.direction = CrossLineAxis::Horizontal;
49 self
50 }
51
52 pub fn both(mut self) -> Self {
54 self.direction = CrossLineAxis::Both;
55 self
56 }
57
58 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 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
129 self.size = size.into();
130 self
131 }
132
133 pub fn stroke(mut self, stroke: Hsla) -> Self {
135 self.stroke = stroke;
136 self
137 }
138
139 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 pub fn position(mut self, position: TooltipPosition) -> Self {
220 self.position = Some(position);
221 self
222 }
223
224 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
226 self.gap = gap.into();
227 self
228 }
229
230 pub fn cross_line(mut self, cross_line: CrossLine) -> Self {
232 self.cross_line = Some(cross_line);
233 self
234 }
235
236 pub fn dots(mut self, dots: impl IntoIterator<Item = Dot>) -> Self {
238 self.dots = Some(dots.into_iter().collect());
239 self
240 }
241
242 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}