1use gpui::{
2 AnyElement, App, Div, Half as _, Hsla, IntoElement, ParentElement, Pixels, Point, RenderOnce,
3 SharedString, Size, StyleRefinement, Styled, Window, deferred, div, prelude::FluentBuilder, px,
4};
5
6use crate::ThemeStyled as _;
7use crate::{ActiveTheme, Colorize, StyledExt, h_flex, v_flex};
8
9#[derive(Default)]
10pub enum CrossLineAxis {
11 #[default]
12 Vertical,
13 Horizontal,
14 Both,
15}
16
17impl CrossLineAxis {
18 #[inline]
20 pub fn show_vertical(&self) -> bool {
21 matches!(self, CrossLineAxis::Vertical | CrossLineAxis::Both)
22 }
23
24 #[inline]
26 pub fn show_horizontal(&self) -> bool {
27 matches!(self, CrossLineAxis::Horizontal | CrossLineAxis::Both)
28 }
29}
30
31#[derive(IntoElement)]
32pub struct CrossLine {
33 point: Point<Pixels>,
34 vertical: (f32, Option<f32>),
37 horizontal: (f32, Option<f32>),
40 thickness: Pixels,
42 dashed: bool,
44 direction: CrossLineAxis,
45}
46
47impl CrossLine {
48 pub fn new(point: Point<Pixels>) -> Self {
49 Self {
50 point,
51 vertical: (0., None),
52 horizontal: (0., None),
53 thickness: px(1.),
54 dashed: true,
55 direction: Default::default(),
56 }
57 }
58
59 pub fn band(mut self, thickness: impl Into<Pixels>) -> Self {
63 self.thickness = thickness.into();
64 self.dashed = false;
65 self
66 }
67
68 pub fn horizontal(mut self) -> Self {
70 self.direction = CrossLineAxis::Horizontal;
71 self
72 }
73
74 pub fn both(mut self) -> Self {
76 self.direction = CrossLineAxis::Both;
77 self
78 }
79
80 pub fn height(mut self, height: f32) -> Self {
82 self.vertical.1 = Some(height);
83 self
84 }
85
86 pub fn width(mut self, width: f32) -> Self {
88 self.horizontal.1 = Some(width);
89 self
90 }
91
92 pub fn span(mut self, start: f32, length: f32) -> Self {
95 self.vertical = (start, Some(length));
96 self
97 }
98
99 pub fn h_span(mut self, start: f32, length: f32) -> Self {
102 self.horizontal = (start, Some(length));
103 self
104 }
105}
106
107impl From<Point<Pixels>> for CrossLine {
108 fn from(value: Point<Pixels>) -> Self {
109 Self::new(value)
110 }
111}
112
113impl CrossLine {
114 fn line(&self, vertical: bool, cx: &App) -> Div {
118 let color = if self.dashed {
119 cx.theme().border.mix(cx.theme().foreground, 0.8)
120 } else {
121 cx.theme().foreground.opacity(0.08)
122 };
123 let thickness = if self.dashed { px(0.) } else { self.thickness };
125 let (start, length) = if vertical {
128 self.vertical
129 } else {
130 self.horizontal
131 };
132
133 let el = div().absolute();
134 let el = if vertical {
135 el.left(self.point.x - thickness * 0.5)
136 .w(thickness)
137 .top(px(start))
138 .map(|el| match length {
139 Some(length) => el.h(px(length)),
140 None => el.h_full(),
141 })
142 } else {
143 el.top(self.point.y - thickness * 0.5)
144 .h(thickness)
145 .left(px(start))
146 .map(|el| match length {
147 Some(length) => el.w(px(length)),
148 None => el.w_full(),
149 })
150 };
151
152 if self.dashed {
153 let el = if vertical {
154 el.border_l_1()
155 } else {
156 el.border_t_1()
157 };
158 el.border_dashed().border_color(color)
159 } else {
160 el.bg(color)
161 }
162 }
163}
164
165impl RenderOnce for CrossLine {
166 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
167 let vertical = self.direction.show_vertical().then(|| self.line(true, cx));
168 let horizontal = self
169 .direction
170 .show_horizontal()
171 .then(|| self.line(false, cx));
172
173 div()
174 .size_full()
175 .absolute()
176 .top_0()
177 .left_0()
178 .children(vertical)
179 .children(horizontal)
180 }
181}
182
183#[derive(IntoElement)]
184pub struct Dot {
185 point: Point<Pixels>,
186 size: Pixels,
187 stroke: Hsla,
188 fill: Hsla,
189}
190
191impl Dot {
192 pub fn new(point: Point<Pixels>) -> Self {
193 Self {
194 point,
195 size: px(6.),
196 stroke: gpui::transparent_black(),
197 fill: gpui::transparent_black(),
198 }
199 }
200
201 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
203 self.size = size.into();
204 self
205 }
206
207 pub fn stroke(mut self, stroke: Hsla) -> Self {
209 self.stroke = stroke;
210 self
211 }
212
213 pub fn fill(mut self, fill: Hsla) -> Self {
215 self.fill = fill;
216 self
217 }
218}
219
220impl RenderOnce for Dot {
221 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
222 let border_width = px(1.);
223 let offset = self.size / 2. - border_width / 2.;
224
225 div()
226 .absolute()
227 .w(self.size)
228 .h(self.size)
229 .rounded_full()
230 .border(border_width)
231 .border_color(self.stroke)
232 .bg(self.fill)
233 .left(self.point.x - offset)
234 .top(self.point.y - offset)
235 }
236}
237
238#[derive(Clone)]
239pub struct TooltipState {
240 pub index: usize,
241 pub cross_line: Point<Pixels>,
242 pub dots: Vec<Point<Pixels>>,
243}
244
245impl TooltipState {
246 pub fn new(index: usize, cross_line: Point<Pixels>, dots: Vec<Point<Pixels>>) -> Self {
247 Self {
248 index,
249 cross_line,
250 dots,
251 }
252 }
253}
254
255struct TooltipRow {
257 color: Hsla,
258 label: SharedString,
259 value: SharedString,
260}
261
262#[derive(IntoElement)]
263pub struct Tooltip {
264 base: Div,
265 gap: Pixels,
266 cross_line: Option<CrossLine>,
267 dots: Option<Vec<Dot>>,
268 appearance: bool,
269 title: Option<SharedString>,
270 rows: Vec<TooltipRow>,
271 cursor: Point<Pixels>,
273 within: Size<Pixels>,
276}
277
278impl Tooltip {
279 pub fn new(cursor: Point<Pixels>, within: Size<Pixels>) -> Self {
281 Self {
282 base: v_flex(),
283 gap: px(0.),
284 cross_line: None,
285 dots: None,
286 appearance: true,
287 title: None,
288 rows: Vec::new(),
289 cursor,
290 within,
291 }
292 }
293
294 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
296 self.title = Some(title.into());
297 self
298 }
299
300 pub fn row(
302 mut self,
303 color: impl Into<Hsla>,
304 label: impl Into<SharedString>,
305 value: impl Into<SharedString>,
306 ) -> Self {
307 self.rows.push(TooltipRow {
308 color: color.into(),
309 label: label.into(),
310 value: value.into(),
311 });
312 self
313 }
314
315 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
317 self.gap = gap.into();
318 self
319 }
320
321 pub fn cross_line(mut self, cross_line: CrossLine) -> Self {
323 self.cross_line = Some(cross_line);
324 self
325 }
326
327 pub fn dots(mut self, dots: impl IntoIterator<Item = Dot>) -> Self {
329 self.dots = Some(dots.into_iter().collect());
330 self
331 }
332
333 pub fn appearance(mut self, appearance: bool) -> Self {
335 self.appearance = appearance;
336 self
337 }
338}
339
340impl Styled for Tooltip {
341 fn style(&mut self) -> &mut StyleRefinement {
342 self.base.style()
343 }
344}
345
346impl ParentElement for Tooltip {
347 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
348 self.base.extend(elements);
349 }
350}
351
352impl RenderOnce for Tooltip {
353 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
354 let Tooltip {
355 base,
356 gap,
357 cross_line,
358 dots,
359 appearance,
360 title,
361 rows,
362 cursor,
363 within,
364 } = self;
365
366 let content = if title.is_some() || !rows.is_empty() {
368 v_flex()
369 .text_sm()
370 .gap_1()
371 .when_some(title, |this, title| {
372 this.child(div().font_semibold().child(title))
373 })
374 .children(rows.into_iter().map(|row| {
375 h_flex()
376 .items_center()
377 .justify_between()
378 .gap_3()
379 .child(
380 h_flex()
381 .items_center()
382 .gap_1p5()
383 .child(
384 div()
385 .size_2()
386 .rounded(cx.theme().radius.half())
387 .bg(row.color),
388 )
389 .child(
390 div()
391 .text_color(cx.theme().muted_foreground)
392 .child(row.label),
393 ),
394 )
395 .child(div().child(row.value))
396 }))
397 } else {
398 base
399 };
400
401 div()
402 .size_full()
403 .absolute()
404 .top_0()
405 .left_0()
406 .when_some(cross_line, |this, cross_line| this.child(cross_line))
407 .when_some(dots, |this, dots| this.children(dots))
408 .child(deferred(content.map(|mut this| {
412 if !appearance {
413 return this.size_full().relative();
414 }
415
416 let min_w_unset = this.style().min_size.width.is_none();
419
420 this.absolute()
423 .when(min_w_unset, |c| c.min_w(px(150.)))
424 .popover_style(cx)
425 .p_2()
426 .map(|c| {
427 if cursor.x < within.width * 0.5 {
428 c.left(cursor.x + gap)
429 } else {
430 c.right(within.width - cursor.x + gap)
431 }
432 })
433 .map(|c| {
434 if cursor.y < within.height * 0.5 {
435 c.top(cursor.y + gap)
436 } else {
437 c.bottom(within.height - cursor.y + gap)
438 }
439 })
440 })))
441 }
442}