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 visible_bounds: RectF,
132 line: &mut Self::LayoutState,
133 cx: &mut PaintContext,
134 ) -> Self::PaintState {
135 line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
136 }
137
138 fn dispatch_event(
139 &mut self,
140 _: &Event,
141 _: RectF,
142 _: &mut Self::LayoutState,
143 _: &mut Self::PaintState,
144 _: &mut EventContext,
145 ) -> bool {
146 false
147 }
148
149 fn debug(
150 &self,
151 bounds: RectF,
152 _: &Self::LayoutState,
153 _: &Self::PaintState,
154 _: &DebugContext,
155 ) -> Value {
156 json!({
157 "type": "Label",
158 "bounds": bounds.to_json(),
159 "text": &self.text,
160 "highlight_indices": self.highlight_indices,
161 "style": self.style.to_json(),
162 })
163 }
164}
165
166impl ToJson for LabelStyle {
167 fn to_json(&self) -> Value {
168 json!({
169 "text": self.text.to_json(),
170 "highlight_text": self.highlight_text
171 .as_ref()
172 .map_or(serde_json::Value::Null, |style| style.to_json())
173 })
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::fonts::{Properties as FontProperties, Weight};
181
182 #[crate::test(self)]
183 fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) {
184 let default_style = TextStyle::new(
185 "Menlo",
186 12.,
187 Default::default(),
188 Color::black(),
189 cx.font_cache(),
190 )
191 .unwrap();
192 let highlight_style = TextStyle::new(
193 "Menlo",
194 12.,
195 *FontProperties::new().weight(Weight::BOLD),
196 Color::new(255, 0, 0, 255),
197 cx.font_cache(),
198 )
199 .unwrap();
200 let label = Label::new(
201 ".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
202 LabelStyle {
203 text: default_style.clone(),
204 highlight_text: Some(highlight_style.clone()),
205 },
206 )
207 .with_highlights(vec![
208 ".α".len(),
209 ".αβ".len(),
210 ".αβγδ".len(),
211 ".αβγδε.ⓐ".len(),
212 ".αβγδε.ⓐⓑ".len(),
213 ]);
214
215 let runs = label.compute_runs();
216 assert_eq!(
217 runs.as_slice(),
218 &[
219 (".α".len(), default_style.font_id, default_style.color),
220 ("βγ".len(), highlight_style.font_id, highlight_style.color),
221 ("δ".len(), default_style.font_id, default_style.color),
222 ("ε".len(), highlight_style.font_id, highlight_style.color),
223 (".ⓐ".len(), default_style.font_id, default_style.color),
224 ("ⓑⓒ".len(), highlight_style.font_id, highlight_style.color),
225 (
226 "ⓓⓔ.abcde.".len(),
227 default_style.font_id,
228 default_style.color
229 ),
230 ]
231 );
232 }
233}