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