1use std::{borrow::Cow, ops::Range};
2
3use crate::{
4 fonts::TextStyle,
5 geometry::{
6 rect::RectF,
7 vector::{vec2f, Vector2F},
8 },
9 json::{ToJson, Value},
10 text_layout::{Line, RunStyle},
11 Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
12};
13use serde::Deserialize;
14use serde_json::json;
15use smallvec::{smallvec, SmallVec};
16
17pub struct Label {
18 text: Cow<'static, str>,
19 style: LabelStyle,
20 highlight_indices: Vec<usize>,
21}
22
23#[derive(Clone, Debug, Deserialize, Default)]
24pub struct LabelStyle {
25 pub text: TextStyle,
26 pub highlight_text: Option<TextStyle>,
27}
28
29impl From<TextStyle> for LabelStyle {
30 fn from(text: TextStyle) -> Self {
31 LabelStyle {
32 text,
33 highlight_text: None,
34 }
35 }
36}
37
38impl LabelStyle {
39 pub fn with_font_size(mut self, font_size: f32) -> Self {
40 self.text.font_size = font_size;
41 self
42 }
43}
44
45impl Label {
46 pub fn new<I: Into<Cow<'static, str>>>(text: I, style: impl Into<LabelStyle>) -> Self {
47 Self {
48 text: text.into(),
49 highlight_indices: Default::default(),
50 style: style.into(),
51 }
52 }
53
54 pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
55 self.highlight_indices = indices;
56 self
57 }
58
59 fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> {
60 let font_id = self.style.text.font_id;
61 if self.highlight_indices.is_empty() {
62 return smallvec![(
63 self.text.len(),
64 RunStyle {
65 font_id,
66 color: self.style.text.color,
67 underline: self.style.text.underline,
68 }
69 )];
70 }
71
72 let highlight_font_id = self
73 .style
74 .highlight_text
75 .as_ref()
76 .map_or(font_id, |style| style.font_id);
77
78 let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
79 let mut runs = SmallVec::new();
80 let highlight_style = self
81 .style
82 .highlight_text
83 .as_ref()
84 .unwrap_or(&self.style.text);
85
86 for (char_ix, c) in self.text.char_indices() {
87 let mut font_id = font_id;
88 let mut color = self.style.text.color;
89 let mut underline = self.style.text.underline;
90 if let Some(highlight_ix) = highlight_indices.peek() {
91 if char_ix == *highlight_ix {
92 font_id = highlight_font_id;
93 color = highlight_style.color;
94 underline = highlight_style.underline;
95 highlight_indices.next();
96 }
97 }
98
99 let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut();
100 let push_new_run = if let Some((last_len, last_style)) = last_run {
101 if font_id == last_style.font_id
102 && color == last_style.color
103 && underline == last_style.underline
104 {
105 *last_len += c.len_utf8();
106 false
107 } else {
108 true
109 }
110 } else {
111 true
112 };
113
114 if push_new_run {
115 runs.push((
116 c.len_utf8(),
117 RunStyle {
118 font_id,
119 color,
120 underline,
121 },
122 ));
123 }
124 }
125
126 runs
127 }
128}
129
130impl<V: View> Element<V> for Label {
131 type LayoutState = Line;
132 type PaintState = ();
133
134 fn layout(
135 &mut self,
136 constraint: SizeConstraint,
137 _: &mut V,
138 cx: &mut LayoutContext<V>,
139 ) -> (Vector2F, Self::LayoutState) {
140 let runs = self.compute_runs();
141 let line = cx.text_layout_cache().layout_str(
142 &self.text,
143 self.style.text.font_size,
144 runs.as_slice(),
145 );
146
147 let size = vec2f(
148 line.width()
149 .ceil()
150 .max(constraint.min.x())
151 .min(constraint.max.x()),
152 cx.font_cache.line_height(self.style.text.font_size),
153 );
154
155 (size, line)
156 }
157
158 fn paint(
159 &mut self,
160 scene: &mut SceneBuilder,
161 bounds: RectF,
162 visible_bounds: RectF,
163 line: &mut Self::LayoutState,
164 _: &mut V,
165 cx: &mut ViewContext<V>,
166 ) -> Self::PaintState {
167 line.paint(
168 scene,
169 bounds.origin(),
170 visible_bounds,
171 bounds.size().y(),
172 cx,
173 )
174 }
175
176 fn rect_for_text_range(
177 &self,
178 _: Range<usize>,
179 _: RectF,
180 _: RectF,
181 _: &Self::LayoutState,
182 _: &Self::PaintState,
183 _: &V,
184 _: &ViewContext<V>,
185 ) -> Option<RectF> {
186 None
187 }
188
189 fn debug(
190 &self,
191 bounds: RectF,
192 _: &Self::LayoutState,
193 _: &Self::PaintState,
194 _: &V,
195 _: &ViewContext<V>,
196 ) -> Value {
197 json!({
198 "type": "Label",
199 "bounds": bounds.to_json(),
200 "text": &self.text,
201 "highlight_indices": self.highlight_indices,
202 "style": self.style.to_json(),
203 })
204 }
205}
206
207impl ToJson for LabelStyle {
208 fn to_json(&self) -> Value {
209 json!({
210 "text": self.text.to_json(),
211 "highlight_text": self.highlight_text
212 .as_ref()
213 .map_or(serde_json::Value::Null, |style| style.to_json())
214 })
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::color::Color;
222 use crate::fonts::{Properties as FontProperties, Weight};
223
224 #[crate::test(self)]
225 fn test_layout_label_with_highlights(cx: &mut crate::AppContext) {
226 let default_style = TextStyle::new(
227 "Menlo",
228 12.,
229 Default::default(),
230 Default::default(),
231 Default::default(),
232 Color::black(),
233 cx.font_cache(),
234 )
235 .unwrap();
236 let highlight_style = TextStyle::new(
237 "Menlo",
238 12.,
239 *FontProperties::new().weight(Weight::BOLD),
240 Default::default(),
241 Default::default(),
242 Color::new(255, 0, 0, 255),
243 cx.font_cache(),
244 )
245 .unwrap();
246 let label = Label::new(
247 ".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
248 LabelStyle {
249 text: default_style.clone(),
250 highlight_text: Some(highlight_style.clone()),
251 },
252 )
253 .with_highlights(vec![
254 ".α".len(),
255 ".αβ".len(),
256 ".αβγδ".len(),
257 ".αβγδε.ⓐ".len(),
258 ".αβγδε.ⓐⓑ".len(),
259 ]);
260
261 let default_run_style = RunStyle {
262 font_id: default_style.font_id,
263 color: default_style.color,
264 underline: default_style.underline,
265 };
266 let highlight_run_style = RunStyle {
267 font_id: highlight_style.font_id,
268 color: highlight_style.color,
269 underline: highlight_style.underline,
270 };
271 let runs = label.compute_runs();
272 assert_eq!(
273 runs.as_slice(),
274 &[
275 (".α".len(), default_run_style),
276 ("βγ".len(), highlight_run_style),
277 ("δ".len(), default_run_style),
278 ("ε".len(), highlight_run_style),
279 (".ⓐ".len(), default_run_style),
280 ("ⓑⓒ".len(), highlight_run_style),
281 ("ⓓⓔ.abcde.".len(), default_run_style),
282 ]
283 );
284 }
285}