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