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