line_wrapper.rs

  1use crate::Settings;
  2use gpui::{fonts::FontId, FontCache, FontSystem};
  3use parking_lot::Mutex;
  4use std::{collections::HashMap, sync::Arc};
  5
  6pub struct LineWrapper {
  7    font_system: Arc<dyn FontSystem>,
  8    font_id: FontId,
  9    font_size: f32,
 10    cached_ascii_char_widths: Mutex<[f32; 128]>,
 11    cached_other_char_widths: Mutex<HashMap<char, f32>>,
 12}
 13
 14impl LineWrapper {
 15    pub fn new(
 16        font_system: Arc<dyn FontSystem>,
 17        font_cache: &FontCache,
 18        settings: Settings,
 19    ) -> Self {
 20        let font_id = font_cache
 21            .select_font(settings.buffer_font_family, &Default::default())
 22            .unwrap();
 23        let font_size = settings.buffer_font_size;
 24        Self {
 25            font_system,
 26            font_id,
 27            font_size,
 28            cached_ascii_char_widths: Mutex::new([f32::NAN; 128]),
 29            cached_other_char_widths: Mutex::new(HashMap::new()),
 30        }
 31    }
 32
 33    #[cfg(test)]
 34    pub fn wrap_line_with_shaping(&self, line: &str, wrap_width: f32) -> Vec<usize> {
 35        self.font_system
 36            .wrap_line(line, self.font_id, self.font_size, wrap_width)
 37    }
 38
 39    pub fn wrap_line(&self, line: &str, wrap_width: f32) -> Vec<usize> {
 40        let mut width = 0.0;
 41        let mut boundaries = Vec::new();
 42        let mut last_boundary_ix = 0;
 43        let mut last_boundary_width = 0.0;
 44        let mut prev_c = '\0';
 45        for (ix, c) in line.char_indices() {
 46            if c == '\n' {
 47                break;
 48            }
 49
 50            if self.is_boundary(prev_c, c) {
 51                last_boundary_ix = ix;
 52                last_boundary_width = width;
 53            }
 54
 55            let char_width = self.width_for_char(c);
 56            width += char_width;
 57            if width > wrap_width && ix > *boundaries.last().unwrap_or(&0) {
 58                if last_boundary_ix > 0 {
 59                    boundaries.push(last_boundary_ix);
 60                    width -= last_boundary_width;
 61                    last_boundary_ix = 0;
 62                } else {
 63                    boundaries.push(ix);
 64                    width = char_width;
 65                }
 66            }
 67            prev_c = c;
 68        }
 69        boundaries
 70    }
 71
 72    fn is_boundary(&self, prev: char, next: char) -> bool {
 73        if prev == ' ' || next == ' ' {
 74            return true;
 75        }
 76        false
 77    }
 78
 79    fn width_for_char(&self, c: char) -> f32 {
 80        if (c as u32) < 128 {
 81            let mut cached_ascii_char_widths = self.cached_ascii_char_widths.lock();
 82            let mut width = cached_ascii_char_widths[c as usize];
 83            if width.is_nan() {
 84                width = self.compute_width_for_char(c);
 85                cached_ascii_char_widths[c as usize] = width;
 86            }
 87            width
 88        } else {
 89            let mut cached_other_char_widths = self.cached_other_char_widths.lock();
 90            let mut width = cached_other_char_widths
 91                .get(&c)
 92                .copied()
 93                .unwrap_or(f32::NAN);
 94            if width.is_nan() {
 95                width = self.compute_width_for_char(c);
 96                cached_other_char_widths.insert(c, width);
 97            }
 98            width
 99        }
100    }
101
102    fn compute_width_for_char(&self, c: char) -> f32 {
103        self.font_system
104            .layout_line(
105                &c.to_string(),
106                self.font_size,
107                &[(1, self.font_id, Default::default())],
108            )
109            .width
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[gpui::test]
118    fn test_line_wrapper(cx: &mut gpui::MutableAppContext) {
119        let font_cache = cx.font_cache().clone();
120        let font_system = cx.platform().fonts();
121        let settings = Settings {
122            tab_size: 4,
123            buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
124            buffer_font_size: 16.0,
125            ..Settings::new(&font_cache).unwrap()
126        };
127
128        let wrapper = LineWrapper::new(font_system, &font_cache, settings);
129
130        assert_eq!(
131            wrapper.wrap_line_with_shaping("aa bbb cccc ddddd eeee", 72.0),
132            &[7, 12, 18],
133        );
134        assert_eq!(
135            wrapper.wrap_line("aa bbb cccc ddddd eeee", 72.0),
136            &[7, 12, 18],
137        );
138
139        assert_eq!(
140            wrapper.wrap_line_with_shaping("aaa aaaaaaaaaaaaaaaaaa", 72.0),
141            &[4, 11, 18],
142        );
143        assert_eq!(
144            wrapper.wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0),
145            &[4, 11, 18],
146        );
147    }
148}