label.rs

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