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