1mod area_chart;
2mod bar_chart;
3mod candlestick_chart;
4mod line_chart;
5mod pie_chart;
6mod radar_chart;
7mod sankey_chart;
8
9pub use area_chart::AreaChart;
10pub use bar_chart::BarChart;
11pub use candlestick_chart::CandlestickChart;
12pub use line_chart::LineChart;
13pub use pie_chart::PieChart;
14pub use radar_chart::{RadarChart, RadarLabel};
15pub use sankey_chart::{SankeyChart, SankeyLabel};
16
17use std::{
18 hash::{DefaultHasher, Hash, Hasher},
19 panic::Location,
20 rc::Rc,
21};
22
23use gpui::{
24 AnyElement, App, Bounds, ContentMask, ElementId, Hsla, IntoElement, ParentElement as _, Pixels,
25 SharedString, Size, TextAlign, Window, point, px,
26};
27
28use crate::{
29 ActiveTheme,
30 plot::{
31 AxisLabelPlacement, AxisText, Grid, PlotAppear, PlotLabel,
32 label::{TEXT_GAP, TEXT_HEIGHT, TEXT_SIZE, Text, measure_text_width},
33 scale::{PlotValue, Scale, ScaleBand, ScaleLinear, ScalePoint},
34 tooltip::Tooltip,
35 },
36};
37
38#[track_caller]
53pub(crate) fn caller_id() -> ElementId {
54 ElementId::CodeLocation(*Location::caller())
55}
56
57pub(crate) struct ChartAppear {
60 enabled: bool,
61 generation: u64,
62 current: PlotAppear,
63}
64
65impl Default for ChartAppear {
66 fn default() -> Self {
67 Self {
68 enabled: true,
69 generation: 0,
70 current: PlotAppear::complete(),
71 }
72 }
73}
74
75impl ChartAppear {
76 pub(crate) fn set_enabled(&mut self, enabled: bool) {
77 self.enabled = enabled;
78 }
79
80 pub(crate) fn set_key(&mut self, key: impl Hash) {
81 let mut hasher = DefaultHasher::new();
82 key.hash(&mut hasher);
83 self.generation = hasher.finish();
84 }
85
86 pub(crate) fn generation(&self) -> Option<u64> {
91 self.enabled.then_some(self.generation)
92 }
93
94 pub(crate) fn update(&mut self, appear: PlotAppear) {
95 self.current = appear;
96 }
97
98 pub(crate) fn get(&self) -> &PlotAppear {
99 &self.current
100 }
101}
102
103pub(crate) fn reveal_mask(
109 bounds: Bounds<Pixels>,
110 left: f32,
111 progress: f32,
112) -> Option<ContentMask<Pixels>> {
113 if progress >= 1. {
114 return None;
115 }
116 let bleed = HOVER_DOT_SIZE / 2.;
117 let start = bounds.left() + px(left) - bleed;
118 let end = start + (bounds.right() + bleed - start) * progress.max(0.);
119 Some(ContentMask {
120 bounds: Bounds::from_corners(
121 gpui::point(bounds.left() - bleed, bounds.top() - bleed),
122 gpui::point(end, bounds.bottom() + bleed),
123 ),
124 })
125}
126
127pub(crate) const HOVER_DOT_SIZE: Pixels = px(8.);
129
130pub(crate) const HOVER_HALO_SIZE: Pixels = px(20.);
135
136pub(crate) fn axis_point_count(point_count: Option<usize>, data_len: usize) -> usize {
140 point_count.unwrap_or(data_len).max(data_len)
141}
142
143pub(crate) fn point_range(start: f32, width: f32, data_len: usize, point_count: usize) -> [f32; 2] {
148 let end = if point_count > 1 {
149 width * data_len.saturating_sub(1) as f32 / (point_count - 1) as f32
150 } else {
151 width
152 };
153 [start, start + end]
154}
155
156#[derive(Clone, Copy)]
159pub(crate) struct ValueExtent {
160 lo: f64,
161 hi: f64,
162 bottom: f32,
163 top: f32,
164}
165
166impl ValueExtent {
167 pub(crate) fn value_at(&self, y: f32) -> f64 {
169 if self.bottom == self.top {
170 return self.lo;
171 }
172 self.lo + (self.hi - self.lo) * ((self.bottom - y) / (self.bottom - self.top)) as f64
173 }
174
175 pub(crate) fn position_of(&self, value: f64) -> Option<f32> {
177 if self.hi == self.lo {
178 return None;
179 }
180 Some(
181 self.bottom
182 - ((value - self.lo) / (self.hi - self.lo)) as f32 * (self.bottom - self.top),
183 )
184 }
185}
186
187pub(crate) fn point_value_scale<Y>(
193 values: impl IntoIterator<Item = Y>,
194 domain: Option<(Y, Y)>,
195 height: f32,
196 (top, bottom): (f32, f32),
197) -> (ScaleLinear<Y>, ValueExtent)
198where
199 Y: PlotValue,
200{
201 let domain: Vec<Y> = match domain {
202 Some((min, max)) => vec![min, max],
203 None => values.into_iter().chain(Some(Y::zero())).collect(),
204 };
205 let (lo, hi) = domain
206 .iter()
207 .filter_map(|v| v.to_f64())
208 .fold((f64::MAX, f64::MIN), |(lo, hi), v| (lo.min(v), hi.max(v)));
209 let extent = ValueExtent {
210 lo,
211 hi,
212 bottom: height - bottom,
213 top,
214 };
215 (ScaleLinear::new(domain, [height - bottom, top]), extent)
216}
217
218pub(crate) const AXIS_GAP: f32 = TEXT_SIZE + TEXT_GAP * 4.;
222
223pub(crate) const MAX_BAND_WIDTH: f32 = 30.;
226
227pub(crate) const VALUE_AXIS_GAP: f32 = 32.;
230
231pub(crate) fn value_axis_gap(
237 labels: impl IntoIterator<Item = SharedString>,
238 window: &mut Window,
239) -> f32 {
240 labels
241 .into_iter()
242 .map(|label| measure_text_width(&label, px(TEXT_SIZE), window) + TEXT_GAP * 2.)
243 .fold(VALUE_AXIS_GAP, f32::max)
244}
245
246pub(crate) type TickFormat = Rc<dyn Fn(f64) -> SharedString>;
248
249pub(crate) fn format_tick(value: f64) -> SharedString {
251 if (value - value.round()).abs() < 0.001 {
252 format!("{:.0}", value).into()
253 } else {
254 format!("{:.1}", value).into()
255 }
256}
257
258pub(crate) fn labeled_items(
261 len: usize,
262 label_count: Option<usize>,
263 tick_margin: usize,
264) -> Vec<bool> {
265 match label_count {
266 Some(count) => {
267 let mut labeled = vec![false; len];
268 match count {
269 0 => {}
270 1 => labeled.iter_mut().take(1).for_each(|l| *l = true),
271 count if count >= len => labeled.iter_mut().for_each(|l| *l = true),
272 count => {
273 for k in 0..count {
274 let ix =
275 (k as f32 * (len - 1) as f32 / (count - 1) as f32).round() as usize;
276 labeled[ix] = true;
277 }
278 }
279 }
280 labeled
281 }
282 None => (0..len).map(|i| (i + 1) % tick_margin == 0).collect(),
283 }
284}
285
286pub(crate) struct TooltipContent<T> {
290 title: Option<Rc<dyn Fn(&T) -> SharedString>>,
291 value: Option<Rc<dyn Fn(&T, usize, f64) -> SharedString>>,
292 value_color: Option<Rc<dyn Fn(&T, usize, f64) -> Hsla>>,
293 content: Option<Rc<dyn Fn(&T, &mut Window, &mut App) -> AnyElement>>,
294}
295
296impl<T> Default for TooltipContent<T> {
297 fn default() -> Self {
298 Self {
299 title: None,
300 value: None,
301 value_color: None,
302 content: None,
303 }
304 }
305}
306
307impl<T: 'static> TooltipContent<T> {
308 pub(crate) fn set_title(&mut self, title: impl Fn(&T) -> SharedString + 'static) {
309 self.title = Some(Rc::new(title));
310 }
311
312 pub(crate) fn set_value(&mut self, value: impl Fn(&T, usize, f64) -> SharedString + 'static) {
313 self.value = Some(Rc::new(value));
314 }
315
316 pub(crate) fn set_value_color<H: Into<Hsla>>(
317 &mut self,
318 color: impl Fn(&T, usize, f64) -> H + 'static,
319 ) {
320 self.value_color = Some(Rc::new(move |d, ix, value| color(d, ix, value).into()));
321 }
322
323 pub(crate) fn set_content<E: IntoElement>(
324 &mut self,
325 content: impl Fn(&T, &mut Window, &mut App) -> E + 'static,
326 ) {
327 self.content = Some(Rc::new(move |d, window, cx| {
328 content(d, window, cx).into_any_element()
329 }));
330 }
331
332 fn title_text(&self, d: &T, fallback: Option<SharedString>) -> Option<SharedString> {
335 match self.title.as_ref() {
336 Some(title) => Some(title(d)),
337 None => fallback,
338 }
339 }
340
341 fn value_text(&self, d: &T, ix: usize, value: f64) -> SharedString {
343 match self.value.as_ref() {
344 Some(text) => text(d, ix, value),
345 None => format!("{}", value).into(),
346 }
347 }
348
349 pub(crate) fn apply<R>(
354 &self,
355 tooltip: Tooltip,
356 d: &T,
357 title: impl FnOnce() -> Option<SharedString>,
358 rows: impl FnOnce() -> Option<R>,
359 window: &mut Window,
360 cx: &mut App,
361 ) -> Option<Tooltip>
362 where
363 R: IntoIterator<Item = (Hsla, SharedString, f64)>,
364 {
365 if let Some(content) = self.content.as_ref() {
366 return Some(tooltip.child(content(d, window, cx)));
367 }
368 let mut tooltip = match self.title_text(d, title()) {
369 Some(title) => tooltip.title(title),
370 None => tooltip,
371 };
372 for (ix, (swatch, name, value)) in rows()?.into_iter().enumerate() {
373 tooltip = tooltip.row(swatch, name, self.value_text(d, ix, value));
374 if let Some(color) = self.value_color.as_ref() {
375 tooltip = tooltip.value_color(color(d, ix, value));
376 }
377 }
378 Some(tooltip)
379 }
380}
381
382pub(crate) struct PointAxes {
385 pub(crate) y_axis: bool,
386 pub(crate) y_axis_label_placement: AxisLabelPlacement,
387 pub(crate) y_tick_count: usize,
388 pub(crate) y_tick_format: Option<TickFormat>,
389 pub(crate) x_tick_count: Option<usize>,
390 pub(crate) grid_columns: usize,
391 pub(crate) grid_dashed: bool,
392 pub(crate) y_padding: (f32, f32),
393 pub(crate) reference_lines: Vec<f64>,
394 y_label_gap: f32,
396}
397
398impl Default for PointAxes {
399 fn default() -> Self {
400 Self {
401 y_axis: false,
402 y_axis_label_placement: AxisLabelPlacement::default(),
403 y_tick_count: 5,
404 y_tick_format: None,
405 x_tick_count: None,
406 grid_columns: 0,
407 grid_dashed: true,
408 y_padding: (10., 0.),
409 reference_lines: vec![],
410 y_label_gap: VALUE_AXIS_GAP,
411 }
412 }
413}
414
415impl PointAxes {
416 pub(crate) fn plot_left(&self) -> f32 {
419 if self.y_axis && self.y_axis_label_placement == AxisLabelPlacement::Outside {
420 self.y_label_gap
421 } else {
422 0.
423 }
424 }
425
426 pub(crate) fn measure_y_labels(
429 &mut self,
430 extent: ValueExtent,
431 height: f32,
432 window: &mut Window,
433 ) {
434 if self.y_axis && self.y_axis_label_placement == AxisLabelPlacement::Outside {
435 let labels = self
436 .y_tick_labels(extent, height)
437 .into_iter()
438 .map(|(_, text)| text);
439 self.y_label_gap = value_axis_gap(labels, window);
440 }
441 }
442
443 fn y_tick_labels(&self, extent: ValueExtent, height: f32) -> Vec<(f32, SharedString)> {
445 self.tick_positions(height)
446 .into_iter()
447 .map(|y| {
448 let value = extent.value_at(y);
449 let text = match self.y_tick_format.as_ref() {
450 Some(format) => format(value),
451 None => format_tick(value),
452 };
453 (y, text)
454 })
455 .collect()
456 }
457
458 pub(crate) fn plot_bounds(&self, bounds: Bounds<Pixels>, height: f32) -> Bounds<Pixels> {
461 let left = self.plot_left();
462 Bounds {
463 origin: bounds.origin + point(px(left), px(0.)),
464 size: Size::new(bounds.size.width - px(left), px(height)),
465 }
466 }
467
468 fn tick_positions(&self, height: f32) -> Vec<f32> {
471 let count = self.y_tick_count.max(2);
472 (0..count)
473 .map(|i| height * i as f32 / (count - 1) as f32)
474 .collect()
475 }
476
477 pub(crate) fn paint_grid(
480 &self,
481 bounds: Bounds<Pixels>,
482 height: f32,
483 window: &mut Window,
484 cx: &mut App,
485 ) {
486 let plot = self.plot_bounds(bounds, height);
487 let mut rows = self.tick_positions(height);
488 rows.pop();
489 let width = plot.size.width.as_f32();
490 let columns: Vec<f32> = (0..self.grid_columns)
491 .map(|i| width * i as f32 / self.grid_columns as f32)
492 .collect();
493 let grid = Grid::new().y(rows).x(columns).stroke(cx.theme().chart_grid);
494 let grid = if self.grid_dashed {
495 grid.dash_array(&[px(4.), px(2.)])
496 } else {
497 grid
498 };
499 grid.paint(&plot, window);
500 }
501
502 pub(crate) fn paint_reference_lines(
505 &self,
506 extent: ValueExtent,
507 bounds: Bounds<Pixels>,
508 height: f32,
509 window: &mut Window,
510 cx: &mut App,
511 ) {
512 let rows: Vec<f32> = self
513 .reference_lines
514 .iter()
515 .filter_map(|v| extent.position_of(*v))
516 .filter(|y| (0. ..=height).contains(y))
517 .collect();
518 if rows.is_empty() {
519 return;
520 }
521 Grid::new()
522 .y(rows)
523 .stroke(cx.theme().muted_foreground)
524 .dash_array(&[px(4.), px(2.)])
525 .paint(&self.plot_bounds(bounds, height), window);
526 }
527
528 pub(crate) fn paint_y_labels(
530 &self,
531 extent: ValueExtent,
532 bounds: Bounds<Pixels>,
533 height: f32,
534 window: &mut Window,
535 cx: &mut App,
536 ) {
537 if !self.y_axis {
538 return;
539 }
540 let color = cx.theme().muted_foreground;
541 let labels = self
542 .y_tick_labels(extent, height)
543 .into_iter()
544 .map(|(y, text)| {
545 match self.y_axis_label_placement {
546 AxisLabelPlacement::Inside => {
549 let top = if y < TEXT_HEIGHT {
550 y + TEXT_GAP
551 } else {
552 y - TEXT_HEIGHT
553 };
554 Text::new(text, point(TEXT_GAP, top), color)
555 }
556 AxisLabelPlacement::Outside => {
557 let top = (y - TEXT_SIZE / 2.).clamp(0., (height - TEXT_SIZE).max(0.));
558 Text::new(text, point(self.y_label_gap - TEXT_GAP * 2., top), color)
559 .align(TextAlign::Right)
560 }
561 }
562 })
563 .collect();
564 PlotLabel::new(labels).paint(&bounds, window, cx);
565 }
566}
567
568pub(crate) fn pinned_plot_mask(bounds: Bounds<Pixels>, height: f32) -> ContentMask<Pixels> {
573 let bleed = HOVER_DOT_SIZE / 2.;
574 ContentMask {
575 bounds: Bounds::from_corners(
576 bounds.origin - gpui::point(bleed, bleed),
577 gpui::point(bounds.right() + bleed, bounds.top() + px(height) + bleed),
578 ),
579 }
580}
581
582pub(crate) fn build_point_x_labels<T, X>(
588 data: &[T],
589 x_fn: &dyn Fn(&T) -> X,
590 x_scale: &ScalePoint<X>,
591 point_count: usize,
592 labeled: &[bool],
593 color: Hsla,
594) -> Vec<AxisText>
595where
596 X: PartialEq + Into<SharedString>,
597{
598 data.iter()
599 .enumerate()
600 .filter_map(|(i, d)| {
601 if !labeled.get(i).copied().unwrap_or(false) {
602 return None;
603 }
604 x_scale.tick_at(i).map(|x_tick| {
605 let align = match i {
606 0 if point_count == 1 => TextAlign::Center,
607 0 => TextAlign::Left,
608 i if i == point_count - 1 => TextAlign::Right,
609 _ => TextAlign::Center,
610 };
611 AxisText::new(x_fn(d).into(), x_tick, color).align(align)
612 })
613 })
614 .collect()
615}
616
617pub(crate) fn build_band_labels<T, X>(
624 data: &[T],
625 x_fn: &dyn Fn(&T) -> X,
626 x_scale: &ScaleBand<X>,
627 band_width: f32,
628 labeled: &[bool],
629 color: Hsla,
630) -> Vec<AxisText>
631where
632 X: Eq + Hash + Into<SharedString>,
633{
634 data.iter()
635 .enumerate()
636 .filter_map(|(i, d)| {
637 if !labeled.get(i).copied().unwrap_or(false) {
638 return None;
639 }
640 x_scale.tick(&x_fn(d)).map(|x_tick| {
641 AxisText::new(x_fn(d).into(), x_tick + band_width / 2., color)
643 .align(TextAlign::Center)
644 })
645 })
646 .collect()
647}
648
649#[cfg(test)]
650mod tests {
651 use std::cell::Cell;
652
653 use gpui::{Hsla, SharedString, TestAppContext, blue, div, green, point, px, red, size};
654
655 use super::TooltipContent;
656 use crate::{
657 chart::PieChart,
658 plot::{Plot, tooltip::Tooltip},
659 };
660
661 fn chart() -> PieChart<f32> {
662 PieChart::new([1., 2.])
663 }
664
665 #[test]
668 fn a_chart_is_interactive_without_being_given_an_id() {
669 assert!(Plot::id(&chart()).is_some());
670 }
671
672 #[test]
674 fn charts_built_at_different_sites_get_different_ids() {
675 assert_ne!(
676 Plot::id(&PieChart::new([1.])),
677 Plot::id(&PieChart::new([1.]))
678 );
679 }
680
681 #[test]
683 fn charts_built_at_one_site_share_an_id() {
684 assert_eq!(Plot::id(&chart()), Plot::id(&chart()));
685 }
686
687 #[test]
691 fn a_chart_turned_off_keeps_its_id_but_not_its_hitbox() {
692 let off = chart().interactive(false);
693 assert!(!Plot::interactive(&off));
694 assert!(Plot::id(&off).is_some());
695 assert_eq!(
696 Plot::id(&chart().id("pie").interactive(false)),
697 Some("pie".into())
698 );
699 }
700
701 #[test]
704 fn an_appear_key_replays_the_appear() {
705 assert_eq!(Plot::appear_generation(&chart()), Some(0));
706 assert_eq!(Plot::appear_generation(&chart().appear(false)), None);
707 let a = Plot::appear_generation(&chart().appear_key("AAPL.US"));
708 let b = Plot::appear_generation(&chart().appear_key("TSLA.US"));
709 assert_ne!(a, b);
710 assert_eq!(a, Plot::appear_generation(&chart().appear_key("AAPL.US")));
711 }
712
713 #[test]
716 fn only_the_last_point_right_aligns_its_label() {
717 use gpui::{Hsla, TextAlign};
718
719 use super::{build_point_x_labels, point_range};
720 use crate::plot::scale::ScalePoint;
721
722 let data = ["a", "b", "c"];
723 let align = |point_count| {
724 let x = ScalePoint::new(
725 data.to_vec(),
726 point_range(0., 100., data.len(), point_count),
727 );
728 build_point_x_labels(
729 &data,
730 &|d: &&'static str| *d,
731 &x,
732 point_count,
733 &[true; 3],
734 Hsla::default(),
735 )
736 .into_iter()
737 .map(|label| label.align)
738 .collect::<Vec<_>>()
739 };
740
741 assert_eq!(
742 align(3),
743 [TextAlign::Left, TextAlign::Center, TextAlign::Right]
744 );
745 assert_eq!(
746 align(5),
747 [TextAlign::Left, TextAlign::Center, TextAlign::Center]
748 );
749 }
750
751 #[test]
752 fn a_named_id_replaces_the_default() {
753 assert_eq!(
754 Plot::id(&chart().id("pie")),
755 Some(gpui::ElementId::Name("pie".into()))
756 );
757 }
758
759 #[test]
762 fn the_default_ticks_keep_the_grid_in_place() {
763 let axes = super::PointAxes::default();
764 let mut rows = axes.tick_positions(100.);
765 rows.pop();
766 assert_eq!(rows, vec![0., 25., 50., 75.]);
767 }
768
769 #[test]
770 fn a_label_count_spreads_labels_from_the_first_item_to_the_last() {
771 use super::labeled_items;
772
773 let shown = |len, count| {
774 labeled_items(len, Some(count), 1)
775 .iter()
776 .enumerate()
777 .filter_map(|(i, &on)| on.then_some(i))
778 .collect::<Vec<_>>()
779 };
780 assert_eq!(shown(11, 3), vec![0, 5, 10]);
781 assert_eq!(shown(10, 2), vec![0, 9]);
782 assert_eq!(shown(3, 5), vec![0, 1, 2]);
783 assert_eq!(shown(4, 1), vec![0]);
784 assert!(shown(4, 0).is_empty());
785
786 assert_eq!(labeled_items(4, None, 2), vec![false, true, false, true]);
788 }
789
790 #[test]
793 fn a_tick_reads_the_value_at_its_height() {
794 use super::point_value_scale;
795
796 let (_, extent) = point_value_scale([10., 20.], None, 110., (10., 0.));
797 assert_eq!(extent.value_at(110.), 0.);
798 assert_eq!(extent.value_at(10.), 20.);
799 assert!((extent.value_at(0.) - 22.).abs() < 1e-4);
800 assert_eq!(extent.position_of(20.), Some(10.));
801
802 let (_, extent) = point_value_scale([0.], Some((100., 200.)), 100., (0., 0.));
803 assert_eq!(extent.value_at(0.), 200.);
804 assert_eq!(extent.position_of(150.), Some(50.));
805 }
806
807 #[test]
810 fn tooltip_text_falls_back_to_the_chart_own() {
811 let mut content = TooltipContent::<f64>::default();
812 assert_eq!(
813 content.title_text(&1., Some("Jan".into())),
814 Some("Jan".into())
815 );
816 assert_eq!(content.title_text(&1., None), None);
817 assert_eq!(content.value_text(&1., 0, 1234.5).as_ref(), "1234.5");
818
819 content.set_title(|d| format!("Day {d}").into());
820 content.set_value(|_, _, value| format!("${value:.2}").into());
821 assert_eq!(content.title_text(&3., None), Some("Day 3".into()));
822 assert_eq!(content.value_text(&3., 0, 1234.5).as_ref(), "$1234.50");
823 }
824
825 #[gpui::test]
828 fn tooltip_fill_writes_each_row_with_the_value_color(cx: &mut TestAppContext) {
829 let mut content = TooltipContent::<f64>::default();
830 content.set_value(|_, ix, value| format!("{ix}: {value:+}").into());
831 content.set_value_color(|_, _, value| if value >= 0. { green() } else { red() });
832 let cx = cx.add_empty_window();
833 let tooltip = cx
834 .update(|window, cx| {
835 content.apply(
836 Tooltip::new(point(px(0.), px(0.)), size(px(100.), px(100.))),
837 &1.,
838 || Some("Jan".into()),
839 || Some([(blue(), "Open".into(), 2.), (blue(), "Close".into(), -1.)]),
840 window,
841 cx,
842 )
843 })
844 .expect("rows are given");
845
846 assert_eq!(tooltip.title_for_test().map(|t| t.as_ref()), Some("Jan"));
847 assert_eq!(
848 tooltip.rows_for_test(),
849 vec![
850 ("0: +2".into(), Some(green())),
851 ("1: -1".into(), Some(red()))
852 ]
853 );
854 }
855
856 #[gpui::test]
859 fn tooltip_fill_leaves_the_title_off_without_one(cx: &mut TestAppContext) {
860 let content = TooltipContent::<f64>::default();
861 let cx = cx.add_empty_window();
862 let tooltip = cx
863 .update(|window, cx| {
864 content.apply(
865 Tooltip::new(point(px(0.), px(0.)), size(px(100.), px(100.))),
866 &1.,
867 || None,
868 || Some([(blue(), "Alpha".into(), 80.)]),
869 window,
870 cx,
871 )
872 })
873 .expect("rows are given");
874
875 assert!(tooltip.title_for_test().is_none());
876 assert_eq!(tooltip.rows_for_test(), vec![("80".into(), None)]);
877 }
878
879 #[gpui::test]
882 fn tooltip_fill_renders_the_caller_content_without_building_rows(cx: &mut TestAppContext) {
883 let mut content = TooltipContent::<f64>::default();
884 content.set_title(|_| "Caller".into());
885 content.set_content(|_, _, _| div());
886 let built = Cell::new(false);
887 let cx = cx.add_empty_window();
888 let tooltip = cx.update(|window, cx| {
889 content.apply(
890 Tooltip::new(point(px(0.), px(0.)), size(px(100.), px(100.))),
891 &1.,
892 || {
893 built.set(true);
894 Some("Jan".into())
895 },
896 || -> Option<[(Hsla, SharedString, f64); 0]> {
897 built.set(true);
898 None
899 },
900 window,
901 cx,
902 )
903 });
904
905 let tooltip = tooltip.expect("the caller renders");
906 assert!(!built.get());
907 assert!(tooltip.title_for_test().is_none());
908 assert!(tooltip.rows_for_test().is_empty());
909 }
910}