label.rs

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