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