Skip to main content

gpui_authoring/
text_system.rs

1mod line;
2#[cfg(test)]
3mod line_wrapper_tests;
4
5pub use line::*;
6
7use crate::{
8    FontId, FontRun, LineLayout, LineLayoutIndex, Pixels, SharedString, TextRun, TextSystem,
9};
10use anyhow::Result;
11use derive_more::Deref;
12use smallvec::SmallVec;
13use std::cmp;
14use std::ops::Range;
15use std::sync::Arc;
16
17/// The GPUI text layout subsystem.
18#[derive(Deref)]
19pub struct WindowTextSystem {
20    #[deref]
21    text_system: Arc<dyn TextSystem>,
22}
23
24impl WindowTextSystem {
25    /// Creates a window-scoped text system over the given engine text system.
26    pub fn new(text_system: Arc<dyn TextSystem>) -> Self {
27        Self { text_system }
28    }
29
30    pub(crate) fn layout_index(&self) -> LineLayoutIndex {
31        self.text_system.layout_index()
32    }
33
34    pub(crate) fn reuse_layouts(&self, index: Range<LineLayoutIndex>) {
35        self.text_system.reuse_layouts(index)
36    }
37
38    pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) {
39        self.text_system.truncate_layouts(index)
40    }
41
42    /// Shape the given line, at the given font_size, for painting to the screen.
43    /// Subsets of the line can be styled independently with the `runs` parameter.
44    ///
45    /// Note that this method can only shape a single line of text. It will panic
46    /// if the text contains newlines. If you need to shape multiple lines of text,
47    /// use [`Self::shape_text`] instead.
48    pub fn shape_line(
49        &self,
50        text: SharedString,
51        font_size: Pixels,
52        runs: &[TextRun],
53        force_width: Option<Pixels>,
54    ) -> ShapedLine {
55        debug_assert!(
56            text.find('\n').is_none(),
57            "text argument should not contain newlines"
58        );
59
60        let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
61        for run in runs {
62            if let Some(last_run) = decoration_runs.last_mut()
63                && last_run.color == run.color
64                && last_run.underline == run.underline
65                && last_run.strikethrough == run.strikethrough
66                && last_run.background_color == run.background_color
67            {
68                last_run.len += run.len as u32;
69                continue;
70            }
71            decoration_runs.push(DecorationRun {
72                len: run.len as u32,
73                color: run.color,
74                background_color: run.background_color,
75                underline: run.underline,
76                strikethrough: run.strikethrough,
77            });
78        }
79
80        let layout = self.layout_line(&text, font_size, runs, force_width);
81
82        ShapedLine {
83            layout,
84            text,
85            decoration_runs,
86        }
87    }
88
89    /// Shape the given line using a caller-provided content hash as the cache key.
90    ///
91    /// This enables cache hits without materializing a contiguous `SharedString` for the text.
92    /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
93    ///
94    /// Contract (caller enforced):
95    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
96    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
97    ///
98    /// Like [`Self::shape_line`], this must be used only for single-line text (no `\n`).
99    pub fn shape_line_by_hash(
100        &self,
101        text_hash: u64,
102        text_len: usize,
103        font_size: Pixels,
104        runs: &[TextRun],
105        force_width: Option<Pixels>,
106        materialize_text: impl FnOnce() -> SharedString + 'static,
107    ) -> ShapedLine {
108        let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
109        for run in runs {
110            if let Some(last_run) = decoration_runs.last_mut()
111                && last_run.color == run.color
112                && last_run.underline == run.underline
113                && last_run.strikethrough == run.strikethrough
114                && last_run.background_color == run.background_color
115            {
116                last_run.len += run.len as u32;
117                continue;
118            }
119            decoration_runs.push(DecorationRun {
120                len: run.len as u32,
121                color: run.color,
122                background_color: run.background_color,
123                underline: run.underline,
124                strikethrough: run.strikethrough,
125            });
126        }
127
128        let mut used_force_width = force_width;
129        let layout = self.layout_line_by_hash(
130            text_hash,
131            text_len,
132            font_size,
133            runs,
134            used_force_width,
135            || {
136                let text = materialize_text();
137                debug_assert!(
138                    text.find('\n').is_none(),
139                    "text argument should not contain newlines"
140                );
141                text
142            },
143        );
144
145        // We only materialize actual text on cache miss; on hit we avoid allocations.
146        // Since `ShapedLine` carries a `SharedString`, use an empty placeholder for hits.
147        // NOTE: Callers must not rely on `ShapedLine.text` for content when using this API.
148        let text: SharedString = SharedString::new_static("");
149
150        ShapedLine {
151            layout,
152            text,
153            decoration_runs,
154        }
155    }
156
157    /// Shape a multi line string of text, at the given font_size, for painting to the screen.
158    /// Subsets of the text can be styled independently with the `runs` parameter.
159    /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width.
160    pub fn shape_text(
161        &self,
162        text: SharedString,
163        font_size: Pixels,
164        runs: &[TextRun],
165        wrap_width: Option<Pixels>,
166        line_clamp: Option<usize>,
167    ) -> Result<SmallVec<[WrappedLine; 1]>> {
168        let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable();
169        let mut font_runs = self.take_font_runs();
170
171        let mut lines = SmallVec::new();
172        let mut max_wrap_lines = line_clamp;
173        let mut wrapped_lines = 0;
174
175        let mut process_line = |line_text: SharedString, line_start, line_end| {
176            font_runs.clear();
177
178            let mut decoration_runs = <Vec<DecorationRun>>::with_capacity(32);
179            let mut run_start = line_start;
180            while run_start < line_end {
181                let Some(run) = runs.peek_mut() else {
182                    log::warn!("`TextRun`s do not cover the entire to be shaped text");
183                    break;
184                };
185
186                let run_len_within_line = cmp::min(line_end - run_start, run.len);
187
188                let decoration_changed = if let Some(last_run) = decoration_runs.last_mut()
189                    && last_run.color == run.color
190                    && last_run.underline == run.underline
191                    && last_run.strikethrough == run.strikethrough
192                    && last_run.background_color == run.background_color
193                {
194                    last_run.len += run_len_within_line as u32;
195                    false
196                } else {
197                    decoration_runs.push(DecorationRun {
198                        len: run_len_within_line as u32,
199                        color: run.color,
200                        background_color: run.background_color,
201                        underline: run.underline,
202                        strikethrough: run.strikethrough,
203                    });
204                    true
205                };
206
207                let font_id = self.resolve_font(&run.font);
208                if let Some(font_run) = font_runs.last_mut()
209                    && font_id == font_run.font_id
210                    && !decoration_changed
211                {
212                    font_run.len += run_len_within_line;
213                } else {
214                    font_runs.push(FontRun {
215                        len: run_len_within_line,
216                        font_id,
217                    });
218                }
219
220                // Preserve the remainder of the run for the next line
221                run.len -= run_len_within_line;
222                if run.len == 0 {
223                    runs.next();
224                }
225                run_start += run_len_within_line;
226            }
227
228            let layout = self.text_system.layout_wrapped_line(
229                &line_text,
230                font_size,
231                &font_runs,
232                wrap_width,
233                max_wrap_lines.map(|max| max.saturating_sub(wrapped_lines)),
234            );
235            wrapped_lines += layout.wrap_boundaries.len();
236
237            lines.push(WrappedLine {
238                layout,
239                decoration_runs,
240                text: line_text,
241            });
242
243            // Skip `\n` character.
244            if let Some(run) = runs.peek_mut() {
245                run.len -= 1;
246                if run.len == 0 {
247                    runs.next();
248                }
249            }
250        };
251
252        let mut split_lines = text.split('\n');
253
254        // Special case single lines to prevent allocating a sharedstring
255        if let Some(first_line) = split_lines.next()
256            && let Some(second_line) = split_lines.next()
257        {
258            let mut line_start = 0;
259            process_line(
260                SharedString::new(first_line),
261                line_start,
262                line_start + first_line.len(),
263            );
264            line_start += first_line.len() + '\n'.len_utf8();
265            process_line(
266                SharedString::new(second_line),
267                line_start,
268                line_start + second_line.len(),
269            );
270            for line_text in split_lines {
271                line_start += line_text.len() + '\n'.len_utf8();
272                process_line(
273                    SharedString::new(line_text),
274                    line_start,
275                    line_start + line_text.len(),
276                );
277            }
278        } else {
279            let end = text.len();
280            process_line(text, 0, end);
281        }
282
283        self.recycle_font_runs(font_runs);
284
285        Ok(lines)
286    }
287
288    pub(crate) fn finish_frame(&self) {
289        self.text_system.finish_frame()
290    }
291
292    /// Layout the given line of text, at the given font_size.
293    /// Subsets of the line can be styled independently with the `runs` parameter.
294    /// Generally, you should prefer to use [`Self::shape_line`] instead, which
295    /// can be painted directly.
296    pub fn layout_line(
297        &self,
298        text: &str,
299        font_size: Pixels,
300        runs: &[TextRun],
301        force_width: Option<Pixels>,
302    ) -> Arc<LineLayout> {
303        let mut last_run = None::<&TextRun>;
304        let mut font_runs = self.take_font_runs();
305        font_runs.clear();
306
307        for run in runs.iter() {
308            let decoration_changed = if let Some(last_run) = last_run
309                && last_run.color == run.color
310                && last_run.underline == run.underline
311                && last_run.strikethrough == run.strikethrough
312            // we do not consider differing background color relevant, as it does not affect glyphs
313            // && last_run.background_color == run.background_color
314            {
315                false
316            } else {
317                last_run = Some(run);
318                true
319            };
320
321            let font_id = self.resolve_font(&run.font);
322            if let Some(font_run) = font_runs.last_mut()
323                && font_id == font_run.font_id
324                && !decoration_changed
325            {
326                font_run.len += run.len;
327            } else {
328                font_runs.push(FontRun {
329                    len: run.len,
330                    font_id,
331                });
332            }
333        }
334
335        let layout = self
336            .text_system
337            .layout_line(text, font_size, &font_runs, force_width);
338
339        self.recycle_font_runs(font_runs);
340
341        layout
342    }
343
344    /// Returns the shaped layout width of for the given character, in the given font and size.
345    pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
346        let mut buffer = [0; 4];
347        let buffer: &_ = ch.encode_utf8(&mut buffer);
348        self.text_system
349            .layout_line(
350                buffer,
351                font_size,
352                &[FontRun {
353                    len: buffer.len(),
354                    font_id,
355                }],
356                None,
357            )
358            .width
359    }
360
361    /// Returns the shaped layout width of an `em`.
362    pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels {
363        self.layout_width(font_id, font_size, 'm')
364    }
365
366    /// Probe the line layout cache using a caller-provided content hash, without allocating.
367    ///
368    /// Returns `Some(layout)` if the layout is already cached in either the current frame
369    /// or the previous frame. Returns `None` if it is not cached.
370    ///
371    /// Contract (caller enforced):
372    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
373    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
374    pub fn try_layout_line_by_hash(
375        &self,
376        text_hash: u64,
377        text_len: usize,
378        font_size: Pixels,
379        runs: &[TextRun],
380        force_width: Option<Pixels>,
381    ) -> Option<Arc<LineLayout>> {
382        let mut last_run = None::<&TextRun>;
383        let mut font_runs = self.take_font_runs();
384        font_runs.clear();
385
386        for run in runs.iter() {
387            let decoration_changed = if let Some(last_run) = last_run
388                && last_run.color == run.color
389                && last_run.underline == run.underline
390                && last_run.strikethrough == run.strikethrough
391            // we do not consider differing background color relevant, as it does not affect glyphs
392            // && last_run.background_color == run.background_color
393            {
394                false
395            } else {
396                last_run = Some(run);
397                true
398            };
399
400            let font_id = self.resolve_font(&run.font);
401            if let Some(font_run) = font_runs.last_mut()
402                && font_id == font_run.font_id
403                && !decoration_changed
404            {
405                font_run.len += run.len;
406            } else {
407                font_runs.push(FontRun {
408                    len: run.len,
409                    font_id,
410                });
411            }
412        }
413
414        let layout = self.text_system.try_layout_line_by_hash(
415            text_hash,
416            text_len,
417            font_size,
418            &font_runs,
419            force_width,
420        );
421
422        self.recycle_font_runs(font_runs);
423
424        layout
425    }
426
427    /// Layout the given line of text using a caller-provided content hash as the cache key.
428    ///
429    /// This enables cache hits without materializing a contiguous `SharedString` for the text.
430    /// If the cache misses, `materialize_text` is invoked to produce the `SharedString` for shaping.
431    ///
432    /// Contract (caller enforced):
433    /// - Same `text_hash` implies identical text content (collision risk accepted by caller).
434    /// - `text_len` should be the UTF-8 byte length of the text (helps reduce accidental collisions).
435    pub fn layout_line_by_hash(
436        &self,
437        text_hash: u64,
438        text_len: usize,
439        font_size: Pixels,
440        runs: &[TextRun],
441        force_width: Option<Pixels>,
442        materialize_text: impl FnOnce() -> SharedString + 'static,
443    ) -> Arc<LineLayout> {
444        let mut last_run = None::<&TextRun>;
445        let mut font_runs = self.take_font_runs();
446        font_runs.clear();
447
448        for run in runs.iter() {
449            let decoration_changed = if let Some(last_run) = last_run
450                && last_run.color == run.color
451                && last_run.underline == run.underline
452                && last_run.strikethrough == run.strikethrough
453            // we do not consider differing background color relevant, as it does not affect glyphs
454            // && last_run.background_color == run.background_color
455            {
456                false
457            } else {
458                last_run = Some(run);
459                true
460            };
461
462            let font_id = self.resolve_font(&run.font);
463            if let Some(font_run) = font_runs.last_mut()
464                && font_id == font_run.font_id
465                && !decoration_changed
466            {
467                font_run.len += run.len;
468            } else {
469                font_runs.push(FontRun {
470                    len: run.len,
471                    font_id,
472                });
473            }
474        }
475
476        let layout = self.text_system.layout_line_by_hash(
477            text_hash,
478            text_len,
479            font_size,
480            &font_runs,
481            force_width,
482            Box::new(materialize_text),
483        );
484
485        self.recycle_font_runs(font_runs);
486
487        layout
488    }
489}