text.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use crate::{
  4    color::Color,
  5    fonts::{HighlightStyle, TextStyle},
  6    geometry::{
  7        rect::RectF,
  8        vector::{vec2f, Vector2F},
  9    },
 10    json::{ToJson, Value},
 11    text_layout::{Line, RunStyle, ShapedBoundary},
 12    DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
 13    SizeConstraint, TextLayoutCache,
 14};
 15use serde_json::json;
 16
 17pub struct Text {
 18    text: String,
 19    style: TextStyle,
 20    soft_wrap: bool,
 21    highlights: Vec<(Range<usize>, HighlightStyle)>,
 22}
 23
 24pub struct LayoutState {
 25    shaped_lines: Vec<Line>,
 26    wrap_boundaries: Vec<Vec<ShapedBoundary>>,
 27    line_height: f32,
 28}
 29
 30impl Text {
 31    pub fn new(text: String, style: TextStyle) -> Self {
 32        Self {
 33            text,
 34            style,
 35            soft_wrap: true,
 36            highlights: Vec::new(),
 37        }
 38    }
 39
 40    pub fn with_default_color(mut self, color: Color) -> Self {
 41        self.style.color = color;
 42        self
 43    }
 44
 45    pub fn with_highlights(mut self, runs: Vec<(Range<usize>, HighlightStyle)>) -> Self {
 46        self.highlights = runs;
 47        self
 48    }
 49
 50    pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
 51        self.soft_wrap = soft_wrap;
 52        self
 53    }
 54}
 55
 56impl Element for Text {
 57    type LayoutState = LayoutState;
 58    type PaintState = ();
 59
 60    fn layout(
 61        &mut self,
 62        constraint: SizeConstraint,
 63        cx: &mut LayoutContext,
 64    ) -> (Vector2F, Self::LayoutState) {
 65        // Convert the string and highlight ranges into an iterator of highlighted chunks.
 66        let mut offset = 0;
 67        let mut highlight_ranges = self.highlights.iter().peekable();
 68        let chunks = std::iter::from_fn(|| {
 69            let result;
 70            if let Some((range, highlight_style)) = highlight_ranges.peek() {
 71                if offset < range.start {
 72                    result = Some((
 73                        &self.text[offset..range.start],
 74                        HighlightStyle::from(&self.style),
 75                    ));
 76                    offset = range.start;
 77                } else {
 78                    result = Some((&self.text[range.clone()], *highlight_style));
 79                    highlight_ranges.next();
 80                    offset = range.end;
 81                }
 82            } else if offset < self.text.len() {
 83                result = Some((&self.text[offset..], HighlightStyle::from(&self.style)));
 84                offset = self.text.len();
 85            } else {
 86                result = None;
 87            }
 88            result
 89        });
 90
 91        // Perform shaping on these highlighted chunks
 92        let shaped_lines = layout_highlighted_chunks(
 93            chunks,
 94            &self.style,
 95            cx.text_layout_cache,
 96            &cx.font_cache,
 97            usize::MAX,
 98            self.text.matches('\n').count() + 1,
 99        );
100
101        // If line wrapping is enabled, wrap each of the shaped lines.
102        let font_id = self.style.font_id;
103        let mut line_count = 0;
104        let mut max_line_width = 0_f32;
105        let mut wrap_boundaries = Vec::new();
106        let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
107        for (line, shaped_line) in self.text.lines().zip(&shaped_lines) {
108            if self.soft_wrap {
109                let boundaries = wrapper
110                    .wrap_shaped_line(line, shaped_line, constraint.max.x())
111                    .collect::<Vec<_>>();
112                line_count += boundaries.len() + 1;
113                wrap_boundaries.push(boundaries);
114            } else {
115                line_count += 1;
116            }
117            max_line_width = max_line_width.max(shaped_line.width());
118        }
119
120        let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
121        let size = vec2f(
122            max_line_width
123                .ceil()
124                .max(constraint.min.x())
125                .min(constraint.max.x()),
126            (line_height * line_count as f32).ceil(),
127        );
128        (
129            size,
130            LayoutState {
131                shaped_lines,
132                wrap_boundaries,
133                line_height,
134            },
135        )
136    }
137
138    fn paint(
139        &mut self,
140        bounds: RectF,
141        visible_bounds: RectF,
142        layout: &mut Self::LayoutState,
143        cx: &mut PaintContext,
144    ) -> Self::PaintState {
145        let mut origin = bounds.origin();
146        let empty = Vec::new();
147        for (ix, line) in layout.shaped_lines.iter().enumerate() {
148            let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
149            let boundaries = RectF::new(
150                origin,
151                vec2f(
152                    bounds.width(),
153                    (wrap_boundaries.len() + 1) as f32 * layout.line_height,
154                ),
155            );
156
157            if boundaries.intersects(visible_bounds) {
158                if self.soft_wrap {
159                    line.paint_wrapped(
160                        origin,
161                        visible_bounds,
162                        layout.line_height,
163                        wrap_boundaries.iter().copied(),
164                        cx,
165                    );
166                } else {
167                    line.paint(origin, visible_bounds, layout.line_height, cx);
168                }
169            }
170            origin.set_y(boundaries.max_y());
171        }
172    }
173
174    fn dispatch_event(
175        &mut self,
176        _: &Event,
177        _: RectF,
178        _: &mut Self::LayoutState,
179        _: &mut Self::PaintState,
180        _: &mut EventContext,
181    ) -> bool {
182        false
183    }
184
185    fn debug(
186        &self,
187        bounds: RectF,
188        _: &Self::LayoutState,
189        _: &Self::PaintState,
190        _: &DebugContext,
191    ) -> Value {
192        json!({
193            "type": "Text",
194            "bounds": bounds.to_json(),
195            "text": &self.text,
196            "style": self.style.to_json(),
197        })
198    }
199}
200
201/// Perform text layout on a series of highlighted chunks of text.
202pub fn layout_highlighted_chunks<'a>(
203    chunks: impl Iterator<Item = (&'a str, HighlightStyle)>,
204    text_style: &'a TextStyle,
205    text_layout_cache: &'a TextLayoutCache,
206    font_cache: &'a Arc<FontCache>,
207    max_line_len: usize,
208    max_line_count: usize,
209) -> Vec<Line> {
210    let mut layouts = Vec::with_capacity(max_line_count);
211    let mut prev_font_properties = text_style.font_properties.clone();
212    let mut prev_font_id = text_style.font_id;
213    let mut line = String::new();
214    let mut styles = Vec::new();
215    let mut row = 0;
216    let mut line_exceeded_max_len = false;
217    for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
218        for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
219            if ix > 0 {
220                layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
221                line.clear();
222                styles.clear();
223                row += 1;
224                line_exceeded_max_len = false;
225                if row == max_line_count {
226                    return layouts;
227                }
228            }
229
230            if !line_chunk.is_empty() && !line_exceeded_max_len {
231                // Avoid a lookup if the font properties match the previous ones.
232                let font_id = if highlight_style.font_properties == prev_font_properties {
233                    prev_font_id
234                } else {
235                    font_cache
236                        .select_font(text_style.font_family_id, &highlight_style.font_properties)
237                        .unwrap_or(text_style.font_id)
238                };
239
240                if line.len() + line_chunk.len() > max_line_len {
241                    let mut chunk_len = max_line_len - line.len();
242                    while !line_chunk.is_char_boundary(chunk_len) {
243                        chunk_len -= 1;
244                    }
245                    line_chunk = &line_chunk[..chunk_len];
246                    line_exceeded_max_len = true;
247                }
248
249                line.push_str(line_chunk);
250                styles.push((
251                    line_chunk.len(),
252                    RunStyle {
253                        font_id,
254                        color: highlight_style.color,
255                        underline: highlight_style.underline,
256                    },
257                ));
258                prev_font_id = font_id;
259                prev_font_properties = highlight_style.font_properties;
260            }
261        }
262    }
263
264    layouts
265}