Skip to main content

gpui_component/chart/
pie_chart.rs

1use std::{hash::Hash, rc::Rc};
2
3use gpui::{
4    AnyElement, App, Bounds, ElementId, Hsla, IntoElement, Pixels, Point, SharedString, TextAlign,
5    Window, point, px,
6};
7use gpui_base::motion::spring;
8use gpui_component_macros::IntoPlot;
9use num_traits::Zero;
10
11use super::{ChartAppear, caller_id};
12use crate::{
13    ActiveTheme,
14    plot::{
15        PathCaches, Plot, PlotAppear,
16        label::{PlotLabel, TEXT_HEIGHT, TEXT_SIZE, Text},
17        polygon,
18        shape::{Arc, ArcData, Pie},
19        tooltip::{PlotHover, Tooltip, TooltipState},
20    },
21};
22
23/// The default extra gap (in pixels) between `outer_radius` and the label radius.
24const DEFAULT_LABEL_GAP: f32 = 15.;
25
26/// How far the hovered slice moves out past its outer radius, in pixels.
27const HOVER_LIFT: f32 = 6.;
28
29/// How much the slices other than the hovered one fade, as a share of their opacity.
30const HOVER_DIM: f32 = 0.35;
31
32/// How far into the appear the leader-line labels start fading in.
33const LABEL_APPEAR_START: f32 = 0.7;
34
35/// The hover a pie chart paints, sampled once per frame in [`Plot::hover`].
36struct PieHover {
37    /// How far each datum's slice has lifted, `0..=1`, springing up on the
38    /// hovered slice and back down on the one the cursor left.
39    lift: Vec<f32>,
40    /// How far the hover has faded in.
41    focus: f32,
42}
43
44#[derive(IntoPlot)]
45pub struct PieChart<T: 'static> {
46    data: Vec<T>,
47    inner_radius: f32,
48    inner_radius_fn: Option<Rc<dyn Fn(&ArcData<T>) -> f32 + 'static>>,
49    outer_radius: f32,
50    outer_radius_fn: Option<Rc<dyn Fn(&ArcData<T>) -> f32 + 'static>>,
51    pad_angle: f32,
52    value: Option<Rc<dyn Fn(&T) -> f32>>,
53    color: Option<Rc<dyn Fn(&T) -> Hsla>>,
54    label: Option<Rc<dyn Fn(&T) -> SharedString + 'static>>,
55    label_line_color: Option<Rc<dyn Fn(&T) -> Hsla + 'static>>,
56    label_color: Option<Hsla>,
57    label_gap: f32,
58    tooltip_name: Option<Rc<dyn Fn(&T) -> SharedString + 'static>>,
59    tooltip_value: Option<Rc<dyn Fn(&T, f32, f32) -> SharedString + 'static>>,
60    id: ElementId,
61    interactive: bool,
62    appear: ChartAppear,
63    name: Option<SharedString>,
64    hover: Option<PieHover>,
65}
66
67impl<T> PieChart<T> {
68    #[track_caller]
69    pub fn new<I>(data: I) -> Self
70    where
71        I: IntoIterator<Item = T>,
72    {
73        Self {
74            data: data.into_iter().collect(),
75            inner_radius: 0.,
76            inner_radius_fn: None,
77            outer_radius: 0.,
78            outer_radius_fn: None,
79            pad_angle: 0.,
80            value: None,
81            color: None,
82            label: None,
83            label_line_color: None,
84            label_color: None,
85            label_gap: DEFAULT_LABEL_GAP,
86            tooltip_name: None,
87            tooltip_value: None,
88            id: caller_id(),
89            interactive: true,
90            appear: ChartAppear::default(),
91            name: None,
92            hover: None,
93        }
94    }
95
96    /// Name this chart's [`ElementId`], replacing the default taken from the
97    /// construction site.
98    ///
99    /// Pass one where a single construction site renders several of these
100    /// charts as siblings: they share the default id, and with it one hover
101    /// state and one path cache. The id must be unique among those siblings.
102    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
103        self.id = id.into();
104        self
105    }
106
107    /// Turn this chart's interactive layer on or off. On by default.
108    ///
109    /// The layer is the hitbox under the cursor and what it drives: the hovered
110    /// slice lifts out of the ring, and a tooltip shows its value and share. Turn
111    /// it off for a chart that only decorates, or one an element above it wants
112    /// the cursor for: without a hitbox it neither answers the mouse nor takes
113    /// the hover from what sits over it.
114    pub fn interactive(mut self, interactive: bool) -> Self {
115        self.interactive = interactive;
116        self
117    }
118
119    /// Draw the data in the first time this chart is painted. On by default.
120    ///
121    /// The theme sets how long it takes, and the system's reduced-motion
122    /// setting skips it. Turn it off for a chart that is painted again and
123    /// again as it scrolls in and out of view, such as one in each row of a
124    /// long list, where it would draw in every time.
125    pub fn appear(mut self, appear: bool) -> Self {
126        self.appear.set_enabled(appear);
127        self
128    }
129
130    /// Draw the data in again whenever `key` changes, such as the symbol or
131    /// period a chart shows.
132    ///
133    /// Without one the data draws in once, and later data paints in place.
134    pub fn appear_key(mut self, key: impl Hash) -> Self {
135        self.appear.set_key(key);
136        self
137    }
138
139    /// Set the series name shown in the hover tooltip row (e.g. "Desktop").
140    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
141        self.name = Some(name.into());
142        self
143    }
144
145    /// Set the inner radius of the pie chart.
146    pub fn inner_radius(mut self, inner_radius: f32) -> Self {
147        self.inner_radius = inner_radius;
148        self
149    }
150
151    /// Set the inner radius of the pie chart based on the arc data.
152    pub fn inner_radius_fn(
153        mut self,
154        inner_radius_fn: impl Fn(&ArcData<T>) -> f32 + 'static,
155    ) -> Self {
156        self.inner_radius_fn = Some(Rc::new(inner_radius_fn));
157        self
158    }
159
160    fn get_inner_radius(&self, arc: &ArcData<T>) -> f32 {
161        if let Some(inner_radius_fn) = self.inner_radius_fn.as_ref() {
162            inner_radius_fn(arc)
163        } else {
164            self.inner_radius
165        }
166    }
167
168    /// Set the outer radius of the pie chart.
169    pub fn outer_radius(mut self, outer_radius: f32) -> Self {
170        self.outer_radius = outer_radius;
171        self
172    }
173
174    /// Set the outer radius of the pie chart based on the arc data.
175    pub fn outer_radius_fn(
176        mut self,
177        outer_radius_fn: impl Fn(&ArcData<T>) -> f32 + 'static,
178    ) -> Self {
179        self.outer_radius_fn = Some(Rc::new(outer_radius_fn));
180        self
181    }
182
183    /// The outer radius of `arc`'s slice: the per-slice one, or `default`.
184    /// `self.outer_radius` is zero until a caller sets it, so the radius the
185    /// ring is laid out with comes from [`Self::resolve_outer_radius`].
186    fn get_outer_radius(&self, arc: &ArcData<T>, default: f32) -> f32 {
187        if let Some(outer_radius_fn) = self.outer_radius_fn.as_ref() {
188            outer_radius_fn(arc)
189        } else {
190            default
191        }
192    }
193
194    /// Set the pad angle of the pie chart.
195    pub fn pad_angle(mut self, pad_angle: f32) -> Self {
196        self.pad_angle = pad_angle;
197        self
198    }
199
200    pub fn value(mut self, value: impl Fn(&T) -> f32 + 'static) -> Self {
201        self.value = Some(Rc::new(value));
202        self
203    }
204
205    /// Set the color of the pie chart.
206    pub fn color<H>(mut self, color: impl Fn(&T) -> H + 'static) -> Self
207    where
208        H: Into<Hsla> + 'static,
209    {
210        self.color = Some(Rc::new(move |t| color(t).into()));
211        self
212    }
213
214    /// Set the label text for each slice.
215    ///
216    /// Once set, a "leader line + text" is drawn outside the ring for every
217    /// slice.
218    pub fn label(mut self, label: impl Fn(&T) -> SharedString + 'static) -> Self {
219        self.label = Some(Rc::new(label));
220        self
221    }
222
223    /// Set the leader line color per slice (defaults to `cx.theme().border`).
224    pub fn label_line_color(mut self, color: impl Fn(&T) -> Hsla + 'static) -> Self {
225        self.label_line_color = Some(Rc::new(color));
226        self
227    }
228
229    /// Set the label text color (defaults to `cx.theme().foreground`).
230    pub fn label_color(mut self, color: Hsla) -> Self {
231        self.label_color = Some(color);
232        self
233    }
234
235    /// Set the extra gap between `outer_radius` and the label radius
236    /// (defaults to 15px).
237    pub fn label_gap(mut self, gap: f32) -> Self {
238        self.label_gap = gap;
239        self
240    }
241
242    /// Name the slice under the cursor in the hover tooltip's row, beside its
243    /// value. Falls back to `name`, the one name the whole series carries.
244    ///
245    /// A pie shows one number per slice, so the slice's own name is what the
246    /// row wants; a single series name leaves the row reading as a swatch and a
247    /// number with a gap between them. The alternative was to title the tooltip
248    /// from `label`, but that also draws the leader lines around the ring.
249    pub fn tooltip_name(mut self, name: impl Fn(&T) -> SharedString + 'static) -> Self {
250        self.tooltip_name = Some(Rc::new(name));
251        self
252    }
253
254    /// Set the text of the hover tooltip's row, the value the slice is worth.
255    /// Defaults to the raw value followed by its share in parentheses.
256    ///
257    /// The closure receives the datum, the value `value` returned for it, and
258    /// that value's share of the total as a percentage. Set it wherever the raw
259    /// number is not what a reader should see: a value that is already a ratio
260    /// reads as `0.35 (35.0%)` by default, and a chart drawn from adjusted
261    /// values — a floor that keeps a hairline slice visible, say — would report
262    /// the adjustment as though it were the datum.
263    pub fn tooltip_value(mut self, value: impl Fn(&T, f32, f32) -> SharedString + 'static) -> Self {
264        self.tooltip_value = Some(Rc::new(value));
265        self
266    }
267
268    /// The outer radius the ring is laid out with: the set one, or 40% of the
269    /// bounds height.
270    fn resolve_outer_radius(&self, bounds: &Bounds<Pixels>) -> f32 {
271        if self.outer_radius.is_zero() {
272            bounds.size.height.as_f32() * 0.4
273        } else {
274            self.outer_radius
275        }
276    }
277
278    /// The slices, in ring order. Shared by `paint` and `tooltip_state` so the
279    /// two stay in sync; empty without a value accessor.
280    fn arcs(&self) -> Vec<ArcData<'_, T>> {
281        let Some(value_fn) = self.value.clone() else {
282            return vec![];
283        };
284        Pie::<T>::new()
285            .value(move |d| Some(value_fn(d)))
286            .pad_angle(self.pad_angle)
287            .arcs(&self.data)
288    }
289
290    /// The fill of a slice: the per-datum color, or the theme's.
291    fn slice_color(&self, datum: &T, cx: &App) -> Hsla {
292        match self.color.as_ref() {
293            Some(color_fn) => color_fn(datum),
294            None => cx.theme().chart_2,
295        }
296    }
297
298    /// How far the slice of datum `index` has lifted and how much it has faded
299    /// behind the hovered one this frame, as `(lift, opacity)`.
300    fn slice_emphasis(&self, index: usize) -> (f32, f32) {
301        let Some(hover) = self.hover.as_ref() else {
302            return (0., 1.);
303        };
304        let lift = hover.lift.get(index).copied().unwrap_or(0.) * hover.focus;
305        (lift, 1. - HOVER_DIM * hover.focus * (1. - lift))
306    }
307}
308
309impl<T> Plot for PieChart<T> {
310    fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
311        if self.value.is_none() {
312            return;
313        }
314
315        let outer_radius = self.resolve_outer_radius(&bounds);
316        let arcs = self.arcs();
317
318        // The ring sweeps clockwise from its first slice as the chart appears.
319        // Every frame of the sweep is a new shape, so the slices tessellate
320        // afresh until it ends rather than churn the cache.
321        let appear = self.appear.get().progress();
322        let swept;
323        let slices = if appear < 1. {
324            let mut arcs = self.arcs();
325            let start = arcs.first().map_or(0., |a| a.start_angle);
326            for a in &mut arcs {
327                a.start_angle = start + (a.start_angle - start) * appear;
328                a.end_angle = start + (a.end_angle - start) * appear;
329            }
330            swept = arcs;
331            &swept
332        } else {
333            &arcs
334        };
335        let caches = (appear >= 1.).then(|| PathCaches::for_paint("slices", window, cx));
336        for (ix, a) in slices.iter().enumerate() {
337            let inner_radius = self.get_inner_radius(a);
338            // The hovered slice lifts out of the ring while the others fade behind it.
339            let (lift, opacity) = self.slice_emphasis(a.index);
340            let slice_radius = self.get_outer_radius(a, outer_radius) + HOVER_LIFT * lift;
341            let color = self.slice_color(a.data, cx).opacity(opacity);
342            let arc = Arc::new()
343                .inner_radius(inner_radius)
344                .outer_radius(slice_radius);
345            match caches.as_ref() {
346                Some(caches) => caches.update(cx, |caches, _| {
347                    arc.paint_cached(a, color, &bounds, caches.slot(ix), window);
348                }),
349                None => arc.paint(a, color, &bounds, window),
350            }
351        }
352
353        // Draw leader-line labels outside the ring (only when `label` is set).
354        let Some(label_fn) = self.label.as_ref() else {
355            return;
356        };
357
358        let label_radius = outer_radius + self.label_gap;
359        let center_x = bounds.size.width.as_f32() / 2.;
360        let center_y = bounds.size.height.as_f32() / 2.;
361        let label_arc = Arc::new()
362            .inner_radius(label_radius)
363            .outer_radius(label_radius);
364
365        // Labels fade in over the end of the sweep, once their slices are
366        // mostly drawn.
367        let label_opacity = ((appear - LABEL_APPEAR_START) / (1. - LABEL_APPEAR_START)).max(0.);
368        if label_opacity <= 0. {
369            return;
370        }
371        let label_color = self
372            .label_color
373            .unwrap_or(cx.theme().foreground)
374            .opacity(label_opacity);
375        let default_line_color = cx.theme().border;
376
377        // First pass: collect a layout candidate per visible slice, split by
378        // side. `y` is the target vertical position relative to the center and
379        // gets adjusted later to remove overlaps.
380        let mut right: Vec<LabelLayout> = vec![];
381        let mut left: Vec<LabelLayout> = vec![];
382        for a in &arcs {
383            // Skip tiny slices (< 0.5°) that are too thin to label.
384            if a.end_angle - a.start_angle < std::f32::consts::PI / 360. {
385                continue;
386            }
387
388            let centroid = label_arc.centroid(a);
389            // Anchor the line on the edge the slice reaches this frame, so a
390            // lifted slice never paints over its own leader line. The label
391            // anchor stays put, so the line may not start past it.
392            let (lift, _) = self.slice_emphasis(a.index);
393            let edge_radius = (outer_radius + HOVER_LIFT * lift).min(label_radius);
394            let edge = Arc::new()
395                .inner_radius(edge_radius)
396                .outer_radius(edge_radius)
397                .centroid(a);
398            let is_right = centroid.x > 0.;
399            let line_color = self
400                .label_line_color
401                .as_ref()
402                .map(|f| f(a.data))
403                .unwrap_or(default_line_color)
404                .opacity(label_opacity);
405
406            let layout = LabelLayout {
407                arc_x: edge.x,
408                arc_y: edge.y,
409                label_x: centroid.x,
410                y: centroid.y,
411                text: label_fn(a.data),
412                line_color,
413            };
414            if is_right { &mut right } else { &mut left }.push(layout);
415        }
416
417        // Second pass: spread labels on each side so neighbors keep at least one
418        // text height apart, clamped within the vertical bounds.
419        let top = -center_y + TEXT_HEIGHT / 2.;
420        let bottom = center_y - TEXT_HEIGHT / 2.;
421        spread_labels(&mut right, top, bottom);
422        spread_labels(&mut left, top, bottom);
423
424        // Third pass: paint leader lines first, then the text on top.
425        let mut labels = vec![];
426        for (side, items) in [(1., &right), (-1., &left)] {
427            for item in items {
428                // Leader line: ring edge -> label anchor -> horizontal pull to
429                // ±label_radius.
430                let pts = [
431                    point(item.arc_x + center_x, item.arc_y + center_y),
432                    point(item.label_x + center_x, item.y + center_y),
433                    point(side * label_radius + center_x, item.y + center_y),
434                ];
435                if let Some(p) = polygon(&pts, &bounds) {
436                    window.paint_path(p, item.line_color);
437                }
438
439                // Text sits 4px further out, aligned by side.
440                let origin = point(
441                    side * (label_radius + 4.) + center_x,
442                    item.y - TEXT_SIZE / 2. + center_y,
443                );
444                let align = if side > 0. {
445                    TextAlign::Left
446                } else {
447                    TextAlign::Right
448                };
449                labels.push(Text::new(item.text.clone(), origin, label_color).align(align));
450            }
451        }
452
453        PlotLabel::new(labels).paint(&bounds, window, cx);
454    }
455
456    fn id(&self) -> Option<ElementId> {
457        Some(self.id.clone())
458    }
459
460    fn interactive(&self) -> bool {
461        self.interactive
462    }
463
464    fn appear(&mut self, appear: PlotAppear, _window: &mut Window, _cx: &mut App) {
465        self.appear.update(appear);
466    }
467
468    fn appear_generation(&self) -> Option<u64> {
469        self.appear.generation()
470    }
471
472    fn tooltip_state(
473        &self,
474        position: Point<Pixels>,
475        bounds: Bounds<Pixels>,
476        _cx: &App,
477    ) -> Option<TooltipState> {
478        let outer_radius = self.resolve_outer_radius(&bounds);
479        let position = point(position.x.as_f32(), position.y.as_f32());
480
481        let index = self.arcs().into_iter().find_map(|a| {
482            Arc::new()
483                .inner_radius(self.get_inner_radius(&a))
484                .outer_radius(self.get_outer_radius(&a, outer_radius))
485                .contains(&a, position, &bounds)
486                .then_some(a.index)
487        })?;
488
489        Some(TooltipState::new(
490            index,
491            point(px(position.x), px(position.y)),
492            vec![],
493        ))
494    }
495
496    fn hover(&mut self, hover: Option<&PlotHover>, window: &mut Window, cx: &mut App) {
497        self.hover = hover.map(|hover| {
498            // Every slice springs toward lifted or resting, so the one the cursor
499            // left settles back while the new one rises. On the first hovered
500            // frame the target is rest, so the slice rises from the ring rather
501            // than adopting the lifted position outright.
502            let policy = cx.theme().motion_tokens().spring_control;
503            let lift = (0..self.data.len())
504                .map(|ix| {
505                    let lifted =
506                        hover.is_hovered() && !hover.is_entering() && ix == hover.state().index;
507                    spring(
508                        ElementId::named_usize("pie-slice", ix),
509                        if lifted { 1. } else { 0. },
510                        policy,
511                        window,
512                        cx,
513                    )
514                })
515                .collect();
516            PieHover {
517                lift,
518                focus: hover.progress(),
519            }
520        });
521    }
522
523    fn tooltip(
524        &self,
525        state: &TooltipState,
526        cursor: Point<Pixels>,
527        bounds: Bounds<Pixels>,
528        _window: &mut Window,
529        cx: &mut App,
530    ) -> Option<AnyElement> {
531        let value_fn = self.value.as_ref()?;
532        let d = self.data.get(state.index)?;
533        let value = value_fn(d);
534        let total: f32 = self.data.iter().map(|d| value_fn(d).max(0.)).sum();
535        let share = if total > 0. { value / total * 100. } else { 0. };
536        let name = match self.tooltip_name.as_ref() {
537            Some(tooltip_name) => tooltip_name(d),
538            None => self.name.clone().unwrap_or_default(),
539        };
540
541        Some(
542            // Follow the cursor; the lifted slice marks the datum. One number
543            // per slice fits one row, so there is no title: `label` used to
544            // supply one, but it is the ring's leader-line text, which is as
545            // often a percentage as a name.
546            Tooltip::new(cursor, bounds.size)
547                .gap(px(8.))
548                .row(
549                    self.slice_color(d, cx),
550                    name,
551                    match self.tooltip_value.as_ref() {
552                        Some(tooltip_value) => tooltip_value(d, value, share),
553                        None => format!("{value} ({share:.1}%)").into(),
554                    },
555                )
556                .into_any_element(),
557        )
558    }
559}
560
561/// A resolved label position before overlap adjustment.
562struct LabelLayout {
563    /// Anchor on the ring edge (relative to center).
564    arc_x: f32,
565    arc_y: f32,
566    /// Centroid x at the label radius (relative to center).
567    label_x: f32,
568    /// Target/adjusted vertical position (relative to center).
569    y: f32,
570    text: SharedString,
571    line_color: Hsla,
572}
573
574/// Spread `items` vertically so that adjacent labels keep at least
575/// [`TEXT_HEIGHT`] apart, clamped within `[top, bottom]`.
576///
577/// Uses a two-direction relaxation: a top-down pass pushes crowded labels down,
578/// then a bottom-up pass (anchored at `bottom`) pushes them back up. This
579/// resolves cascading overlaps that a single-neighbor nudge cannot.
580fn spread_labels(items: &mut [LabelLayout], top: f32, bottom: f32) {
581    let n = items.len();
582    if n == 0 {
583        return;
584    }
585
586    // Sort by target position so neighbors in the slice are neighbors in y.
587    items.sort_by(|a, b| a.y.total_cmp(&b.y));
588
589    // Top-down: enforce the minimum gap by pushing labels down.
590    for i in 1..n {
591        let min_y = items[i - 1].y + TEXT_HEIGHT;
592        if items[i].y < min_y {
593            items[i].y = min_y;
594        }
595    }
596
597    // Bottom-up: clamp the bottom-most label, then pull overflowing labels up.
598    if items[n - 1].y > bottom {
599        items[n - 1].y = bottom;
600    }
601    for i in (0..n - 1).rev() {
602        let max_y = items[i + 1].y - TEXT_HEIGHT;
603        if items[i].y > max_y {
604            items[i].y = max_y;
605        }
606    }
607
608    // Keep the top-most label within bounds.
609    if items[0].y < top {
610        items[0].y = top;
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use gpui::size;
617
618    use super::*;
619
620    /// A chart left without an `outer_radius` lays its ring out at 40% of the
621    /// height. Slices and hit-testing have to use that radius: reading the
622    /// unset `outer_radius` field instead leaves every slice at zero, which
623    /// paints nothing and matches no cursor.
624    #[test]
625    fn test_pie_chart_slice_radius_falls_back_to_the_ring() {
626        let bounds = Bounds {
627            origin: point(px(0.), px(0.)),
628            size: size(px(200.), px(200.)),
629        };
630
631        let chart = PieChart::new(vec![1f32, 3.]).value(|d| *d);
632        let ring = chart.resolve_outer_radius(&bounds);
633        assert_eq!(ring, 80.);
634        assert_eq!(chart.get_outer_radius(&chart.arcs()[0], ring), ring);
635
636        // An explicit radius, and a per-slice one, still win.
637        let chart = PieChart::new(vec![1f32, 3.])
638            .value(|d| *d)
639            .outer_radius(50.);
640        let ring = chart.resolve_outer_radius(&bounds);
641        assert_eq!(ring, 50.);
642        assert_eq!(chart.get_outer_radius(&chart.arcs()[0], ring), 50.);
643
644        let chart = PieChart::new(vec![1f32, 3.])
645            .value(|d| *d)
646            .outer_radius_fn(|a| 10. + a.index as f32);
647        let ring = chart.resolve_outer_radius(&bounds);
648        let arcs = chart.arcs();
649        assert_eq!(chart.get_outer_radius(&arcs[0], ring), 10.);
650        assert_eq!(chart.get_outer_radius(&arcs[1], ring), 11.);
651    }
652
653    /// The row's name is the slice's own, and reaching it must not put labels
654    /// on the ring: `label` is the only other per-slice text a pie has, and it
655    /// draws the leader lines.
656    #[test]
657    fn test_tooltip_name_does_not_turn_on_leader_lines() {
658        let titled = PieChart::new(vec![1f32]).tooltip_name(|_| "Tech".into());
659        assert!(titled.tooltip_name.is_some());
660        assert!(titled.label.is_none());
661
662        // `label` still titles the tooltip when no title is set.
663        let labelled = PieChart::new(vec![1f32]).label(|_| "Tech".into());
664        assert!(labelled.tooltip_name.is_none());
665        assert!(labelled.label.is_some());
666    }
667}