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