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