1use std::collections::BTreeMap;
10use std::ops::Range;
11
12use crate::editor::buffer::{BufferSnapshot, Version};
13use crate::editor::position::Position;
14
15#[derive(Debug, Clone)]
16pub struct LineLayout {
17 pub logical_line: usize,
18 pub visual_rows: Vec<VisualRow>,
19 pub version: Version,
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct VisualRow {
24 pub byte_range: Range<usize>,
25 pub char_range: Range<usize>,
26 pub width: usize,
27}
28
29impl VisualRow {
30 pub fn new(byte_start: usize, byte_end: usize, char_start: usize, char_end: usize) -> Self {
31 Self {
32 byte_range: byte_start..byte_end,
33 char_range: char_start..char_end,
34 width: char_end - char_start,
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
40pub struct ViewportLayout {
41 pub lines: Vec<ViewportLine>,
42 pub cursor_screen_pos: ScreenPosition,
43 pub total_visual_rows: usize,
44 pub version: Version,
45}
46
47#[derive(Debug, Clone)]
48pub struct ViewportLine {
49 pub logical_line: usize,
50 pub visual_rows: Vec<VisualRow>,
51 pub is_current_line: bool,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq)]
55pub struct ScreenPosition {
56 pub row: usize,
57 pub col: usize,
58}
59
60impl ScreenPosition {
61 pub fn new(row: usize, col: usize) -> Self {
62 Self { row, col }
63 }
64}
65
66#[derive(Debug, Default)]
67pub struct LayoutCache {
68 entries: BTreeMap<(Version, usize), LineLayout>,
69 max_entries: usize,
70}
71
72impl LayoutCache {
73 pub fn new() -> Self {
74 Self {
75 entries: BTreeMap::new(),
76 max_entries: 1000,
77 }
78 }
79
80 pub fn get(&self, version: Version, line: usize) -> Option<&LineLayout> {
81 self.entries.get(&(version, line))
82 }
83
84 pub fn insert(&mut self, version: Version, line: usize, layout: LineLayout) {
85 if self.entries.len() >= self.max_entries
86 && let Some(first_key) = self.entries.keys().next().copied() {
87 self.entries.remove(&first_key);
88 }
89 self.entries.insert((version, line), layout);
90 }
91
92 pub fn invalidate_for_version(&mut self, version: Version) {
93 self.entries.retain(|(v, _), _| *v != version);
94 }
95
96 pub fn clear(&mut self) {
97 self.entries.clear();
98 }
99}
100
101pub struct LayoutEngine {
102 cache: LayoutCache,
103 wrap_width: usize,
104 wrap_enabled: bool,
105}
106
107impl Default for LayoutEngine {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl LayoutEngine {
114 pub fn new() -> Self {
115 Self {
116 cache: LayoutCache::new(),
117 wrap_width: 80,
118 wrap_enabled: true,
119 }
120 }
121
122 pub fn with_wrap_width(mut self, width: usize) -> Self {
123 self.wrap_width = width;
124 self
125 }
126
127 pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
128 self.wrap_enabled = enabled;
129 self
130 }
131
132 pub fn set_wrap_width(&mut self, width: usize) {
133 if self.wrap_width != width {
134 self.wrap_width = width;
135 self.cache.clear();
136 }
137 }
138
139 pub fn set_wrap_enabled(&mut self, enabled: bool) {
140 if self.wrap_enabled != enabled {
141 self.wrap_enabled = enabled;
142 self.cache.clear();
143 }
144 }
145
146 pub fn wrap_width(&self) -> usize {
147 self.wrap_width
148 }
149
150 pub fn wrap_enabled(&self) -> bool {
151 self.wrap_enabled
152 }
153
154 pub fn invalidate_cache(&mut self, version: Version) {
155 self.cache.invalidate_for_version(version);
156 }
157
158 pub fn compute_line_layout(
159 &mut self,
160 line: &str,
161 logical_line: usize,
162 version: Version,
163 ) -> LineLayout {
164 if let Some(cached) = self.cache.get(version, logical_line) {
165 return cached.clone();
166 }
167
168 let visual_rows = if self.wrap_enabled && self.wrap_width > 0 {
169 self.compute_wrap_segments(line)
170 } else {
171 vec![VisualRow::new(0, line.len(), 0, line.chars().count())]
172 };
173
174 let layout = LineLayout {
175 logical_line,
176 visual_rows,
177 version,
178 };
179
180 self.cache.insert(version, logical_line, layout.clone());
181 layout
182 }
183
184 fn compute_wrap_segments(&self, line: &str) -> Vec<VisualRow> {
185 let mut rows = Vec::new();
186 let chars: Vec<char> = line.chars().collect();
187 let total_chars = chars.len();
188
189 if total_chars == 0 {
190 rows.push(VisualRow::new(0, 0, 0, 0));
191 return rows;
192 }
193
194 let mut char_pos = 0;
195 let mut byte_offset = 0;
196
197 while char_pos < total_chars {
198 let remaining = total_chars - char_pos;
199 let segment_width = remaining.min(self.wrap_width);
200
201 let segment_end = char_pos + segment_width;
202 let mut segment_byte_end = byte_offset;
203
204 for (i, ch) in chars[char_pos..].iter().enumerate() {
205 if i >= segment_width {
206 break;
207 }
208 segment_byte_end += ch.len_utf8();
209 }
210
211 rows.push(VisualRow::new(byte_offset, segment_byte_end, char_pos, segment_end));
212
213 char_pos = segment_end;
214 byte_offset = segment_byte_end;
215 }
216
217 if rows.is_empty() {
218 rows.push(VisualRow::new(0, 0, 0, 0));
219 }
220
221 rows
222 }
223
224 pub fn compute_viewport(
225 &mut self,
226 snapshot: &BufferSnapshot,
227 cursor: Position,
228 scroll: usize,
229 height: usize,
230 width: usize,
231 ) -> ViewportLayout {
232 let version = snapshot.version;
233
234 if self.wrap_width != width && self.wrap_enabled {
235 self.wrap_width = width;
236 self.cache.clear();
237 }
238
239 let mut visible_lines = Vec::new();
240 let mut visual_row = 0;
241 let mut cursor_screen_pos = ScreenPosition::new(0, 0);
242
243 let lines = snapshot.lines();
244 let total_lines = lines.len();
245 let mut buffer_line = scroll;
246
247 while visual_row < height && buffer_line < total_lines {
248 let line_text = &lines[buffer_line];
249 let layout = self.compute_line_layout(line_text, buffer_line, version);
250 let is_current_line = buffer_line == cursor.line;
251
252 if is_current_line && cursor_screen_pos.row == 0 {
253 cursor_screen_pos = self.map_cursor_to_screen(
254 cursor,
255 &layout,
256 visual_row,
257 );
258 }
259
260 for row in &layout.visual_rows {
261 if visual_row >= height {
262 break;
263 }
264 visible_lines.push(ViewportLine {
265 logical_line: buffer_line,
266 visual_rows: vec![row.clone()],
267 is_current_line,
268 });
269 visual_row += 1;
270 }
271
272 buffer_line += 1;
273 }
274
275 ViewportLayout {
276 lines: visible_lines,
277 cursor_screen_pos,
278 total_visual_rows: visual_row,
279 version,
280 }
281 }
282
283 fn map_cursor_to_screen(
284 &self,
285 cursor: Position,
286 layout: &LineLayout,
287 start_visual_row: usize,
288 ) -> ScreenPosition {
289 if layout.visual_rows.is_empty() {
290 return ScreenPosition::new(start_visual_row, 0);
291 }
292
293 for (row_idx, visual_row) in layout.visual_rows.iter().enumerate() {
294 if cursor.column >= visual_row.char_range.start
295 && cursor.column < visual_row.char_range.end
296 {
297 return ScreenPosition::new(
298 start_visual_row + row_idx,
299 cursor.column - visual_row.char_range.start,
300 );
301 }
302
303 if cursor.column >= visual_row.char_range.end && row_idx == layout.visual_rows.len() - 1 {
304 return ScreenPosition::new(
305 start_visual_row + row_idx,
306 visual_row.width,
307 );
308 }
309 }
310
311 let last_row = layout.visual_rows.last();
312 if let Some(row) = last_row {
313 if cursor.column >= row.char_range.end {
314 ScreenPosition::new(start_visual_row + layout.visual_rows.len() - 1, row.width)
315 } else {
316 ScreenPosition::new(start_visual_row, 0)
317 }
318 } else {
319 ScreenPosition::new(start_visual_row, 0)
320 }
321 }
322
323 pub fn compute_cursor_screen_position(
324 &mut self,
325 lines: &[String],
326 cursor: Position,
327 scroll: usize,
328 height: usize,
329 version: Version,
330 ) -> ScreenPosition {
331 let mut visual_row = 0;
332 let mut buffer_line = scroll;
333
334 while visual_row < height && buffer_line <= cursor.line {
335 let line_text = lines.get(buffer_line).map(|s| s.as_str()).unwrap_or("");
336 let layout = self.compute_line_layout(line_text, buffer_line, version);
337
338 for (row_idx, row) in layout.visual_rows.iter().enumerate() {
339 if visual_row + row_idx >= height {
340 return ScreenPosition::new(visual_row + row_idx, 0);
341 }
342
343 if buffer_line == cursor.line {
344 if cursor.column >= row.char_range.start
345 && cursor.column < row.char_range.end
346 {
347 return ScreenPosition::new(visual_row + row_idx, cursor.column - row.char_range.start);
348 }
349 if row_idx == layout.visual_rows.len() - 1
350 && cursor.column >= row.char_range.end
351 {
352 return ScreenPosition::new(visual_row + row_idx, row.width);
353 }
354 }
355
356 if buffer_line < cursor.line {
357 visual_row += 1;
358 }
359 }
360
361 if buffer_line < cursor.line {
362 visual_row += layout.visual_rows.len();
363 }
364
365 buffer_line += 1;
366 }
367
368 ScreenPosition::new(visual_row, cursor.column)
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn wrap_segments_basic() {
378 let engine = LayoutEngine::new().with_wrap_width(10);
379 let layout = engine.compute_wrap_segments("hello world foo bar");
380
381 assert!(layout.len() > 1);
382 let total_chars: usize = layout.iter().map(|r| r.width).sum();
383 assert_eq!(total_chars, 19);
384 }
385
386 #[test]
387 fn wrap_segments_short_line() {
388 let engine = LayoutEngine::new().with_wrap_width(80);
389 let layout = engine.compute_wrap_segments("hello");
390
391 assert_eq!(layout.len(), 1);
392 assert_eq!(layout[0].width, 5);
393 }
394
395 #[test]
396 fn wrap_segments_empty_line() {
397 let engine = LayoutEngine::new().with_wrap_width(80);
398 let layout = engine.compute_wrap_segments("");
399
400 assert_eq!(layout.len(), 1);
401 assert_eq!(layout[0].width, 0);
402 }
403
404 #[test]
405 fn cache_operations() {
406 let mut cache = LayoutCache::new();
407 let version = Version::new();
408
409 let layout = LineLayout {
410 logical_line: 0,
411 visual_rows: vec![VisualRow::new(0, 5, 0, 5)],
412 version,
413 };
414
415 cache.insert(version, 0, layout.clone());
416 assert!(cache.get(version, 0).is_some());
417
418 cache.invalidate_for_version(version);
419 assert!(cache.get(version, 0).is_none());
420 }
421}