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}