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