text.rs

  1use crate::{
  2    color::Color,
  3    fonts::{HighlightStyle, TextStyle},
  4    geometry::{
  5        rect::RectF,
  6        vector::{vec2f, Vector2F},
  7    },
  8    json::{ToJson, Value},
  9    text_layout::{Line, RunStyle, ShapedBoundary},
 10    AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
 11    View, ViewContext,
 12};
 13use log::warn;
 14use serde_json::json;
 15use std::{borrow::Cow, ops::Range, sync::Arc};
 16
 17pub struct Text {
 18    text: Cow<'static, str>,
 19    style: TextStyle,
 20    soft_wrap: bool,
 21    highlights: Option<Box<[(Range<usize>, HighlightStyle)]>>,
 22    custom_runs: Option<(
 23        Box<[Range<usize>]>,
 24        Box<dyn FnMut(usize, RectF, &mut SceneBuilder, &mut AppContext)>,
 25    )>,
 26}
 27
 28pub struct LayoutState {
 29    shaped_lines: Vec<Line>,
 30    wrap_boundaries: Vec<Vec<ShapedBoundary>>,
 31    line_height: f32,
 32}
 33
 34impl Text {
 35    pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
 36        Self {
 37            text: text.into(),
 38            style,
 39            soft_wrap: true,
 40            highlights: None,
 41            custom_runs: None,
 42        }
 43    }
 44
 45    pub fn with_default_color(mut self, color: Color) -> Self {
 46        self.style.color = color;
 47        self
 48    }
 49
 50    pub fn with_highlights(
 51        mut self,
 52        runs: impl Into<Box<[(Range<usize>, HighlightStyle)]>>,
 53    ) -> Self {
 54        self.highlights = Some(runs.into());
 55        self
 56    }
 57
 58    pub fn with_custom_runs(
 59        mut self,
 60        runs: impl Into<Box<[Range<usize>]>>,
 61        callback: impl 'static + FnMut(usize, RectF, &mut SceneBuilder, &mut AppContext),
 62    ) -> Self {
 63        self.custom_runs = Some((runs.into(), Box::new(callback)));
 64        self
 65    }
 66
 67    pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
 68        self.soft_wrap = soft_wrap;
 69        self
 70    }
 71}
 72
 73impl<V: View> Element<V> for Text {
 74    type LayoutState = LayoutState;
 75    type PaintState = ();
 76
 77    fn layout(
 78        &mut self,
 79        constraint: SizeConstraint,
 80        _: &mut V,
 81        cx: &mut LayoutContext<V>,
 82    ) -> (Vector2F, Self::LayoutState) {
 83        // Convert the string and highlight ranges into an iterator of highlighted chunks.
 84
 85        let mut offset = 0;
 86        let mut highlight_ranges = self
 87            .highlights
 88            .as_ref()
 89            .map_or(Default::default(), AsRef::as_ref)
 90            .iter()
 91            .peekable();
 92        let chunks = std::iter::from_fn(|| {
 93            let result;
 94            if let Some((range, highlight_style)) = highlight_ranges.peek() {
 95                if offset < range.start {
 96                    result = Some((&self.text[offset..range.start], None));
 97                    offset = range.start;
 98                } else if range.end <= self.text.len() {
 99                    result = Some((&self.text[range.clone()], Some(*highlight_style)));
100                    highlight_ranges.next();
101                    offset = range.end;
102                } else {
103                    warn!(
104                        "Highlight out of text range. Text len: {}, Highlight range: {}..{}",
105                        self.text.len(),
106                        range.start,
107                        range.end
108                    );
109                    result = None;
110                }
111            } else if offset < self.text.len() {
112                result = Some((&self.text[offset..], None));
113                offset = self.text.len();
114            } else {
115                result = None;
116            }
117            result
118        });
119
120        // Perform shaping on these highlighted chunks
121        let shaped_lines = layout_highlighted_chunks(
122            chunks,
123            &self.style,
124            cx.text_layout_cache(),
125            &cx.font_cache,
126            usize::MAX,
127            self.text.matches('\n').count() + 1,
128        );
129
130        // If line wrapping is enabled, wrap each of the shaped lines.
131        let font_id = self.style.font_id;
132        let mut line_count = 0;
133        let mut max_line_width = 0_f32;
134        let mut wrap_boundaries = Vec::new();
135        let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
136        for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) {
137            if self.soft_wrap {
138                let boundaries = wrapper
139                    .wrap_shaped_line(line, shaped_line, constraint.max.x())
140                    .collect::<Vec<_>>();
141                line_count += boundaries.len() + 1;
142                wrap_boundaries.push(boundaries);
143            } else {
144                line_count += 1;
145            }
146            max_line_width = max_line_width.max(shaped_line.width());
147        }
148
149        let line_height = cx.font_cache.line_height(self.style.font_size);
150        let size = vec2f(
151            max_line_width
152                .ceil()
153                .max(constraint.min.x())
154                .min(constraint.max.x()),
155            (line_height * line_count as f32).ceil(),
156        );
157        (
158            size,
159            LayoutState {
160                shaped_lines,
161                wrap_boundaries,
162                line_height,
163            },
164        )
165    }
166
167    fn paint(
168        &mut self,
169        scene: &mut SceneBuilder,
170        bounds: RectF,
171        visible_bounds: RectF,
172        layout: &mut Self::LayoutState,
173        _: &mut V,
174        cx: &mut ViewContext<V>,
175    ) -> Self::PaintState {
176        let mut origin = bounds.origin();
177        let empty = Vec::new();
178        let mut callback = |_, _, _: &mut SceneBuilder, _: &mut AppContext| {};
179
180        let mouse_runs;
181        let custom_run_callback;
182        if let Some((runs, build_region)) = &mut self.custom_runs {
183            mouse_runs = runs.iter();
184            custom_run_callback = build_region.as_mut();
185        } else {
186            mouse_runs = [].iter();
187            custom_run_callback = &mut callback;
188        }
189        let mut custom_runs = mouse_runs.enumerate().peekable();
190
191        let mut offset = 0;
192        for (ix, line) in layout.shaped_lines.iter().enumerate() {
193            let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
194            let boundaries = RectF::new(
195                origin,
196                vec2f(
197                    bounds.width(),
198                    (wrap_boundaries.len() + 1) as f32 * layout.line_height,
199                ),
200            );
201
202            if boundaries.intersects(visible_bounds) {
203                if self.soft_wrap {
204                    line.paint_wrapped(
205                        scene,
206                        origin,
207                        visible_bounds,
208                        layout.line_height,
209                        wrap_boundaries,
210                        cx,
211                    );
212                } else {
213                    line.paint(scene, origin, visible_bounds, layout.line_height, cx);
214                }
215            }
216
217            // Paint any custom runs that intersect this line.
218            let end_offset = offset + line.len();
219            if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() {
220                if custom_run_range.start < end_offset {
221                    let mut current_custom_run = None;
222                    if custom_run_range.start <= offset {
223                        current_custom_run = Some((custom_run_ix, custom_run_range.end, origin));
224                    }
225
226                    let mut glyph_origin = origin;
227                    let mut prev_position = 0.;
228                    let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable();
229                    for (run_ix, glyph_ix, glyph) in
230                        line.runs().iter().enumerate().flat_map(|(run_ix, run)| {
231                            run.glyphs()
232                                .iter()
233                                .enumerate()
234                                .map(move |(ix, glyph)| (run_ix, ix, glyph))
235                        })
236                    {
237                        glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
238                        prev_position = glyph.position.x();
239
240                        // If we've reached a soft wrap position, move down one line. If there
241                        // is a custom run in-progress, paint it.
242                        if wrap_boundaries
243                            .peek()
244                            .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
245                        {
246                            if let Some((run_ix, _, run_origin)) = &mut current_custom_run {
247                                let bounds = RectF::from_points(
248                                    *run_origin,
249                                    glyph_origin + vec2f(0., layout.line_height),
250                                );
251                                custom_run_callback(*run_ix, bounds, scene, cx);
252                                *run_origin =
253                                    vec2f(origin.x(), glyph_origin.y() + layout.line_height);
254                            }
255                            wrap_boundaries.next();
256                            glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height);
257                        }
258
259                        // If we've reached the end of the current custom run, paint it.
260                        if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
261                            if offset + glyph.index == run_end_offset {
262                                current_custom_run.take();
263                                let bounds = RectF::from_points(
264                                    run_origin,
265                                    glyph_origin + vec2f(0., layout.line_height),
266                                );
267                                custom_run_callback(run_ix, bounds, scene, cx);
268                                custom_runs.next();
269                            }
270
271                            if let Some((_, run_range)) = custom_runs.peek() {
272                                if run_range.start >= end_offset {
273                                    break;
274                                }
275                                if run_range.start == offset + glyph.index {
276                                    current_custom_run =
277                                        Some((run_ix, run_range.end, glyph_origin));
278                                }
279                            }
280                        }
281
282                        // If we've reached the start of a new custom run, start tracking it.
283                        if let Some((run_ix, run_range)) = custom_runs.peek() {
284                            if offset + glyph.index == run_range.start {
285                                current_custom_run = Some((*run_ix, run_range.end, glyph_origin));
286                            }
287                        }
288                    }
289
290                    // If a custom run extends beyond the end of the line, paint it.
291                    if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
292                        let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.);
293                        let bounds = RectF::from_points(
294                            run_origin,
295                            line_end + vec2f(0., layout.line_height),
296                        );
297                        custom_run_callback(run_ix, bounds, scene, cx);
298                        if end_offset == run_end_offset {
299                            custom_runs.next();
300                        }
301                    }
302                }
303            }
304
305            offset = end_offset + 1;
306            origin.set_y(boundaries.max_y());
307        }
308    }
309
310    fn rect_for_text_range(
311        &self,
312        _: Range<usize>,
313        _: RectF,
314        _: RectF,
315        _: &Self::LayoutState,
316        _: &Self::PaintState,
317        _: &V,
318        _: &ViewContext<V>,
319    ) -> Option<RectF> {
320        None
321    }
322
323    fn debug(
324        &self,
325        bounds: RectF,
326        _: &Self::LayoutState,
327        _: &Self::PaintState,
328        _: &V,
329        _: &ViewContext<V>,
330    ) -> Value {
331        json!({
332            "type": "Text",
333            "bounds": bounds.to_json(),
334            "text": &self.text,
335            "style": self.style.to_json(),
336        })
337    }
338}
339
340/// Perform text layout on a series of highlighted chunks of text.
341fn layout_highlighted_chunks<'a>(
342    chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
343    text_style: &TextStyle,
344    text_layout_cache: &TextLayoutCache,
345    font_cache: &Arc<FontCache>,
346    max_line_len: usize,
347    max_line_count: usize,
348) -> Vec<Line> {
349    let mut layouts = Vec::with_capacity(max_line_count);
350    let mut line = String::new();
351    let mut styles = Vec::new();
352    let mut row = 0;
353    let mut line_exceeded_max_len = false;
354    for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
355        for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
356            if ix > 0 {
357                layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
358                line.clear();
359                styles.clear();
360                row += 1;
361                line_exceeded_max_len = false;
362                if row == max_line_count {
363                    return layouts;
364                }
365            }
366
367            if !line_chunk.is_empty() && !line_exceeded_max_len {
368                let text_style = if let Some(style) = highlight_style {
369                    text_style
370                        .clone()
371                        .highlight(style, font_cache)
372                        .map(Cow::Owned)
373                        .unwrap_or_else(|_| Cow::Borrowed(text_style))
374                } else {
375                    Cow::Borrowed(text_style)
376                };
377
378                if line.len() + line_chunk.len() > max_line_len {
379                    let mut chunk_len = max_line_len - line.len();
380                    while !line_chunk.is_char_boundary(chunk_len) {
381                        chunk_len -= 1;
382                    }
383                    line_chunk = &line_chunk[..chunk_len];
384                    line_exceeded_max_len = true;
385                }
386
387                line.push_str(line_chunk);
388                styles.push((
389                    line_chunk.len(),
390                    RunStyle {
391                        font_id: text_style.font_id,
392                        color: text_style.color,
393                        underline: text_style.underline,
394                    },
395                ));
396            }
397        }
398    }
399
400    layouts
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext};
407
408    #[crate::test(self)]
409    fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
410        cx.add_window(Default::default(), |cx| {
411            let mut view = TestView;
412            fonts::with_font_cache(cx.font_cache().clone(), || {
413                let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
414                let mut new_parents = Default::default();
415                let mut notify_views_if_parents_change = Default::default();
416                let mut layout_cx = LayoutContext::new(
417                    cx,
418                    &mut new_parents,
419                    &mut notify_views_if_parents_change,
420                    false,
421                );
422                let (_, state) = text.layout(
423                    SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
424                    &mut view,
425                    &mut layout_cx,
426                );
427                assert_eq!(state.shaped_lines.len(), 2);
428                assert_eq!(state.wrap_boundaries.len(), 2);
429            });
430            view
431        });
432    }
433
434    struct TestView;
435
436    impl Entity for TestView {
437        type Event = ();
438    }
439
440    impl View for TestView {
441        fn ui_name() -> &'static str {
442            "TestView"
443        }
444
445        fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
446            Empty::new().into_any()
447        }
448    }
449}