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.line_height(self.style.text.font_size),
150 );
151
152 (size, line)
153 }
154
155 fn paint(
156 &mut self,
157 bounds: RectF,
158 visible_bounds: RectF,
159 line: &mut Self::LayoutState,
160 cx: &mut PaintContext,
161 ) -> Self::PaintState {
162 line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
163 }
164
165 fn dispatch_event(
166 &mut self,
167 _: &Event,
168 _: RectF,
169 _: &mut Self::LayoutState,
170 _: &mut Self::PaintState,
171 _: &mut EventContext,
172 ) -> bool {
173 false
174 }
175
176 fn debug(
177 &self,
178 bounds: RectF,
179 _: &Self::LayoutState,
180 _: &Self::PaintState,
181 _: &DebugContext,
182 ) -> Value {
183 json!({
184 "type": "Label",
185 "bounds": bounds.to_json(),
186 "text": &self.text,
187 "highlight_indices": self.highlight_indices,
188 "style": self.style.to_json(),
189 })
190 }
191}
192
193impl ToJson for LabelStyle {
194 fn to_json(&self) -> Value {
195 json!({
196 "text": self.text.to_json(),
197 "highlight_text": self.highlight_text
198 .as_ref()
199 .map_or(serde_json::Value::Null, |style| style.to_json())
200 })
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::color::Color;
208 use crate::fonts::{Properties as FontProperties, Weight};
209
210 #[crate::test(self)]
211 fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) {
212 let default_style = TextStyle::new(
213 "Menlo",
214 12.,
215 Default::default(),
216 Default::default(),
217 Color::black(),
218 cx.font_cache(),
219 )
220 .unwrap();
221 let highlight_style = TextStyle::new(
222 "Menlo",
223 12.,
224 *FontProperties::new().weight(Weight::BOLD),
225 Default::default(),
226 Color::new(255, 0, 0, 255),
227 cx.font_cache(),
228 )
229 .unwrap();
230 let label = Label::new(
231 ".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
232 LabelStyle {
233 text: default_style.clone(),
234 highlight_text: Some(highlight_style.clone()),
235 },
236 )
237 .with_highlights(vec![
238 ".α".len(),
239 ".αβ".len(),
240 ".αβγδ".len(),
241 ".αβγδε.ⓐ".len(),
242 ".αβγδε.ⓐⓑ".len(),
243 ]);
244
245 let default_run_style = RunStyle {
246 font_id: default_style.font_id,
247 color: default_style.color,
248 underline: default_style.underline,
249 };
250 let highlight_run_style = RunStyle {
251 font_id: highlight_style.font_id,
252 color: highlight_style.color,
253 underline: highlight_style.underline,
254 };
255 let runs = label.compute_runs();
256 assert_eq!(
257 runs.as_slice(),
258 &[
259 (".α".len(), default_run_style),
260 ("βγ".len(), highlight_run_style),
261 ("δ".len(), default_run_style),
262 ("ε".len(), highlight_run_style),
263 (".ⓐ".len(), default_run_style),
264 ("ⓑⓒ".len(), highlight_run_style),
265 ("ⓓⓔ.abcde.".len(), default_run_style),
266 ]
267 );
268 }
269}