label.rs

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