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, TextRun, WindowTextSystem, WrapBoundary};
147    use rand::prelude::*;
148
149    #[test]
150    fn test_wrap_line() {
151        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
152        let cx = TestAppContext::new(dispatcher, None);
153
154        cx.update(|cx| {
155            let text_system = cx.text_system().clone();
156            let mut wrapper = LineWrapper::new(
157                text_system.font_id(&font("Courier")).unwrap(),
158                px(16.),
159                text_system.platform_text_system.clone(),
160            );
161            assert_eq!(
162                wrapper
163                    .wrap_line("aa bbb cccc ddddd eeee", px(72.))
164                    .collect::<Vec<_>>(),
165                &[
166                    Boundary::new(7, 0),
167                    Boundary::new(12, 0),
168                    Boundary::new(18, 0)
169                ],
170            );
171            assert_eq!(
172                wrapper
173                    .wrap_line("aaa aaaaaaaaaaaaaaaaaa", px(72.0))
174                    .collect::<Vec<_>>(),
175                &[
176                    Boundary::new(4, 0),
177                    Boundary::new(11, 0),
178                    Boundary::new(18, 0)
179                ],
180            );
181            assert_eq!(
182                wrapper
183                    .wrap_line("     aaaaaaa", px(72.))
184                    .collect::<Vec<_>>(),
185                &[
186                    Boundary::new(7, 5),
187                    Boundary::new(9, 5),
188                    Boundary::new(11, 5),
189                ]
190            );
191            assert_eq!(
192                wrapper
193                    .wrap_line("                            ", px(72.))
194                    .collect::<Vec<_>>(),
195                &[
196                    Boundary::new(7, 0),
197                    Boundary::new(14, 0),
198                    Boundary::new(21, 0)
199                ]
200            );
201            assert_eq!(
202                wrapper
203                    .wrap_line("          aaaaaaaaaaaaaa", px(72.))
204                    .collect::<Vec<_>>(),
205                &[
206                    Boundary::new(7, 0),
207                    Boundary::new(14, 3),
208                    Boundary::new(18, 3),
209                    Boundary::new(22, 3),
210                ]
211            );
212        });
213    }
214
215    // For compatibility with the test macro
216    use crate as gpui;
217
218    #[crate::test]
219    fn test_wrap_shaped_line(cx: &mut TestAppContext) {
220        cx.update(|cx| {
221            let text_system = WindowTextSystem::new(cx.text_system().clone());
222
223            let normal = TextRun {
224                len: 0,
225                font: font("Helvetica"),
226                color: Default::default(),
227                underline: Default::default(),
228                strikethrough: None,
229                background_color: None,
230            };
231            let bold = TextRun {
232                len: 0,
233                font: font("Helvetica").bold(),
234                color: Default::default(),
235                underline: Default::default(),
236                strikethrough: None,
237                background_color: None,
238            };
239
240            impl TextRun {
241                fn with_len(&self, len: usize) -> Self {
242                    let mut this = self.clone();
243                    this.len = len;
244                    this
245                }
246            }
247
248            let text = "aa bbb cccc ddddd eeee".into();
249            let lines = text_system
250                .shape_text(
251                    text,
252                    px(16.),
253                    &[
254                        normal.with_len(4),
255                        bold.with_len(5),
256                        normal.with_len(6),
257                        bold.with_len(1),
258                        normal.with_len(7),
259                    ],
260                    Some(px(72.)),
261                )
262                .unwrap();
263
264            assert_eq!(
265                lines[0].layout.wrap_boundaries(),
266                &[
267                    WrapBoundary {
268                        run_ix: 1,
269                        glyph_ix: 3
270                    },
271                    WrapBoundary {
272                        run_ix: 2,
273                        glyph_ix: 3
274                    },
275                    WrapBoundary {
276                        run_ix: 4,
277                        glyph_ix: 2
278                    }
279                ],
280            );
281        });
282    }
283}