label.rs

  1use std::{borrow::Cow, ops::Range};
  2
  3use crate::{
  4    fonts::TextStyle,
  5    geometry::{
  6        rect::RectF,
  7        vector::{vec2f, Vector2F},
  8    },
  9    json::{ToJson, Value},
 10    text_layout::{Line, RunStyle},
 11    Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, ViewContext,
 12};
 13use schemars::JsonSchema;
 14use serde::Deserialize;
 15use serde_json::json;
 16use smallvec::{smallvec, SmallVec};
 17
 18pub struct Label {
 19    text: Cow<'static, str>,
 20    style: LabelStyle,
 21    highlight_indices: Vec<usize>,
 22}
 23
 24#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
 25pub struct LabelStyle {
 26    pub text: TextStyle,
 27    pub highlight_text: Option<TextStyle>,
 28}
 29
 30impl From<TextStyle> for LabelStyle {
 31    fn from(text: TextStyle) -> Self {
 32        LabelStyle {
 33            text,
 34            highlight_text: None,
 35        }
 36    }
 37}
 38
 39impl LabelStyle {
 40    pub fn with_font_size(mut self, font_size: f32) -> Self {
 41        self.text.font_size = font_size;
 42        self
 43    }
 44}
 45
 46impl Label {
 47    pub fn new<I: Into<Cow<'static, str>>>(text: I, style: impl Into<LabelStyle>) -> Self {
 48        Self {
 49            text: text.into(),
 50            highlight_indices: Default::default(),
 51            style: style.into(),
 52        }
 53    }
 54
 55    pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
 56        self.highlight_indices = indices;
 57        self
 58    }
 59
 60    fn compute_runs(&self) -> SmallVec<[(usize, RunStyle); 8]> {
 61        let font_id = self.style.text.font_id;
 62        if self.highlight_indices.is_empty() {
 63            return smallvec![(
 64                self.text.len(),
 65                RunStyle {
 66                    font_id,
 67                    color: self.style.text.color,
 68                    underline: self.style.text.underline,
 69                }
 70            )];
 71        }
 72
 73        let highlight_font_id = self
 74            .style
 75            .highlight_text
 76            .as_ref()
 77            .map_or(font_id, |style| style.font_id);
 78
 79        let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
 80        let mut runs = SmallVec::new();
 81        let highlight_style = self
 82            .style
 83            .highlight_text
 84            .as_ref()
 85            .unwrap_or(&self.style.text);
 86
 87        for (char_ix, c) in self.text.char_indices() {
 88            let mut font_id = font_id;
 89            let mut color = self.style.text.color;
 90            let mut underline = self.style.text.underline;
 91            if let Some(highlight_ix) = highlight_indices.peek() {
 92                if char_ix == *highlight_ix {
 93                    font_id = highlight_font_id;
 94                    color = highlight_style.color;
 95                    underline = highlight_style.underline;
 96                    highlight_indices.next();
 97                }
 98            }
 99
100            let last_run: Option<&mut (usize, RunStyle)> = runs.last_mut();
101            let push_new_run = if let Some((last_len, last_style)) = last_run {
102                if font_id == last_style.font_id
103                    && color == last_style.color
104                    && underline == last_style.underline
105                {
106                    *last_len += c.len_utf8();
107                    false
108                } else {
109                    true
110                }
111            } else {
112                true
113            };
114
115            if push_new_run {
116                runs.push((
117                    c.len_utf8(),
118                    RunStyle {
119                        font_id,
120                        color,
121                        underline,
122                    },
123                ));
124            }
125        }
126
127        runs
128    }
129}
130
131impl<V: 'static> Element<V> for Label {
132    type LayoutState = Line;
133    type PaintState = ();
134
135    fn layout(
136        &mut self,
137        constraint: SizeConstraint,
138        _: &mut V,
139        cx: &mut LayoutContext<V>,
140    ) -> (Vector2F, Self::LayoutState) {
141        let runs = self.compute_runs();
142        let line = cx.text_layout_cache().layout_str(
143            &self.text,
144            self.style.text.font_size,
145            runs.as_slice(),
146        );
147
148        let size = vec2f(
149            line.width()
150                .ceil()
151                .max(constraint.min.x())
152                .min(constraint.max.x()),
153            cx.font_cache.line_height(self.style.text.font_size),
154        );
155
156        (size, line)
157    }
158
159    fn paint(
160        &mut self,
161        scene: &mut SceneBuilder,
162        bounds: RectF,
163        visible_bounds: RectF,
164        line: &mut Self::LayoutState,
165        _: &mut V,
166        cx: &mut PaintContext<V>,
167    ) -> Self::PaintState {
168        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
169        line.paint(
170            scene,
171            bounds.origin(),
172            visible_bounds,
173            bounds.size().y(),
174            cx,
175        )
176    }
177
178    fn rect_for_text_range(
179        &self,
180        _: Range<usize>,
181        _: RectF,
182        _: RectF,
183        _: &Self::LayoutState,
184        _: &Self::PaintState,
185        _: &V,
186        _: &ViewContext<V>,
187    ) -> Option<RectF> {
188        None
189    }
190
191    fn debug(
192        &self,
193        bounds: RectF,
194        _: &Self::LayoutState,
195        _: &Self::PaintState,
196        _: &V,
197        _: &ViewContext<V>,
198    ) -> Value {
199        json!({
200            "type": "Label",
201            "bounds": bounds.to_json(),
202            "text": &self.text,
203            "highlight_indices": self.highlight_indices,
204            "style": self.style.to_json(),
205        })
206    }
207}
208
209impl ToJson for LabelStyle {
210    fn to_json(&self) -> Value {
211        json!({
212            "text": self.text.to_json(),
213            "highlight_text": self.highlight_text
214                .as_ref()
215                .map_or(serde_json::Value::Null, |style| style.to_json())
216        })
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::color::Color;
224    use crate::fonts::{Properties as FontProperties, Weight};
225
226    #[crate::test(self)]
227    fn test_layout_label_with_highlights(cx: &mut crate::AppContext) {
228        let default_style = TextStyle::new(
229            "Menlo",
230            12.,
231            Default::default(),
232            Default::default(),
233            Default::default(),
234            Color::black(),
235            cx.font_cache(),
236        )
237        .unwrap();
238        let highlight_style = TextStyle::new(
239            "Menlo",
240            12.,
241            *FontProperties::new().weight(Weight::BOLD),
242            Default::default(),
243            Default::default(),
244            Color::new(255, 0, 0, 255),
245            cx.font_cache(),
246        )
247        .unwrap();
248        let label = Label::new(
249            ".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
250            LabelStyle {
251                text: default_style.clone(),
252                highlight_text: Some(highlight_style.clone()),
253            },
254        )
255        .with_highlights(vec![
256            "".len(),
257            ".αβ".len(),
258            ".αβγδ".len(),
259            ".αβγδε.ⓐ".len(),
260            ".αβγδε.ⓐⓑ".len(),
261        ]);
262
263        let default_run_style = RunStyle {
264            font_id: default_style.font_id,
265            color: default_style.color,
266            underline: default_style.underline,
267        };
268        let highlight_run_style = RunStyle {
269            font_id: highlight_style.font_id,
270            color: highlight_style.color,
271            underline: highlight_style.underline,
272        };
273        let runs = label.compute_runs();
274        assert_eq!(
275            runs.as_slice(),
276            &[
277                ("".len(), default_run_style),
278                ("βγ".len(), highlight_run_style),
279                ("δ".len(), default_run_style),
280                ("ε".len(), highlight_run_style),
281                (".ⓐ".len(), default_run_style),
282                ("ⓑⓒ".len(), highlight_run_style),
283                ("ⓓⓔ.abcde.".len(), default_run_style),
284            ]
285        );
286    }
287}