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
33pub 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
55pub 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 let cuts: Vec<usize> = text.char_indices().skip(1).map(|(i, _)| i).collect();
74
75 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 pub fn font_size(mut self, font_size: impl Into<Pixels>) -> Self {
123 self.font_size = font_size.into();
124 self
125 }
126
127 pub fn font_weight(mut self, font_weight: FontWeight) -> Self {
129 self.font_weight = font_weight;
130 self
131 }
132
133 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 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}