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