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