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