label.rs

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