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