Skip to main content

oo_ide/editor/
layout.rs

1//! Layout engine for computing visible lines and soft wrapping.
2//!
3//! This module handles:
4//! - Viewport computation from buffer snapshots
5//! - Soft wrapping (word wrap) with configurable width
6//! - Line-level layout caching for performance
7//! - Cursor position mapping across wrapped lines
8
9use 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}