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