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