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, LayoutContext, PaintContext, SizeConstraint, TextLayoutCache, ViewContext,
 11    WindowContext,
 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 WindowContext)>,
 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 WindowContext),
 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: 'static> 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        bounds: RectF,
170        visible_bounds: RectF,
171        layout: &mut Self::LayoutState,
172        _: &mut V,
173        cx: &mut PaintContext<V>,
174    ) -> Self::PaintState {
175        let mut origin = bounds.origin();
176        let empty = Vec::new();
177        let mut callback = |_, _, _: &mut WindowContext| {};
178
179        let mouse_runs;
180        let custom_run_callback;
181        if let Some((runs, build_region)) = &mut self.custom_runs {
182            mouse_runs = runs.iter();
183            custom_run_callback = build_region.as_mut();
184        } else {
185            mouse_runs = [].iter();
186            custom_run_callback = &mut callback;
187        }
188        let mut custom_runs = mouse_runs.enumerate().peekable();
189
190        let mut offset = 0;
191        for (ix, line) in layout.shaped_lines.iter().enumerate() {
192            let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
193            let boundaries = RectF::new(
194                origin,
195                vec2f(
196                    bounds.width(),
197                    (wrap_boundaries.len() + 1) as f32 * layout.line_height,
198                ),
199            );
200
201            if boundaries.intersects(visible_bounds) {
202                if self.soft_wrap {
203                    line.paint_wrapped(
204                        origin,
205                        visible_bounds,
206                        layout.line_height,
207                        wrap_boundaries,
208                        cx,
209                    );
210                } else {
211                    line.paint(origin, visible_bounds, layout.line_height, cx);
212                }
213            }
214
215            // Paint any custom runs that intersect this line.
216            let end_offset = offset + line.len();
217            if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() {
218                if custom_run_range.start < end_offset {
219                    let mut current_custom_run = None;
220                    if custom_run_range.start <= offset {
221                        current_custom_run = Some((custom_run_ix, custom_run_range.end, origin));
222                    }
223
224                    let mut glyph_origin = origin;
225                    let mut prev_position = 0.;
226                    let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable();
227                    for (run_ix, glyph_ix, glyph) in
228                        line.runs().iter().enumerate().flat_map(|(run_ix, run)| {
229                            run.glyphs()
230                                .iter()
231                                .enumerate()
232                                .map(move |(ix, glyph)| (run_ix, ix, glyph))
233                        })
234                    {
235                        glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
236                        prev_position = glyph.position.x();
237
238                        // If we've reached a soft wrap position, move down one line. If there
239                        // is a custom run in-progress, paint it.
240                        if wrap_boundaries
241                            .peek()
242                            .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
243                        {
244                            if let Some((run_ix, _, run_origin)) = &mut current_custom_run {
245                                let bounds = RectF::from_points(
246                                    *run_origin,
247                                    glyph_origin + vec2f(0., layout.line_height),
248                                );
249                                custom_run_callback(*run_ix, bounds, cx);
250                                *run_origin =
251                                    vec2f(origin.x(), glyph_origin.y() + layout.line_height);
252                            }
253                            wrap_boundaries.next();
254                            glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height);
255                        }
256
257                        // If we've reached the end of the current custom run, paint it.
258                        if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
259                            if offset + glyph.index == run_end_offset {
260                                current_custom_run.take();
261                                let bounds = RectF::from_points(
262                                    run_origin,
263                                    glyph_origin + vec2f(0., layout.line_height),
264                                );
265                                custom_run_callback(run_ix, bounds, cx);
266                                custom_runs.next();
267                            }
268
269                            if let Some((_, run_range)) = custom_runs.peek() {
270                                if run_range.start >= end_offset {
271                                    break;
272                                }
273                                if run_range.start == offset + glyph.index {
274                                    current_custom_run =
275                                        Some((run_ix, run_range.end, glyph_origin));
276                                }
277                            }
278                        }
279
280                        // If we've reached the start of a new custom run, start tracking it.
281                        if let Some((run_ix, run_range)) = custom_runs.peek() {
282                            if offset + glyph.index == run_range.start {
283                                current_custom_run = Some((*run_ix, run_range.end, glyph_origin));
284                            }
285                        }
286                    }
287
288                    // If a custom run extends beyond the end of the line, paint it.
289                    if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
290                        let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.);
291                        let bounds = RectF::from_points(
292                            run_origin,
293                            line_end + vec2f(0., layout.line_height),
294                        );
295                        custom_run_callback(run_ix, bounds, cx);
296                        if end_offset == run_end_offset {
297                            custom_runs.next();
298                        }
299                    }
300                }
301            }
302
303            offset = end_offset + 1;
304            origin.set_y(boundaries.max_y());
305        }
306    }
307
308    fn rect_for_text_range(
309        &self,
310        _: Range<usize>,
311        _: RectF,
312        _: RectF,
313        _: &Self::LayoutState,
314        _: &Self::PaintState,
315        _: &V,
316        _: &ViewContext<V>,
317    ) -> Option<RectF> {
318        None
319    }
320
321    fn debug(
322        &self,
323        bounds: RectF,
324        _: &Self::LayoutState,
325        _: &Self::PaintState,
326        _: &V,
327        _: &ViewContext<V>,
328    ) -> Value {
329        json!({
330            "type": "Text",
331            "bounds": bounds.to_json(),
332            "text": &self.text,
333            "style": self.style.to_json(),
334        })
335    }
336}
337
338/// Perform text layout on a series of highlighted chunks of text.
339pub fn layout_highlighted_chunks<'a>(
340    chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
341    text_style: &TextStyle,
342    text_layout_cache: &TextLayoutCache,
343    font_cache: &Arc<FontCache>,
344    max_line_len: usize,
345    max_line_count: usize,
346) -> Vec<Line> {
347    let mut layouts = Vec::with_capacity(max_line_count);
348    let mut line = String::new();
349    let mut styles = Vec::new();
350    let mut row = 0;
351    let mut line_exceeded_max_len = false;
352    for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
353        for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
354            if ix > 0 {
355                layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
356                line.clear();
357                styles.clear();
358                row += 1;
359                line_exceeded_max_len = false;
360                if row == max_line_count {
361                    return layouts;
362                }
363            }
364
365            if !line_chunk.is_empty() && !line_exceeded_max_len {
366                let text_style = if let Some(style) = highlight_style {
367                    text_style
368                        .clone()
369                        .highlight(style, font_cache)
370                        .map(Cow::Owned)
371                        .unwrap_or_else(|_| Cow::Borrowed(text_style))
372                } else {
373                    Cow::Borrowed(text_style)
374                };
375
376                if line.len() + line_chunk.len() > max_line_len {
377                    let mut chunk_len = max_line_len - line.len();
378                    while !line_chunk.is_char_boundary(chunk_len) {
379                        chunk_len -= 1;
380                    }
381                    line_chunk = &line_chunk[..chunk_len];
382                    line_exceeded_max_len = true;
383                }
384
385                line.push_str(line_chunk);
386                styles.push((
387                    line_chunk.len(),
388                    RunStyle {
389                        font_id: text_style.font_id,
390                        color: text_style.color,
391                        underline: text_style.underline,
392                    },
393                ));
394            }
395        }
396    }
397
398    layouts
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext};
405
406    #[crate::test(self)]
407    fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
408        cx.add_window(Default::default(), |cx| {
409            let mut view = TestView;
410            fonts::with_font_cache(cx.font_cache().clone(), || {
411                let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
412                let mut layout_cx = LayoutContext::new(cx);
413                let (_, state) = text.layout(
414                    SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
415                    &mut view,
416                    &mut layout_cx,
417                );
418                assert_eq!(state.shaped_lines.len(), 2);
419                assert_eq!(state.wrap_boundaries.len(), 2);
420            });
421            view
422        });
423    }
424
425    struct TestView;
426
427    impl Entity for TestView {
428        type Event = ();
429    }
430
431    impl View for TestView {
432        fn ui_name() -> &'static str {
433            "TestView"
434        }
435
436        fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
437            Empty::new().into_any()
438        }
439    }
440}