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}