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