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    Drawable, FontCache, SceneBuilder, SizeConstraint, TextLayoutCache, View, ViewContext,
 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: Vec<(Range<usize>, HighlightStyle)>,
 21}
 22
 23pub struct LayoutState {
 24    shaped_lines: Vec<Line>,
 25    wrap_boundaries: Vec<Vec<ShapedBoundary>>,
 26    line_height: f32,
 27}
 28
 29impl Text {
 30    pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
 31        Self {
 32            text: text.into(),
 33            style,
 34            soft_wrap: true,
 35            highlights: Vec::new(),
 36        }
 37    }
 38
 39    pub fn with_default_color(mut self, color: Color) -> Self {
 40        self.style.color = color;
 41        self
 42    }
 43
 44    pub fn with_highlights(mut self, runs: Vec<(Range<usize>, HighlightStyle)>) -> Self {
 45        self.highlights = runs;
 46        self
 47    }
 48
 49    pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
 50        self.soft_wrap = soft_wrap;
 51        self
 52    }
 53}
 54
 55impl<V: View> Drawable<V> for Text {
 56    type LayoutState = LayoutState;
 57    type PaintState = ();
 58
 59    fn layout(
 60        &mut self,
 61        constraint: SizeConstraint,
 62        _: &mut V,
 63        cx: &mut ViewContext<V>,
 64    ) -> (Vector2F, Self::LayoutState) {
 65        // Convert the string and highlight ranges into an iterator of highlighted chunks.
 66
 67        let mut offset = 0;
 68        let mut highlight_ranges = self.highlights.iter().peekable();
 69        let chunks = std::iter::from_fn(|| {
 70            let result;
 71            if let Some((range, highlight_style)) = highlight_ranges.peek() {
 72                if offset < range.start {
 73                    result = Some((&self.text[offset..range.start], None));
 74                    offset = range.start;
 75                } else if range.end <= self.text.len() {
 76                    result = Some((&self.text[range.clone()], Some(*highlight_style)));
 77                    highlight_ranges.next();
 78                    offset = range.end;
 79                } else {
 80                    warn!(
 81                        "Highlight out of text range. Text len: {}, Highlight range: {}..{}",
 82                        self.text.len(),
 83                        range.start,
 84                        range.end
 85                    );
 86                    result = None;
 87                }
 88            } else if offset < self.text.len() {
 89                result = Some((&self.text[offset..], None));
 90                offset = self.text.len();
 91            } else {
 92                result = None;
 93            }
 94            result
 95        });
 96
 97        // Perform shaping on these highlighted chunks
 98        let shaped_lines = layout_highlighted_chunks(
 99            chunks,
100            &self.style,
101            cx.text_layout_cache(),
102            &cx.font_cache,
103            usize::MAX,
104            self.text.matches('\n').count() + 1,
105        );
106
107        // If line wrapping is enabled, wrap each of the shaped lines.
108        let font_id = self.style.font_id;
109        let mut line_count = 0;
110        let mut max_line_width = 0_f32;
111        let mut wrap_boundaries = Vec::new();
112        let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
113        for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) {
114            if self.soft_wrap {
115                let boundaries = wrapper
116                    .wrap_shaped_line(line, shaped_line, constraint.max.x())
117                    .collect::<Vec<_>>();
118                line_count += boundaries.len() + 1;
119                wrap_boundaries.push(boundaries);
120            } else {
121                line_count += 1;
122            }
123            max_line_width = max_line_width.max(shaped_line.width());
124        }
125
126        let line_height = cx.font_cache.line_height(self.style.font_size);
127        let size = vec2f(
128            max_line_width
129                .ceil()
130                .max(constraint.min.x())
131                .min(constraint.max.x()),
132            (line_height * line_count as f32).ceil(),
133        );
134        (
135            size,
136            LayoutState {
137                shaped_lines,
138                wrap_boundaries,
139                line_height,
140            },
141        )
142    }
143
144    fn paint(
145        &mut self,
146        scene: &mut SceneBuilder,
147        bounds: RectF,
148        visible_bounds: RectF,
149        layout: &mut Self::LayoutState,
150        _: &mut V,
151        cx: &mut ViewContext<V>,
152    ) -> Self::PaintState {
153        let mut origin = bounds.origin();
154        let empty = Vec::new();
155        for (ix, line) in layout.shaped_lines.iter().enumerate() {
156            let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
157            let boundaries = RectF::new(
158                origin,
159                vec2f(
160                    bounds.width(),
161                    (wrap_boundaries.len() + 1) as f32 * layout.line_height,
162                ),
163            );
164
165            if boundaries.intersects(visible_bounds) {
166                if self.soft_wrap {
167                    line.paint_wrapped(
168                        scene,
169                        origin,
170                        visible_bounds,
171                        layout.line_height,
172                        wrap_boundaries.iter().copied(),
173                        cx,
174                    );
175                } else {
176                    line.paint(scene, origin, visible_bounds, layout.line_height, cx);
177                }
178            }
179            origin.set_y(boundaries.max_y());
180        }
181    }
182
183    fn rect_for_text_range(
184        &self,
185        _: Range<usize>,
186        _: RectF,
187        _: RectF,
188        _: &Self::LayoutState,
189        _: &Self::PaintState,
190        _: &V,
191        _: &ViewContext<V>,
192    ) -> Option<RectF> {
193        None
194    }
195
196    fn debug(
197        &self,
198        bounds: RectF,
199        _: &Self::LayoutState,
200        _: &Self::PaintState,
201        _: &V,
202        _: &ViewContext<V>,
203    ) -> Value {
204        json!({
205            "type": "Text",
206            "bounds": bounds.to_json(),
207            "text": &self.text,
208            "style": self.style.to_json(),
209        })
210    }
211}
212
213/// Perform text layout on a series of highlighted chunks of text.
214pub fn layout_highlighted_chunks<'a>(
215    chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
216    text_style: &TextStyle,
217    text_layout_cache: &TextLayoutCache,
218    font_cache: &Arc<FontCache>,
219    max_line_len: usize,
220    max_line_count: usize,
221) -> Vec<Line> {
222    let mut layouts = Vec::with_capacity(max_line_count);
223    let mut line = String::new();
224    let mut styles = Vec::new();
225    let mut row = 0;
226    let mut line_exceeded_max_len = false;
227    for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
228        for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
229            if ix > 0 {
230                layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
231                line.clear();
232                styles.clear();
233                row += 1;
234                line_exceeded_max_len = false;
235                if row == max_line_count {
236                    return layouts;
237                }
238            }
239
240            if !line_chunk.is_empty() && !line_exceeded_max_len {
241                let text_style = if let Some(style) = highlight_style {
242                    text_style
243                        .clone()
244                        .highlight(style, font_cache)
245                        .map(Cow::Owned)
246                        .unwrap_or_else(|_| Cow::Borrowed(text_style))
247                } else {
248                    Cow::Borrowed(text_style)
249                };
250
251                if line.len() + line_chunk.len() > max_line_len {
252                    let mut chunk_len = max_line_len - line.len();
253                    while !line_chunk.is_char_boundary(chunk_len) {
254                        chunk_len -= 1;
255                    }
256                    line_chunk = &line_chunk[..chunk_len];
257                    line_exceeded_max_len = true;
258                }
259
260                line.push_str(line_chunk);
261                styles.push((
262                    line_chunk.len(),
263                    RunStyle {
264                        font_id: text_style.font_id,
265                        color: text_style.color,
266                        underline: text_style.underline,
267                    },
268                ));
269            }
270        }
271    }
272
273    layouts
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::{elements::Empty, fonts, AppContext, Element, Entity, View, ViewContext};
280
281    #[crate::test(self)]
282    fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
283        let (window_id, root_view) = cx.add_window(Default::default(), |_| TestView);
284        fonts::with_font_cache(cx.font_cache().clone(), || {
285            root_view.update(cx, |view, cx| {
286                let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
287                let (_, state) = text.layout(
288                    SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
289                    view,
290                    cx,
291                );
292                assert_eq!(state.shaped_lines.len(), 2);
293                assert_eq!(state.wrap_boundaries.len(), 2);
294            });
295        });
296    }
297
298    struct TestView;
299
300    impl Entity for TestView {
301        type Event = ();
302    }
303
304    impl View for TestView {
305        fn ui_name() -> &'static str {
306            "TestView"
307        }
308
309        fn render(&mut self, _: &mut ViewContext<Self>) -> Element<Self> {
310            Empty::new().boxed()
311        }
312    }
313}