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