From 1b4268b078cc07b2d4bb7d4ad608c26988d813ee Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Wed, 12 Nov 2025 16:54:51 +0000 Subject: [PATCH] Added vertical pane support --- src/action.rs | 4 +- src/ui/input.rs | 17 ++-- src/ui/panes.rs | 207 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 184 insertions(+), 44 deletions(-) diff --git a/src/action.rs b/src/action.rs index 1ee95a1..8babbc2 100644 --- a/src/action.rs +++ b/src/action.rs @@ -7,7 +7,7 @@ use crossterm::event::{ }; use std::path::PathBuf; -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub enum Dir { Left, Right, @@ -65,7 +65,7 @@ pub enum Dist { Doc, } -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub enum MouseAction { Click, Drag, diff --git a/src/ui/input.rs b/src/ui/input.rs index 282ebc8..22d5ce8 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -20,6 +20,7 @@ pub struct Input { // x/y location in the buffer that the pane is trying to focus on pub focus: [isize; 2], // Remember the last area for things like scrolling + pub frame_area: Area, pub last_area: Area, pub last_scroll_pos: Option<([isize; 2], usize, usize)>, pub scroll_grab: Option<(usize, isize)>, @@ -147,6 +148,7 @@ impl Input { } Some(Action::Mouse(MouseAction::Click, pos, false, drag_id)) => { if let Some((scroll_pos, h, _)) = self.last_scroll_pos + && let Some(pos) = self.frame_area.contains(pos) && scroll_pos[0] == pos[0] && (scroll_pos[1]..=scroll_pos[1] + h as isize).contains(&pos[1]) { @@ -169,6 +171,7 @@ impl Input { if self.scroll_grab.map_or(false, |(di, _)| di == drag_id) => { if let Some((_, offset)) = self.scroll_grab + && let Some(pos) = self.frame_area.contains(pos) && let Some((_, scroll_sz, frame_sz)) = self.last_scroll_pos { self.focus[1] = ((pos[1] - offset).max(0) as usize @@ -181,11 +184,13 @@ impl Input { Action::Mouse(MouseAction::Drag, pos, false, _) | Action::Mouse(MouseAction::Click, pos, true, _), ) => { - buffer.goto_cursor( - cursor_id, - [self.focus[0] + pos[0], self.focus[1] + pos[1]], - false, - ); + if let Some(pos) = self.frame_area.contains(pos) { + buffer.goto_cursor( + cursor_id, + [self.focus[0] + pos[0], self.focus[1] + pos[1]], + false, + ); + } Ok(Resp::handled(None)) } Some(Action::Undo) => { @@ -250,6 +255,8 @@ impl Input { finder: Option<&Finder>, outer_frame: &mut Rect, ) { + self.frame_area = outer_frame.area(); + // Add frame let mut frame = if matches!(self.mode, Mode::SearchResult) { outer_frame.rect([0; 2], [!0; 2]) diff --git a/src/ui/panes.rs b/src/ui/panes.rs index bb658c6..5ed6e6c 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -11,45 +11,25 @@ pub struct Pane { last_area: Area, } -pub struct Panes { +pub struct VBox { selected: usize, panes: Vec, last_area: Area, } -impl Panes { - pub fn new(state: &mut State, buffers: &[BufferId]) -> Self { - Self { - selected: 0, - panes: buffers - .iter() - .map(|b| Pane { - kind: PaneKind::Doc(Doc::new(state, *b)), - last_area: Area::default(), - }) - .collect(), - last_area: Default::default(), - } - } - - pub fn selected_mut(&mut self) -> Option<&mut Pane> { - self.panes.get_mut(self.selected) - } -} - -impl Element for Panes { - fn handle(&mut self, state: &mut State, event: Event) -> Result { +impl Element<()> for VBox { + fn handle(&mut self, state: &mut State, event: Event) -> Result, Event> { match event.to_action(|e| { e.to_pane_move() .map(Action::PaneMove) .or_else(|| e.to_pane_open().map(Action::PaneOpen)) .or_else(|| e.to_pane_close()) }) { - Some(Action::PaneMove(Dir::Left)) => { + Some(Action::PaneMove(Dir::Up)) => { self.selected = (self.selected + self.panes.len() - 1) % self.panes.len(); Ok(Resp::handled(None)) } - Some(Action::PaneMove(Dir::Right)) => { + Some(Action::PaneMove(Dir::Down)) => { self.selected = (self.selected + 1) % self.panes.len(); Ok(Resp::handled(None)) } @@ -60,16 +40,20 @@ impl Element for Panes { PaneKind::Doc(doc) => doc.close(state), } self.selected = self.selected.clamp(0, self.panes.len().saturating_sub(1)); - Ok(Resp::handled(None)) + if self.panes.is_empty() { + Ok(Resp::end(None)) + } else { + Ok(Resp::handled(None)) + } } else { Err(event) } } Some(Action::PaneOpen(dir)) => { let new_idx = match dir { - Dir::Left => self.selected.saturating_sub(1).clamp(0, self.panes.len()), - Dir::Right => (self.selected + 1).min(self.panes.len()), - Dir::Up | Dir::Down => return Err(event), + Dir::Up => self.selected.clamp(0, self.panes.len()), + Dir::Down => (self.selected + 1).min(self.panes.len()), + Dir::Left | Dir::Right => return Err(event), }; let kind = match state.buffers.keys().next() { Some(b) => PaneKind::Doc(Doc::new(state, b)), @@ -85,16 +69,21 @@ impl Element for Panes { self.selected = new_idx; Ok(Resp::handled(None)) } - Some(ref action @ Action::Mouse(ref m_action, pos, _, _)) => { + Some(action @ Action::Mouse(m_action, pos, is_ctrl, drag_id)) => { for (i, pane) in self.panes.iter_mut().enumerate() { if pane.last_area.contains(pos).is_some() { if matches!(m_action, MouseAction::Click) { self.selected = i; } match &mut pane.kind { - PaneKind::Doc(doc) => return doc.handle(state, action.clone().into()), + PaneKind::Doc(doc) => { + return doc + .handle(state, action.clone().into()) + .map(Resp::into_can_end); + } PaneKind::Empty => {} } + break; } } Ok(Resp::handled(None)) @@ -105,7 +94,7 @@ impl Element for Panes { // Pass to pane match &mut pane.kind { PaneKind::Empty => Err(event), - PaneKind::Doc(doc) => doc.handle(state, event), + PaneKind::Doc(doc) => doc.handle(state, event).map(Resp::into_can_end), } } else { // No active pane, don't handle @@ -116,20 +105,20 @@ impl Element for Panes { } } -impl Visual for Panes { +impl Visual for VBox { fn render(&mut self, state: &State, frame: &mut Rect) { let n = self.panes.len(); - let frame_w = frame.size()[0]; - let boundary = |i| frame_w * i / n; + let frame_h = frame.size()[1]; + let boundary = |i| frame_h * i / n; self.last_area = frame.area(); for (i, pane) in self.panes.iter_mut().enumerate() { - let (x0, x1) = (boundary(i), boundary(i + 1)); + let (y0, y1) = (boundary(i), boundary(i + 1)); // Draw pane contents frame - .rect([x0, 0], [x1 - x0, frame.size()[1]]) + .rect([0, y0], [frame.size()[0], y1 - y0]) .with_focus(self.selected == i) .with(|frame| { pane.last_area = frame.area(); @@ -141,3 +130,147 @@ impl Visual for Panes { } } } + +pub struct Panes { + selected: usize, + vboxes: Vec, + last_area: Area, +} + +impl Panes { + pub fn new(state: &mut State, buffers: &[BufferId]) -> Self { + Self { + selected: 0, + vboxes: buffers + .iter() + .map(|b| VBox { + selected: 0, + panes: vec![Pane { + kind: PaneKind::Doc(Doc::new(state, *b)), + last_area: Area::default(), + }], + last_area: Area::default(), + }) + .collect(), + last_area: Default::default(), + } + } + + pub fn selected_mut(&mut self) -> Option<&mut Pane> { + let vbox = self.vboxes.get_mut(self.selected)?; + vbox.panes.get_mut(vbox.selected) + } + + pub fn selected_vbox_mut(&mut self) -> Option<&mut VBox> { + self.vboxes.get_mut(self.selected) + } +} + +impl Element for Panes { + fn handle(&mut self, state: &mut State, event: Event) -> Result { + match event.to_action(|e| { + e.to_pane_move() + .map(Action::PaneMove) + .or_else(|| e.to_pane_open().map(Action::PaneOpen)) + .or_else(|| e.to_pane_close()) + }) { + Some(Action::PaneMove(Dir::Left)) => { + self.selected = (self.selected + self.vboxes.len() - 1) % self.vboxes.len(); + Ok(Resp::handled(None)) + } + Some(Action::PaneMove(Dir::Right)) => { + self.selected = (self.selected + 1) % self.vboxes.len(); + Ok(Resp::handled(None)) + } + Some(Action::PaneOpen(dir @ (Dir::Left | Dir::Right))) => { + let new_idx = match dir { + Dir::Left => self.selected.clamp(0, self.vboxes.len()), + Dir::Right => (self.selected + 1).min(self.vboxes.len()), + _ => unreachable!(), + }; + let kind = match state.buffers.keys().next() { + Some(b) => PaneKind::Doc(Doc::new(state, b)), + None => PaneKind::Empty, + }; + self.vboxes.insert( + new_idx, + VBox { + selected: 0, + panes: vec![Pane { + kind, + last_area: Area::default(), + }], + last_area: Area::default(), + }, + ); + self.selected = new_idx; + Ok(Resp::handled(None)) + } + Some(action @ Action::Mouse(m_action, pos, is_ctrl, drag_id)) => { + for (i, vbox) in self.vboxes.iter_mut().enumerate() { + if vbox.last_area.contains(pos).is_some() { + if matches!(m_action, MouseAction::Click) { + self.selected = i; + } + let resp = vbox.handle(state, action.clone().into())?; + if resp.is_end() { + self.vboxes.remove(self.selected); + self.selected = self.selected.min(self.vboxes.len()).saturating_sub(1); + } + return Ok(Resp::handled(resp.event)); + } + } + Ok(Resp::handled(None)) + } + // Pass anything else through to the active pane + action => { + let mut to_handle = self.selected; + // Set selected vbox on mouse click + if let Some(Action::Mouse(ref m_action, pos, is_ctrl, drag_id)) = action { + for (i, vbox) in self.vboxes.iter_mut().enumerate() { + if vbox.last_area.contains(pos).is_some() { + if matches!(m_action, MouseAction::Click) { + self.selected = i; + } + to_handle = i; + break; + } + } + } + + if let Some(vbox) = self.vboxes.get_mut(to_handle) { + // Pass to vbox + let resp = vbox.handle(state, event)?; + if resp.is_end() { + self.vboxes.remove(self.selected); + self.selected = self.selected.min(self.vboxes.len().saturating_sub(1)); + } + Ok(Resp::handled(resp.event)) + } else { + // No active pane, don't handle + Err(event) + } + } + } + } +} + +impl Visual for Panes { + fn render(&mut self, state: &State, frame: &mut Rect) { + let n = self.vboxes.len(); + let frame_w = frame.size()[0]; + let boundary = |i| frame_w * i / n; + + self.last_area = frame.area(); + + for (i, vbox) in self.vboxes.iter_mut().enumerate() { + let (x0, x1) = (boundary(i), boundary(i + 1)); + + // Draw pane contents + frame + .rect([x0, 0], [x1 - x0, frame.size()[1]]) + .with_focus(self.selected == i) + .with(|frame| vbox.render(state, frame)); + } + } +}