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