label.rs

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