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