line_wrapper.rs

  1use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem};
  2use collections::HashMap;
  3use std::{iter, sync::Arc};
  4
  5/// The GPUI line wrapper, used to wrap lines of text to a given width.
  6pub struct LineWrapper {
  7    platform_text_system: Arc<dyn PlatformTextSystem>,
  8    pub(crate) font_id: FontId,
  9    pub(crate) font_size: Pixels,
 10    cached_ascii_char_widths: [Option<Pixels>; 128],
 11    cached_other_char_widths: HashMap<char, Pixels>,
 12}
 13
 14impl LineWrapper {
 15    /// The maximum indent that can be applied to a line.
 16    pub const MAX_INDENT: u32 = 256;
 17
 18    pub(crate) fn new(
 19        font_id: FontId,
 20        font_size: Pixels,
 21        text_system: Arc<dyn PlatformTextSystem>,
 22    ) -> Self {
 23        Self {
 24            platform_text_system: text_system,
 25            font_id,
 26            font_size,
 27            cached_ascii_char_widths: [None; 128],
 28            cached_other_char_widths: HashMap::default(),
 29        }
 30    }
 31
 32    /// Wrap a line of text to the given width with this wrapper's font and font size.
 33    pub fn wrap_line<'a>(
 34        &'a mut self,
 35        line: &'a str,
 36        wrap_width: Pixels,
 37    ) -> impl Iterator<Item = Boundary> + 'a {
 38        let mut width = px(0.);
 39        let mut first_non_whitespace_ix = None;
 40        let mut indent = None;
 41        let mut last_candidate_ix = 0;
 42        let mut last_candidate_width = px(0.);
 43        let mut last_wrap_ix = 0;
 44        let mut prev_c = '\0';
 45        let mut char_indices = line.char_indices();
 46        iter::from_fn(move || {
 47            for (ix, c) in char_indices.by_ref() {
 48                if c == '\n' {
 49                    continue;
 50                }
 51
 52                if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
 53                    last_candidate_ix = ix;
 54                    last_candidate_width = width;
 55                }
 56
 57                if c != ' ' && first_non_whitespace_ix.is_none() {
 58                    first_non_whitespace_ix = Some(ix);
 59                }
 60
 61                let char_width = self.width_for_char(c);
 62                width += char_width;
 63                if width > wrap_width && ix > last_wrap_ix {
 64                    if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
 65                    {
 66                        indent = Some(
 67                            Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
 68                        );
 69                    }
 70
 71                    if last_candidate_ix > 0 {
 72                        last_wrap_ix = last_candidate_ix;
 73                        width -= last_candidate_width;
 74                        last_candidate_ix = 0;
 75                    } else {
 76                        last_wrap_ix = ix;
 77                        width = char_width;
 78                    }
 79
 80                    if let Some(indent) = indent {
 81                        width += self.width_for_char(' ') * indent as f32;
 82                    }
 83
 84                    return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
 85                }
 86                prev_c = c;
 87            }
 88
 89            None
 90        })
 91    }
 92
 93    #[inline(always)]
 94    fn width_for_char(&mut self, c: char) -> Pixels {
 95        if (c as u32) < 128 {
 96            if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
 97                cached_width
 98            } else {
 99                let width = self.compute_width_for_char(c);
100                self.cached_ascii_char_widths[c as usize] = Some(width);
101                width
102            }
103        } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
104            *cached_width
105        } else {
106            let width = self.compute_width_for_char(c);
107            self.cached_other_char_widths.insert(c, width);
108            width
109        }
110    }
111
112    fn compute_width_for_char(&self, c: char) -> Pixels {
113        let mut buffer = [0; 4];
114        let buffer = c.encode_utf8(&mut buffer);
115        self.platform_text_system
116            .layout_line(
117                buffer,
118                self.font_size,
119                &[FontRun {
120                    len: buffer.len(),
121                    font_id: self.font_id,
122                }],
123            )
124            .width
125    }
126}
127
128/// A boundary between two lines of text.
129#[derive(Copy, Clone, Debug, PartialEq, Eq)]
130pub struct Boundary {
131    /// The index of the last character in a line
132    pub ix: usize,
133    /// The indent of the next line.
134    pub next_indent: u32,
135}
136
137impl Boundary {
138    fn new(ix: usize, next_indent: u32) -> Self {
139        Self { ix, next_indent }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::{font, TestAppContext, TestDispatcher};
147    #[cfg(target_os = "macos")]
148    use crate::{TextRun, WindowTextSystem, WrapBoundary};
149    use rand::prelude::*;
150
151    #[test]
152    fn test_wrap_line() {
153        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
154        let cx = TestAppContext::new(dispatcher, None);
155        cx.text_system()
156            .add_fonts(vec![std::fs::read(
157                "../../assets/fonts/plex-mono/ZedPlexMono-Regular.ttf",
158            )
159            .unwrap()
160            .into()])
161            .unwrap();
162        let id = cx.text_system().font_id(&font("Zed Plex Mono")).unwrap();
163
164        cx.update(|cx| {
165            let text_system = cx.text_system().clone();
166            let mut wrapper =
167                LineWrapper::new(id, px(16.), text_system.platform_text_system.clone());
168            assert_eq!(
169                wrapper
170                    .wrap_line("aa bbb cccc ddddd eeee", px(72.))
171                    .collect::<Vec<_>>(),
172                &[
173                    Boundary::new(7, 0),
174                    Boundary::new(12, 0),
175                    Boundary::new(18, 0)
176                ],
177            );
178            assert_eq!(
179                wrapper
180                    .wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
181                    .collect::<Vec<_>>(),
182                &[
183                    Boundary::new(4, 0),
184                    Boundary::new(11, 0),
185                    Boundary::new(18, 0)
186                ],
187            );
188            assert_eq!(
189                wrapper
190                    .wrap_line("     aaaaaaa", px(72.))
191                    .collect::<Vec<_>>(),
192                &[
193                    Boundary::new(7, 5),
194                    Boundary::new(9, 5),
195                    Boundary::new(11, 5),
196                ]
197            );
198            assert_eq!(
199                wrapper
200                    .wrap_line("                            ", px(72.))
201                    .collect::<Vec<_>>(),
202                &[
203                    Boundary::new(7, 0),
204                    Boundary::new(14, 0),
205                    Boundary::new(21, 0)
206                ]
207            );
208            assert_eq!(
209                wrapper
210                    .wrap_line("          aaaaaaaaaaaaaa", px(72.))
211                    .collect::<Vec<_>>(),
212                &[
213                    Boundary::new(7, 0),
214                    Boundary::new(14, 3),
215                    Boundary::new(18, 3),
216                    Boundary::new(22, 3),
217                ]
218            );
219        });
220    }
221
222    // For compatibility with the test macro
223    #[cfg(target_os = "macos")]
224    use crate as gpui;
225
226    // These seem to vary wildly based on the the text system.
227    #[cfg(target_os = "macos")]
228    #[crate::test]
229    fn test_wrap_shaped_line(cx: &mut TestAppContext) {
230        cx.update(|cx| {
231            let text_system = WindowTextSystem::new(cx.text_system().clone());
232
233            let normal = TextRun {
234                len: 0,
235                font: font("Helvetica"),
236                color: Default::default(),
237                underline: Default::default(),
238                strikethrough: None,
239                background_color: None,
240            };
241            let bold = TextRun {
242                len: 0,
243                font: font("Helvetica").bold(),
244                color: Default::default(),
245                underline: Default::default(),
246                strikethrough: None,
247                background_color: None,
248            };
249
250            impl TextRun {
251                fn with_len(&self, len: usize) -> Self {
252                    let mut this = self.clone();
253                    this.len = len;
254                    this
255                }
256            }
257
258            let text = "aa bbb cccc ddddd eeee".into();
259            let lines = text_system
260                .shape_text(
261                    text,
262                    px(16.),
263                    &[
264                        normal.with_len(4),
265                        bold.with_len(5),
266                        normal.with_len(6),
267                        bold.with_len(1),
268                        normal.with_len(7),
269                    ],
270                    Some(px(72.)),
271                )
272                .unwrap();
273
274            assert_eq!(
275                lines[0].layout.wrap_boundaries(),
276                &[
277                    WrapBoundary {
278                        run_ix: 1,
279                        glyph_ix: 3
280                    },
281                    WrapBoundary {
282                        run_ix: 2,
283                        glyph_ix: 3
284                    },
285                    WrapBoundary {
286                        run_ix: 4,
287                        glyph_ix: 2
288                    }
289                ],
290            );
291        });
292    }
293}