Skip to main content

gpui_component/plot/
label.rs

1use std::fmt::Debug;
2
3use gpui::{
4    App, Bounds, FontWeight, Hsla, Pixels, Point, ShapedLine, SharedString, TextAlign, TextRun,
5    Window, point, px,
6};
7
8use super::origin_point;
9
10pub const TEXT_SIZE: f32 = 10.;
11pub const TEXT_GAP: f32 = 2.;
12pub const TEXT_HEIGHT: f32 = TEXT_SIZE + TEXT_GAP;
13
14fn shape_label(
15    text: &SharedString,
16    font_size: Pixels,
17    color: Hsla,
18    window: &mut Window,
19) -> ShapedLine {
20    let text_run = TextRun {
21        len: text.len(),
22        font: window.text_style().font(),
23        color,
24        background_color: None,
25        underline: None,
26        strikethrough: None,
27    };
28    window
29        .text_system()
30        .shape_line(text.clone(), font_size, &[text_run], None)
31}
32
33/// Returns the rendered width of `text` at `font_size` using the window's
34/// current text style.  Used for layout calculations that need to reserve
35/// space for labels before the scale ranges are fixed.
36pub fn measure_text_width(text: &SharedString, font_size: Pixels, window: &mut Window) -> f32 {
37    if text.is_empty() {
38        return 0.;
39    }
40    shape_label(
41        text,
42        font_size,
43        Hsla {
44            h: 0.,
45            s: 0.,
46            l: 0.,
47            a: 1.,
48        },
49        window,
50    )
51    .width()
52    .as_f32()
53}
54
55/// Truncate `text` with a trailing ellipsis so it fits within `max_width` at
56/// `font_size`. Returns `text` unchanged when it already fits; returns just
57/// the ellipsis when not even one character fits. `max_width <= 0` is treated
58/// as "no budget" and returns `text` unchanged (the caller has no room to
59/// reason about).
60pub fn truncate_text_to_width(
61    text: &SharedString,
62    font_size: Pixels,
63    max_width: f32,
64    window: &mut Window,
65) -> SharedString {
66    if max_width <= 0. || measure_text_width(text, font_size, window) <= max_width {
67        return text.clone();
68    }
69
70    const ELLIPSIS: &str = "…";
71    // Byte offsets of every char boundary after the first char; cutting at
72    // `cuts[k]` keeps the first `k + 1` chars.
73    let cuts: Vec<usize> = text.char_indices().skip(1).map(|(i, _)| i).collect();
74
75    // Binary search the largest prefix whose text + ellipsis still fits.
76    let mut best: Option<usize> = None;
77    let (mut lo, mut hi) = (0usize, cuts.len());
78    while lo < hi {
79        let mid = (lo + hi) / 2;
80        let candidate = SharedString::from(format!("{}{ELLIPSIS}", &text[..cuts[mid]]));
81        if measure_text_width(&candidate, font_size, window) <= max_width {
82            best = Some(mid);
83            lo = mid + 1;
84        } else {
85            hi = mid;
86        }
87    }
88
89    match best {
90        Some(mid) => SharedString::from(format!("{}{ELLIPSIS}", &text[..cuts[mid]])),
91        None => SharedString::from(ELLIPSIS),
92    }
93}
94
95pub struct Text {
96    pub text: SharedString,
97    pub origin: Point<Pixels>,
98    pub color: Hsla,
99    pub font_size: Pixels,
100    pub font_weight: FontWeight,
101    pub align: TextAlign,
102}
103
104impl Text {
105    pub fn new<T>(text: impl Into<SharedString>, origin: Point<T>, color: Hsla) -> Self
106    where
107        T: Default + Clone + Copy + Debug + PartialEq + Into<Pixels>,
108    {
109        let origin = point(origin.x.into(), origin.y.into());
110
111        Self {
112            text: text.into(),
113            origin,
114            color,
115            font_size: TEXT_SIZE.into(),
116            font_weight: FontWeight::NORMAL,
117            align: TextAlign::Left,
118        }
119    }
120
121    /// Set the font size of the Text.
122    pub fn font_size(mut self, font_size: impl Into<Pixels>) -> Self {
123        self.font_size = font_size.into();
124        self
125    }
126
127    /// Set the font weight of the Text.
128    pub fn font_weight(mut self, font_weight: FontWeight) -> Self {
129        self.font_weight = font_weight;
130        self
131    }
132
133    /// Set the alignment of the Text.
134    pub fn align(mut self, align: TextAlign) -> Self {
135        self.align = align;
136        self
137    }
138}
139
140impl<I> From<I> for PlotLabel
141where
142    I: Iterator<Item = Text>,
143{
144    fn from(items: I) -> Self {
145        Self::new(items.collect())
146    }
147}
148
149#[derive(Default)]
150pub struct PlotLabel(Vec<Text>);
151
152impl PlotLabel {
153    pub fn new(items: Vec<Text>) -> Self {
154        Self(items)
155    }
156
157    /// Paint the Label.
158    pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window, cx: &mut App) {
159        for Text {
160            text,
161            origin,
162            color,
163            font_size,
164            font_weight: _,
165            align,
166        } in self.0.iter()
167        {
168            let origin = origin_point(origin.x, origin.y, bounds.origin);
169
170            let line = shape_label(text, *font_size, *color, window);
171            let origin = match align {
172                TextAlign::Left => origin,
173                TextAlign::Right => origin - point(line.width(), px(0.)),
174                _ => origin - point(line.width() / 2., px(0.)),
175            };
176            let _ = line.paint(origin, *font_size, *align, None, window, cx);
177        }
178    }
179}