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