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::{Invisible, 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.map(|(chunk, style)| HighlightedChunk {
118                chunk,
119                style,
120                is_tab: false,
121            })
122        });
123
124        // Perform shaping on these highlighted chunks
125        let shaped_lines = layout_highlighted_chunks(
126            chunks,
127            &self.style,
128            cx.text_layout_cache(),
129            &cx.font_cache,
130            usize::MAX,
131            self.text.matches('\n').count() + 1,
132            false,
133        );
134
135        // If line wrapping is enabled, wrap each of the shaped lines.
136        let font_id = self.style.font_id;
137        let mut line_count = 0;
138        let mut max_line_width = 0_f32;
139        let mut wrap_boundaries = Vec::new();
140        let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
141        for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) {
142            if self.soft_wrap {
143                let boundaries = wrapper
144                    .wrap_shaped_line(line, shaped_line, constraint.max.x())
145                    .collect::<Vec<_>>();
146                line_count += boundaries.len() + 1;
147                wrap_boundaries.push(boundaries);
148            } else {
149                line_count += 1;
150            }
151            max_line_width = max_line_width.max(shaped_line.width());
152        }
153
154        let line_height = cx.font_cache.line_height(self.style.font_size);
155        let size = vec2f(
156            max_line_width
157                .ceil()
158                .max(constraint.min.x())
159                .min(constraint.max.x()),
160            (line_height * line_count as f32).ceil(),
161        );
162        (
163            size,
164            LayoutState {
165                shaped_lines,
166                wrap_boundaries,
167                line_height,
168            },
169        )
170    }
171
172    fn paint(
173        &mut self,
174        scene: &mut SceneBuilder,
175        bounds: RectF,
176        visible_bounds: RectF,
177        layout: &mut Self::LayoutState,
178        _: &mut V,
179        cx: &mut ViewContext<V>,
180    ) -> Self::PaintState {
181        let mut origin = bounds.origin();
182        let empty = Vec::new();
183        let mut callback = |_, _, _: &mut SceneBuilder, _: &mut AppContext| {};
184
185        let mouse_runs;
186        let custom_run_callback;
187        if let Some((runs, build_region)) = &mut self.custom_runs {
188            mouse_runs = runs.iter();
189            custom_run_callback = build_region.as_mut();
190        } else {
191            mouse_runs = [].iter();
192            custom_run_callback = &mut callback;
193        }
194        let mut custom_runs = mouse_runs.enumerate().peekable();
195
196        let mut offset = 0;
197        for (ix, line) in layout.shaped_lines.iter().enumerate() {
198            let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
199            let boundaries = RectF::new(
200                origin,
201                vec2f(
202                    bounds.width(),
203                    (wrap_boundaries.len() + 1) as f32 * layout.line_height,
204                ),
205            );
206
207            if boundaries.intersects(visible_bounds) {
208                if self.soft_wrap {
209                    line.paint_wrapped(
210                        scene,
211                        origin,
212                        visible_bounds,
213                        layout.line_height,
214                        wrap_boundaries,
215                        cx,
216                    );
217                } else {
218                    line.paint(scene, origin, visible_bounds, layout.line_height, cx);
219                }
220            }
221
222            // Paint any custom runs that intersect this line.
223            let end_offset = offset + line.len();
224            if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() {
225                if custom_run_range.start < end_offset {
226                    let mut current_custom_run = None;
227                    if custom_run_range.start <= offset {
228                        current_custom_run = Some((custom_run_ix, custom_run_range.end, origin));
229                    }
230
231                    let mut glyph_origin = origin;
232                    let mut prev_position = 0.;
233                    let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable();
234                    for (run_ix, glyph_ix, glyph) in
235                        line.runs().iter().enumerate().flat_map(|(run_ix, run)| {
236                            run.glyphs()
237                                .iter()
238                                .enumerate()
239                                .map(move |(ix, glyph)| (run_ix, ix, glyph))
240                        })
241                    {
242                        glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
243                        prev_position = glyph.position.x();
244
245                        // If we've reached a soft wrap position, move down one line. If there
246                        // is a custom run in-progress, paint it.
247                        if wrap_boundaries
248                            .peek()
249                            .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
250                        {
251                            if let Some((run_ix, _, run_origin)) = &mut current_custom_run {
252                                let bounds = RectF::from_points(
253                                    *run_origin,
254                                    glyph_origin + vec2f(0., layout.line_height),
255                                );
256                                custom_run_callback(*run_ix, bounds, scene, cx);
257                                *run_origin =
258                                    vec2f(origin.x(), glyph_origin.y() + layout.line_height);
259                            }
260                            wrap_boundaries.next();
261                            glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height);
262                        }
263
264                        // If we've reached the end of the current custom run, paint it.
265                        if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
266                            if offset + glyph.index == run_end_offset {
267                                current_custom_run.take();
268                                let bounds = RectF::from_points(
269                                    run_origin,
270                                    glyph_origin + vec2f(0., layout.line_height),
271                                );
272                                custom_run_callback(run_ix, bounds, scene, cx);
273                                custom_runs.next();
274                            }
275
276                            if let Some((_, run_range)) = custom_runs.peek() {
277                                if run_range.start >= end_offset {
278                                    break;
279                                }
280                                if run_range.start == offset + glyph.index {
281                                    current_custom_run =
282                                        Some((run_ix, run_range.end, glyph_origin));
283                                }
284                            }
285                        }
286
287                        // If we've reached the start of a new custom run, start tracking it.
288                        if let Some((run_ix, run_range)) = custom_runs.peek() {
289                            if offset + glyph.index == run_range.start {
290                                current_custom_run = Some((*run_ix, run_range.end, glyph_origin));
291                            }
292                        }
293                    }
294
295                    // If a custom run extends beyond the end of the line, paint it.
296                    if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
297                        let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.);
298                        let bounds = RectF::from_points(
299                            run_origin,
300                            line_end + vec2f(0., layout.line_height),
301                        );
302                        custom_run_callback(run_ix, bounds, scene, cx);
303                        if end_offset == run_end_offset {
304                            custom_runs.next();
305                        }
306                    }
307                }
308            }
309
310            offset = end_offset + 1;
311            origin.set_y(boundaries.max_y());
312        }
313    }
314
315    fn rect_for_text_range(
316        &self,
317        _: Range<usize>,
318        _: RectF,
319        _: RectF,
320        _: &Self::LayoutState,
321        _: &Self::PaintState,
322        _: &V,
323        _: &ViewContext<V>,
324    ) -> Option<RectF> {
325        None
326    }
327
328    fn debug(
329        &self,
330        bounds: RectF,
331        _: &Self::LayoutState,
332        _: &Self::PaintState,
333        _: &V,
334        _: &ViewContext<V>,
335    ) -> Value {
336        json!({
337            "type": "Text",
338            "bounds": bounds.to_json(),
339            "text": &self.text,
340            "style": self.style.to_json(),
341        })
342    }
343}
344
345pub struct HighlightedChunk<'a> {
346    pub chunk: &'a str,
347    pub style: Option<HighlightStyle>,
348    pub is_tab: bool,
349}
350
351impl<'a> HighlightedChunk<'a> {
352    fn plain_str(str_symbols: &'a str) -> Self {
353        Self {
354            chunk: str_symbols,
355            style: None,
356            is_tab: str_symbols == "\t",
357        }
358    }
359}
360
361/// Perform text layout on a series of highlighted chunks of text.
362pub fn layout_highlighted_chunks<'a>(
363    chunks: impl Iterator<Item = HighlightedChunk<'a>>,
364    text_style: &TextStyle,
365    text_layout_cache: &TextLayoutCache,
366    font_cache: &Arc<FontCache>,
367    max_line_len: usize,
368    max_line_count: usize,
369    show_invisibles: bool,
370) -> Vec<Line> {
371    let mut layouts = Vec::with_capacity(max_line_count);
372    let mut line = String::new();
373    let mut invisibles = Vec::new();
374    let mut styles = Vec::new();
375    let mut row = 0;
376    let mut line_exceeded_max_len = false;
377    for highlighted_chunk in chunks.chain(std::iter::once(HighlightedChunk::plain_str("\n"))) {
378        for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
379            if ix > 0 {
380                let mut laid_out_line =
381                    text_layout_cache.layout_str(&line, text_style.font_size, &styles);
382                laid_out_line.invisibles.extend(invisibles.drain(..));
383                layouts.push(laid_out_line);
384                line.clear();
385                styles.clear();
386                row += 1;
387                line_exceeded_max_len = false;
388                if row == max_line_count {
389                    return layouts;
390                }
391            }
392
393            if !line_chunk.is_empty() && !line_exceeded_max_len {
394                let text_style = if let Some(style) = highlighted_chunk.style {
395                    text_style
396                        .clone()
397                        .highlight(style, font_cache)
398                        .map(Cow::Owned)
399                        .unwrap_or_else(|_| Cow::Borrowed(text_style))
400                } else {
401                    Cow::Borrowed(text_style)
402                };
403
404                if line.len() + line_chunk.len() > max_line_len {
405                    let mut chunk_len = max_line_len - line.len();
406                    while !line_chunk.is_char_boundary(chunk_len) {
407                        chunk_len -= 1;
408                    }
409                    line_chunk = &line_chunk[..chunk_len];
410                    line_exceeded_max_len = true;
411                }
412
413                styles.push((
414                    line_chunk.len(),
415                    RunStyle {
416                        font_id: text_style.font_id,
417                        color: text_style.color,
418                        underline: text_style.underline,
419                    },
420                ));
421                if show_invisibles && highlighted_chunk.is_tab {
422                    invisibles.push(Invisible::Tab {
423                        range: line.len()..line.len() + line_chunk.len(),
424                    });
425                }
426                line.push_str(line_chunk);
427            }
428        }
429    }
430
431    layouts
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext};
438
439    #[crate::test(self)]
440    fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
441        cx.add_window(Default::default(), |cx| {
442            let mut view = TestView;
443            fonts::with_font_cache(cx.font_cache().clone(), || {
444                let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
445                let mut new_parents = Default::default();
446                let mut notify_views_if_parents_change = Default::default();
447                let mut layout_cx = LayoutContext::new(
448                    cx,
449                    &mut new_parents,
450                    &mut notify_views_if_parents_change,
451                    false,
452                );
453                let (_, state) = text.layout(
454                    SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
455                    &mut view,
456                    &mut layout_cx,
457                );
458                assert_eq!(state.shaped_lines.len(), 2);
459                assert_eq!(state.wrap_boundaries.len(), 2);
460            });
461            view
462        });
463    }
464
465    struct TestView;
466
467    impl Entity for TestView {
468        type Event = ();
469    }
470
471    impl View for TestView {
472        fn ui_name() -> &'static str {
473            "TestView"
474        }
475
476        fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
477            Empty::new().into_any()
478        }
479    }
480}