1use crate::model::cursor::Cursors;
6use crate::model::event::BufferId;
7use ratatui::layout::Rect;
8
9#[derive(Debug, Clone)]
11pub struct CompositeViewState {
12 pub composite_id: BufferId,
14
15 pub pane_viewports: Vec<PaneViewport>,
17
18 pub focused_pane: usize,
20
21 pub scroll_row: usize,
24
25 pub cursor_row: usize,
27
28 pub cursor_column: usize,
30
31 pub sticky_column: usize,
34
35 pub pane_cursors: Vec<Cursors>,
37
38 pub pane_widths: Vec<u16>,
40
41 pub visual_mode: bool,
43
44 pub selection_anchor_row: usize,
46
47 pub selection_anchor_column: usize,
49}
50
51impl CompositeViewState {
52 pub fn new(composite_id: BufferId, pane_count: usize) -> Self {
54 Self {
55 composite_id,
56 pane_viewports: (0..pane_count).map(|_| PaneViewport::default()).collect(),
57 focused_pane: 0,
58 scroll_row: 0,
59 cursor_row: 0,
60 cursor_column: 0,
61 sticky_column: 0,
62 pane_cursors: (0..pane_count).map(|_| Cursors::new()).collect(),
63 pane_widths: vec![0; pane_count],
64 visual_mode: false,
65 selection_anchor_row: 0,
66 selection_anchor_column: 0,
67 }
68 }
69
70 pub fn start_visual_selection(&mut self) {
72 self.visual_mode = true;
73 self.selection_anchor_row = self.cursor_row;
74 self.selection_anchor_column = self.cursor_column;
75 }
76
77 pub fn clear_selection(&mut self) {
79 self.visual_mode = false;
80 }
81
82 pub fn selection_row_range(&self) -> Option<(usize, usize)> {
85 if !self.visual_mode {
86 return None;
87 }
88 let start = self.selection_anchor_row.min(self.cursor_row);
89 let end = self.selection_anchor_row.max(self.cursor_row);
90 Some((start, end))
91 }
92
93 pub fn is_row_selected(&self, row: usize) -> bool {
95 if !self.visual_mode {
96 return false;
97 }
98 let (start, end) = self.selection_row_range().unwrap();
99 row >= start && row <= end
100 }
101
102 pub fn selection_column_range(&self, row: usize) -> Option<(usize, usize)> {
106 if !self.visual_mode {
107 return None;
108 }
109
110 let (start_row, end_row) = self.selection_row_range()?;
111 if row < start_row || row > end_row {
112 return None;
113 }
114
115 let (sel_start_row, sel_start_col, sel_end_row, sel_end_col) = if self.selection_anchor_row
117 < self.cursor_row
118 || (self.selection_anchor_row == self.cursor_row
119 && self.selection_anchor_column <= self.cursor_column)
120 {
121 (
122 self.selection_anchor_row,
123 self.selection_anchor_column,
124 self.cursor_row,
125 self.cursor_column,
126 )
127 } else {
128 (
129 self.cursor_row,
130 self.cursor_column,
131 self.selection_anchor_row,
132 self.selection_anchor_column,
133 )
134 };
135
136 if sel_start_row == sel_end_row {
142 Some((sel_start_col, sel_end_col))
144 } else if row == sel_start_row {
145 Some((sel_start_col, usize::MAX))
147 } else if row == sel_end_row {
148 Some((0, sel_end_col))
150 } else {
151 Some((0, usize::MAX))
153 }
154 }
155
156 pub fn move_cursor_down(&mut self, max_row: usize, viewport_height: usize) {
159 const SCROLL_MARGIN: usize = 3;
160 if self.cursor_row < max_row {
161 self.cursor_row += 1;
162 let margin = SCROLL_MARGIN.min(viewport_height.saturating_sub(1) / 2);
163 if self.cursor_row + margin >= self.scroll_row + viewport_height {
164 self.scroll_row += 1;
165 }
166 }
167 }
168
169 pub fn move_cursor_up(&mut self, viewport_height: usize) {
172 const SCROLL_MARGIN: usize = 3;
173 if self.cursor_row > 0 {
174 self.cursor_row -= 1;
175 let margin = SCROLL_MARGIN.min(viewport_height.saturating_sub(1) / 2);
176 if self.cursor_row < self.scroll_row + margin && self.scroll_row > 0 {
177 self.scroll_row -= 1;
178 }
179 }
180 }
181
182 pub fn move_cursor_to_top(&mut self) {
184 self.cursor_row = 0;
185 self.scroll_row = 0;
186 }
187
188 pub fn move_cursor_to_bottom(&mut self, max_row: usize, viewport_height: usize) {
190 self.cursor_row = max_row;
191 self.scroll_row = max_row.saturating_sub(viewport_height.saturating_sub(1));
192 }
193
194 pub fn move_cursor_left(&mut self) {
196 if self.cursor_column > 0 {
197 self.cursor_column -= 1;
198 self.sticky_column = self.cursor_column;
199 let current_left = self
201 .pane_viewports
202 .get(self.focused_pane)
203 .map(|v| v.left_column)
204 .unwrap_or(0);
205 if self.cursor_column < current_left {
206 for viewport in &mut self.pane_viewports {
207 viewport.left_column = self.cursor_column;
208 }
209 }
210 }
211 }
212
213 pub fn move_cursor_right(&mut self, max_column: usize, pane_width: usize) {
215 if self.cursor_column < max_column {
216 self.cursor_column += 1;
217 self.sticky_column = self.cursor_column;
218 let visible_width = pane_width.saturating_sub(4); let current_left = self
221 .pane_viewports
222 .get(self.focused_pane)
223 .map(|v| v.left_column)
224 .unwrap_or(0);
225 if visible_width > 0 && self.cursor_column >= current_left + visible_width {
226 let new_left = self
227 .cursor_column
228 .saturating_sub(visible_width.saturating_sub(1));
229 for viewport in &mut self.pane_viewports {
230 viewport.left_column = new_left;
231 }
232 }
233 }
234 }
235
236 pub fn move_cursor_to_line_start(&mut self) {
238 self.cursor_column = 0;
239 self.sticky_column = 0;
240 for viewport in &mut self.pane_viewports {
242 viewport.left_column = 0;
243 }
244 }
245
246 pub fn move_cursor_to_line_end(&mut self, line_length: usize, pane_width: usize) {
248 self.cursor_column = line_length;
249 self.sticky_column = line_length;
250 let visible_width = pane_width.saturating_sub(4); let current_left = self
253 .pane_viewports
254 .get(self.focused_pane)
255 .map(|v| v.left_column)
256 .unwrap_or(0);
257 if visible_width > 0 && self.cursor_column >= current_left + visible_width {
258 let new_left = self
259 .cursor_column
260 .saturating_sub(visible_width.saturating_sub(1));
261 for viewport in &mut self.pane_viewports {
262 viewport.left_column = new_left;
263 }
264 }
265 }
266
267 pub fn clamp_cursor_to_line(&mut self, line_length: usize) {
270 self.cursor_column = self.sticky_column.min(line_length);
272 }
273
274 pub fn scroll(&mut self, delta: isize, max_row: usize) {
276 if delta >= 0 {
277 self.scroll_row = self.scroll_row.saturating_add(delta as usize).min(max_row);
278 } else {
279 self.scroll_row = self.scroll_row.saturating_sub(delta.unsigned_abs());
280 }
281 }
282
283 pub fn set_scroll_row(&mut self, row: usize, max_row: usize) {
285 self.scroll_row = row.min(max_row);
286 }
287
288 pub fn scroll_to_top(&mut self) {
290 self.scroll_row = 0;
291 }
292
293 pub fn scroll_to_bottom(&mut self, total_rows: usize, viewport_height: usize) {
295 self.scroll_row = total_rows.saturating_sub(viewport_height);
296 }
297
298 pub fn page_down(&mut self, viewport_height: usize, max_row: usize) {
300 self.scroll_row = self.scroll_row.saturating_add(viewport_height).min(max_row);
301 }
302
303 pub fn page_up(&mut self, viewport_height: usize) {
305 self.scroll_row = self.scroll_row.saturating_sub(viewport_height);
306 }
307
308 pub fn focus_next_pane(&mut self) {
310 if !self.pane_viewports.is_empty() {
311 self.focused_pane = (self.focused_pane + 1) % self.pane_viewports.len();
312 }
313 }
314
315 pub fn focus_prev_pane(&mut self) {
317 let count = self.pane_viewports.len();
318 if count > 0 {
319 self.focused_pane = (self.focused_pane + count - 1) % count;
320 }
321 }
322
323 pub fn set_focused_pane(&mut self, pane_index: usize) {
325 if pane_index < self.pane_viewports.len() {
326 self.focused_pane = pane_index;
327 }
328 }
329
330 pub fn get_pane_viewport(&self, pane_index: usize) -> Option<&PaneViewport> {
332 self.pane_viewports.get(pane_index)
333 }
334
335 pub fn get_pane_viewport_mut(&mut self, pane_index: usize) -> Option<&mut PaneViewport> {
337 self.pane_viewports.get_mut(pane_index)
338 }
339
340 pub fn get_pane_cursor(&self, pane_index: usize) -> Option<&Cursors> {
342 self.pane_cursors.get(pane_index)
343 }
344
345 pub fn get_pane_cursor_mut(&mut self, pane_index: usize) -> Option<&mut Cursors> {
347 self.pane_cursors.get_mut(pane_index)
348 }
349
350 pub fn focused_cursor(&self) -> Option<&Cursors> {
352 self.pane_cursors.get(self.focused_pane)
353 }
354
355 pub fn focused_cursor_mut(&mut self) -> Option<&mut Cursors> {
357 self.pane_cursors.get_mut(self.focused_pane)
358 }
359
360 pub fn update_pane_widths(&mut self, total_width: u16, ratios: &[f32], separator_width: u16) {
362 let separator_count = if self.pane_viewports.len() > 1 {
363 self.pane_viewports.len() - 1
364 } else {
365 0
366 };
367 let available_width = total_width.saturating_sub(separator_count as u16 * separator_width);
368
369 self.pane_widths.clear();
370 for ratio in ratios {
371 let width = (available_width as f32 * ratio).round() as u16;
372 self.pane_widths.push(width);
373 }
374
375 let total: u16 = self.pane_widths.iter().sum();
377 if total < available_width {
378 if let Some(last) = self.pane_widths.last_mut() {
379 *last += available_width - total;
380 }
381 } else if total > available_width {
382 if let Some(last) = self.pane_widths.last_mut() {
383 *last = last.saturating_sub(total - available_width);
384 }
385 }
386 }
387
388 pub fn compute_pane_rects(&self, area: Rect, separator_width: u16) -> Vec<Rect> {
390 let mut rects = Vec::with_capacity(self.pane_widths.len());
391 let mut x = area.x;
392
393 for (i, &width) in self.pane_widths.iter().enumerate() {
394 rects.push(Rect {
395 x,
396 y: area.y,
397 width,
398 height: area.height,
399 });
400 x += width;
401 if i < self.pane_widths.len() - 1 {
402 x += separator_width;
403 }
404 }
405
406 rects
407 }
408}
409
410#[derive(Debug, Clone, Default)]
412pub struct PaneViewport {
413 pub rect: Rect,
415 pub left_column: usize,
417}
418
419impl PaneViewport {
420 pub fn new() -> Self {
422 Self::default()
423 }
424
425 pub fn set_rect(&mut self, rect: Rect) {
427 self.rect = rect;
428 }
429
430 pub fn scroll_horizontal(&mut self, delta: isize, max_column: usize) {
432 if delta >= 0 {
433 self.left_column = self
434 .left_column
435 .saturating_add(delta as usize)
436 .min(max_column);
437 } else {
438 self.left_column = self.left_column.saturating_sub(delta.unsigned_abs());
439 }
440 }
441
442 pub fn reset_horizontal_scroll(&mut self) {
444 self.left_column = 0;
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_composite_view_scroll() {
454 let mut view = CompositeViewState::new(BufferId(1), 2);
455 assert_eq!(view.scroll_row, 0);
456
457 view.scroll(10, 100);
458 assert_eq!(view.scroll_row, 10);
459
460 view.scroll(-5, 100);
461 assert_eq!(view.scroll_row, 5);
462
463 view.scroll(-10, 100);
464 assert_eq!(view.scroll_row, 0); }
466
467 #[test]
468 fn test_composite_view_focus() {
469 let mut view = CompositeViewState::new(BufferId(1), 3);
470 assert_eq!(view.focused_pane, 0);
471
472 view.focus_next_pane();
473 assert_eq!(view.focused_pane, 1);
474
475 view.focus_next_pane();
476 assert_eq!(view.focused_pane, 2);
477
478 view.focus_next_pane();
479 assert_eq!(view.focused_pane, 0); view.focus_prev_pane();
482 assert_eq!(view.focused_pane, 2);
483 }
484
485 #[test]
486 fn test_pane_width_calculation() {
487 let mut view = CompositeViewState::new(BufferId(1), 2);
488 view.update_pane_widths(100, &[0.5, 0.5], 1);
489
490 assert_eq!(view.pane_widths.len(), 2);
491 assert!(view.pane_widths[0] + view.pane_widths[1] == 99);
493 }
494
495 #[test]
496 fn test_compute_pane_rects() {
497 let mut view = CompositeViewState::new(BufferId(1), 2);
498 view.update_pane_widths(101, &[0.5, 0.5], 1);
499
500 let area = Rect {
501 x: 0,
502 y: 0,
503 width: 101,
504 height: 50,
505 };
506 let rects = view.compute_pane_rects(area, 1);
507
508 assert_eq!(rects.len(), 2);
509 assert_eq!(rects[0].x, 0);
510 assert_eq!(rects[1].x, rects[0].width + 1); assert_eq!(rects[0].height, 50);
512 assert_eq!(rects[1].height, 50);
513 }
514}