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