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