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.;
17pub const TEXT_GAP: f32 = 2.;
19pub const TEXT_HEIGHT: f32 = TEXT_SIZE + TEXT_GAP;
22
23fn shape_label(
24 text: &SharedString,
25 font_size: Pixels,
26 color: Hsla,
27 window: &mut Window,
28) -> ShapedLine {
29 let text_run = TextRun {
30 len: text.len(),
31 font: window.text_style().font(),
32 color,
33 background_color: None,
34 underline: None,
35 strikethrough: None,
36 };
37 window
38 .text_system()
39 .shape_line(text.clone(), font_size, &[text_run], None)
40}
41
42pub fn measure_text_width(text: &SharedString, font_size: Pixels, window: &mut Window) -> f32 {
46 if text.is_empty() {
47 return 0.;
48 }
49 shape_label(
50 text,
51 font_size,
52 Hsla {
53 h: 0.,
54 s: 0.,
55 l: 0.,
56 a: 1.,
57 },
58 window,
59 )
60 .width()
61 .as_f32()
62}
63
64pub fn truncate_text_to_width(
70 text: &SharedString,
71 font_size: Pixels,
72 max_width: f32,
73 window: &mut Window,
74) -> SharedString {
75 if max_width <= 0. || measure_text_width(text, font_size, window) <= max_width {
76 return text.clone();
77 }
78
79 const ELLIPSIS: &str = "…";
80 let cuts: Vec<usize> = text.char_indices().skip(1).map(|(i, _)| i).collect();
83
84 let mut best: Option<usize> = None;
86 let (mut lo, mut hi) = (0usize, cuts.len());
87 while lo < hi {
88 let mid = (lo + hi) / 2;
89 let candidate = SharedString::from(format!("{}{ELLIPSIS}", &text[..cuts[mid]]));
90 if measure_text_width(&candidate, font_size, window) <= max_width {
91 best = Some(mid);
92 lo = mid + 1;
93 } else {
94 hi = mid;
95 }
96 }
97
98 match best {
99 Some(mid) => SharedString::from(format!("{}{ELLIPSIS}", &text[..cuts[mid]])),
100 None => SharedString::from(ELLIPSIS),
101 }
102}
103
104#[non_exhaustive]
107pub struct Text {
108 pub text: SharedString,
109 pub origin: Point<Pixels>,
110 pub color: Hsla,
111 pub font_size: Pixels,
112 pub font_weight: FontWeight,
113 pub align: TextAlign,
114}
115
116impl Text {
117 pub fn new<T>(text: impl Into<SharedString>, origin: Point<T>, color: Hsla) -> Self
118 where
119 T: Default + Clone + Copy + Debug + PartialEq + Into<Pixels>,
120 {
121 let origin = point(origin.x.into(), origin.y.into());
122
123 Self {
124 text: text.into(),
125 origin,
126 color,
127 font_size: TEXT_SIZE.into(),
128 font_weight: FontWeight::NORMAL,
129 align: TextAlign::Left,
130 }
131 }
132
133 pub fn font_size(mut self, font_size: impl Into<Pixels>) -> Self {
135 self.font_size = font_size.into();
136 self
137 }
138
139 pub fn font_weight(mut self, font_weight: FontWeight) -> Self {
141 self.font_weight = font_weight;
142 self
143 }
144
145 pub fn align(mut self, align: TextAlign) -> Self {
147 self.align = align;
148 self
149 }
150}
151
152impl<I> From<I> for PlotLabel
153where
154 I: Iterator<Item = Text>,
155{
156 fn from(items: I) -> Self {
157 Self::new(items.collect())
158 }
159}
160
161#[derive(Default)]
162pub struct PlotLabel(Vec<Text>);
163
164impl PlotLabel {
165 pub fn new(items: Vec<Text>) -> Self {
166 Self(items)
167 }
168
169 pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window, cx: &mut App) {
171 for Text {
172 text,
173 origin,
174 color,
175 font_size,
176 font_weight: _,
177 align,
178 } in self.0.iter()
179 {
180 let origin = origin_point(origin.x, origin.y, bounds.origin);
181
182 let line = shape_label(text, *font_size, *color, window);
183 let origin = match align {
184 TextAlign::Left => origin,
185 TextAlign::Right => origin - point(line.width(), px(0.)),
186 _ => origin - point(line.width() / 2., px(0.)),
187 };
188 let _ = line.paint(origin, *font_size, *align, None, window, cx);
189 }
190 }
191}