use crate::model::cursor::Cursors;
use crate::model::event::BufferId;
use ratatui::layout::Rect;
#[derive(Debug, Clone)]
pub struct CompositeViewState {
pub composite_id: BufferId,
pub pane_viewports: Vec<PaneViewport>,
pub focused_pane: usize,
pub scroll_row: usize,
pub cursor_row: usize,
pub cursor_column: usize,
pub sticky_column: usize,
pub pane_cursors: Vec<Cursors>,
pub pane_widths: Vec<u16>,
pub visual_mode: bool,
pub selection_anchor_row: usize,
pub selection_anchor_column: usize,
}
impl CompositeViewState {
pub fn new(composite_id: BufferId, pane_count: usize) -> Self {
Self {
composite_id,
pane_viewports: (0..pane_count).map(|_| PaneViewport::default()).collect(),
focused_pane: 0,
scroll_row: 0,
cursor_row: 0,
cursor_column: 0,
sticky_column: 0,
pane_cursors: (0..pane_count).map(|_| Cursors::new()).collect(),
pane_widths: vec![0; pane_count],
visual_mode: false,
selection_anchor_row: 0,
selection_anchor_column: 0,
}
}
pub fn start_visual_selection(&mut self) {
self.visual_mode = true;
self.selection_anchor_row = self.cursor_row;
self.selection_anchor_column = self.cursor_column;
}
pub fn clear_selection(&mut self) {
self.visual_mode = false;
}
pub fn selection_row_range(&self) -> Option<(usize, usize)> {
if !self.visual_mode {
return None;
}
let start = self.selection_anchor_row.min(self.cursor_row);
let end = self.selection_anchor_row.max(self.cursor_row);
Some((start, end))
}
pub fn is_row_selected(&self, row: usize) -> bool {
if !self.visual_mode {
return false;
}
let (start, end) = self.selection_row_range().unwrap();
row >= start && row <= end
}
pub fn selection_column_range(&self, row: usize) -> Option<(usize, usize)> {
if !self.visual_mode {
return None;
}
let (start_row, end_row) = self.selection_row_range()?;
if row < start_row || row > end_row {
return None;
}
let (sel_start_row, sel_start_col, sel_end_row, sel_end_col) = if self.selection_anchor_row
< self.cursor_row
|| (self.selection_anchor_row == self.cursor_row
&& self.selection_anchor_column <= self.cursor_column)
{
(
self.selection_anchor_row,
self.selection_anchor_column,
self.cursor_row,
self.cursor_column,
)
} else {
(
self.cursor_row,
self.cursor_column,
self.selection_anchor_row,
self.selection_anchor_column,
)
};
if sel_start_row == sel_end_row {
Some((sel_start_col, sel_end_col))
} else if row == sel_start_row {
Some((sel_start_col, usize::MAX))
} else if row == sel_end_row {
Some((0, sel_end_col))
} else {
Some((0, usize::MAX))
}
}
pub fn move_cursor_down(&mut self, max_row: usize, viewport_height: usize) {
if self.cursor_row < max_row {
self.cursor_row += 1;
if self.cursor_row >= self.scroll_row + viewport_height {
self.scroll_row = self.cursor_row.saturating_sub(viewport_height - 1);
}
}
}
pub fn move_cursor_up(&mut self) {
if self.cursor_row > 0 {
self.cursor_row -= 1;
if self.cursor_row < self.scroll_row {
self.scroll_row = self.cursor_row;
}
}
}
pub fn move_cursor_to_top(&mut self) {
self.cursor_row = 0;
self.scroll_row = 0;
}
pub fn move_cursor_to_bottom(&mut self, max_row: usize, viewport_height: usize) {
self.cursor_row = max_row;
self.scroll_row = max_row.saturating_sub(viewport_height.saturating_sub(1));
}
pub fn move_cursor_left(&mut self) {
if self.cursor_column > 0 {
self.cursor_column -= 1;
self.sticky_column = self.cursor_column;
let current_left = self
.pane_viewports
.get(self.focused_pane)
.map(|v| v.left_column)
.unwrap_or(0);
if self.cursor_column < current_left {
for viewport in &mut self.pane_viewports {
viewport.left_column = self.cursor_column;
}
}
}
}
pub fn move_cursor_right(&mut self, max_column: usize, pane_width: usize) {
if self.cursor_column < max_column {
self.cursor_column += 1;
self.sticky_column = self.cursor_column;
let visible_width = pane_width.saturating_sub(4); let current_left = self
.pane_viewports
.get(self.focused_pane)
.map(|v| v.left_column)
.unwrap_or(0);
if visible_width > 0 && self.cursor_column >= current_left + visible_width {
let new_left = self
.cursor_column
.saturating_sub(visible_width.saturating_sub(1));
for viewport in &mut self.pane_viewports {
viewport.left_column = new_left;
}
}
}
}
pub fn move_cursor_to_line_start(&mut self) {
self.cursor_column = 0;
self.sticky_column = 0;
for viewport in &mut self.pane_viewports {
viewport.left_column = 0;
}
}
pub fn move_cursor_to_line_end(&mut self, line_length: usize, pane_width: usize) {
self.cursor_column = line_length;
self.sticky_column = line_length;
let visible_width = pane_width.saturating_sub(4); let current_left = self
.pane_viewports
.get(self.focused_pane)
.map(|v| v.left_column)
.unwrap_or(0);
if visible_width > 0 && self.cursor_column >= current_left + visible_width {
let new_left = self
.cursor_column
.saturating_sub(visible_width.saturating_sub(1));
for viewport in &mut self.pane_viewports {
viewport.left_column = new_left;
}
}
}
pub fn clamp_cursor_to_line(&mut self, line_length: usize) {
self.cursor_column = self.sticky_column.min(line_length);
}
pub fn scroll(&mut self, delta: isize, max_row: usize) {
if delta >= 0 {
self.scroll_row = self.scroll_row.saturating_add(delta as usize).min(max_row);
} else {
self.scroll_row = self.scroll_row.saturating_sub(delta.unsigned_abs());
}
}
pub fn set_scroll_row(&mut self, row: usize, max_row: usize) {
self.scroll_row = row.min(max_row);
}
pub fn scroll_to_top(&mut self) {
self.scroll_row = 0;
}
pub fn scroll_to_bottom(&mut self, total_rows: usize, viewport_height: usize) {
self.scroll_row = total_rows.saturating_sub(viewport_height);
}
pub fn page_down(&mut self, viewport_height: usize, max_row: usize) {
self.scroll_row = self.scroll_row.saturating_add(viewport_height).min(max_row);
}
pub fn page_up(&mut self, viewport_height: usize) {
self.scroll_row = self.scroll_row.saturating_sub(viewport_height);
}
pub fn focus_next_pane(&mut self) {
if !self.pane_viewports.is_empty() {
self.focused_pane = (self.focused_pane + 1) % self.pane_viewports.len();
}
}
pub fn focus_prev_pane(&mut self) {
let count = self.pane_viewports.len();
if count > 0 {
self.focused_pane = (self.focused_pane + count - 1) % count;
}
}
pub fn set_focused_pane(&mut self, pane_index: usize) {
if pane_index < self.pane_viewports.len() {
self.focused_pane = pane_index;
}
}
pub fn get_pane_viewport(&self, pane_index: usize) -> Option<&PaneViewport> {
self.pane_viewports.get(pane_index)
}
pub fn get_pane_viewport_mut(&mut self, pane_index: usize) -> Option<&mut PaneViewport> {
self.pane_viewports.get_mut(pane_index)
}
pub fn get_pane_cursor(&self, pane_index: usize) -> Option<&Cursors> {
self.pane_cursors.get(pane_index)
}
pub fn get_pane_cursor_mut(&mut self, pane_index: usize) -> Option<&mut Cursors> {
self.pane_cursors.get_mut(pane_index)
}
pub fn focused_cursor(&self) -> Option<&Cursors> {
self.pane_cursors.get(self.focused_pane)
}
pub fn focused_cursor_mut(&mut self) -> Option<&mut Cursors> {
self.pane_cursors.get_mut(self.focused_pane)
}
pub fn update_pane_widths(&mut self, total_width: u16, ratios: &[f32], separator_width: u16) {
let separator_count = if self.pane_viewports.len() > 1 {
self.pane_viewports.len() - 1
} else {
0
};
let available_width = total_width.saturating_sub(separator_count as u16 * separator_width);
self.pane_widths.clear();
for ratio in ratios {
let width = (available_width as f32 * ratio).round() as u16;
self.pane_widths.push(width);
}
let total: u16 = self.pane_widths.iter().sum();
if total < available_width {
if let Some(last) = self.pane_widths.last_mut() {
*last += available_width - total;
}
} else if total > available_width {
if let Some(last) = self.pane_widths.last_mut() {
*last = last.saturating_sub(total - available_width);
}
}
}
pub fn compute_pane_rects(&self, area: Rect, separator_width: u16) -> Vec<Rect> {
let mut rects = Vec::with_capacity(self.pane_widths.len());
let mut x = area.x;
for (i, &width) in self.pane_widths.iter().enumerate() {
rects.push(Rect {
x,
y: area.y,
width,
height: area.height,
});
x += width;
if i < self.pane_widths.len() - 1 {
x += separator_width;
}
}
rects
}
}
#[derive(Debug, Clone, Default)]
pub struct PaneViewport {
pub rect: Rect,
pub left_column: usize,
}
impl PaneViewport {
pub fn new() -> Self {
Self::default()
}
pub fn set_rect(&mut self, rect: Rect) {
self.rect = rect;
}
pub fn scroll_horizontal(&mut self, delta: isize, max_column: usize) {
if delta >= 0 {
self.left_column = self
.left_column
.saturating_add(delta as usize)
.min(max_column);
} else {
self.left_column = self.left_column.saturating_sub(delta.unsigned_abs());
}
}
pub fn reset_horizontal_scroll(&mut self) {
self.left_column = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_composite_view_scroll() {
let mut view = CompositeViewState::new(BufferId(1), 2);
assert_eq!(view.scroll_row, 0);
view.scroll(10, 100);
assert_eq!(view.scroll_row, 10);
view.scroll(-5, 100);
assert_eq!(view.scroll_row, 5);
view.scroll(-10, 100);
assert_eq!(view.scroll_row, 0); }
#[test]
fn test_composite_view_focus() {
let mut view = CompositeViewState::new(BufferId(1), 3);
assert_eq!(view.focused_pane, 0);
view.focus_next_pane();
assert_eq!(view.focused_pane, 1);
view.focus_next_pane();
assert_eq!(view.focused_pane, 2);
view.focus_next_pane();
assert_eq!(view.focused_pane, 0);
view.focus_prev_pane();
assert_eq!(view.focused_pane, 2);
}
#[test]
fn test_pane_width_calculation() {
let mut view = CompositeViewState::new(BufferId(1), 2);
view.update_pane_widths(100, &[0.5, 0.5], 1);
assert_eq!(view.pane_widths.len(), 2);
assert!(view.pane_widths[0] + view.pane_widths[1] == 99);
}
#[test]
fn test_compute_pane_rects() {
let mut view = CompositeViewState::new(BufferId(1), 2);
view.update_pane_widths(101, &[0.5, 0.5], 1);
let area = Rect {
x: 0,
y: 0,
width: 101,
height: 50,
};
let rects = view.compute_pane_rects(area, 1);
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].x, 0);
assert_eq!(rects[1].x, rects[0].width + 1); assert_eq!(rects[0].height, 50);
assert_eq!(rects[1].height, 50);
}
}