diff --git a/book/src/generated/static-cmd.md b/book/src/generated/static-cmd.md index 1d831f26eb5f..22d80d1ff6b8 100644 --- a/book/src/generated/static-cmd.md +++ b/book/src/generated/static-cmd.md @@ -155,6 +155,10 @@ | `goto_prev_change` | Goto previous change | normal: `` [g ``, select: `` [g `` | | `goto_first_change` | Goto first change | normal: `` [G ``, select: `` [G `` | | `goto_last_change` | Goto last change | normal: `` ]G ``, select: `` ]G `` | +| `grow_view_width` | Grow focused container width | normal: `` rl ``, `` wrl ``, `` r ``, `` wr ``, select: `` rl ``, `` wrl ``, `` r ``, `` wr `` | +| `shrink_view_width` | Shrink focused container width | normal: `` rh ``, `` wrh ``, `` r ``, `` wr ``, select: `` rh ``, `` wrh ``, `` r ``, `` wr `` | +| `grow_view_height` | Grow focused container height | normal: `` rk ``, `` r ``, `` wrk ``, `` wr ``, select: `` rk ``, `` r ``, `` wrk ``, `` wr `` | +| `shrink_view_height` | Shrink focused container height | normal: `` rj ``, `` wrj ``, `` r ``, `` wr ``, select: `` rj ``, `` wrj ``, `` r ``, `` wr `` | | `goto_line_start` | Goto line start | normal: `` gh ``, `` ``, select: `` gh ``, insert: `` `` | | `goto_line_end` | Goto line end | normal: `` gl ``, `` ``, select: `` gl `` | | `goto_column` | Goto column | normal: `` g\| `` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9e8cae00c798..0011ff207d6a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -52,7 +52,7 @@ use helix_view::{ input::KeyEvent, keyboard::KeyCode, theme::Style, - tree, + tree::{self, Dimension, Resize}, view::View, Document, DocumentId, Editor, ViewId, }; @@ -458,6 +458,10 @@ impl MappableCommand { goto_prev_change, "Goto previous change", goto_first_change, "Goto first change", goto_last_change, "Goto last change", + grow_view_width, "Grow focused container width", + shrink_view_width, "Shrink focused container width", + grow_view_height, "Grow focused container height", + shrink_view_height, "Shrink focused container height", goto_line_start, "Goto line start", goto_line_end, "Goto line end", goto_column, "Goto column", @@ -906,6 +910,26 @@ fn goto_line_start(cx: &mut Context) { ) } +fn grow_view_width(cx: &mut Context) { + cx.editor + .resize_view(Resize::Grow, Dimension::Width, cx.count()); +} + +fn shrink_view_width(cx: &mut Context) { + cx.editor + .resize_view(Resize::Shrink, Dimension::Width, cx.count()); +} + +fn grow_view_height(cx: &mut Context) { + cx.editor + .resize_view(Resize::Grow, Dimension::Height, cx.count()); +} + +fn shrink_view_height(cx: &mut Context) { + cx.editor + .resize_view(Resize::Shrink, Dimension::Height, cx.count()); +} + fn goto_next_buffer(cx: &mut Context) { goto_buffer(cx.editor, Direction::Forward, cx.count()); } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 849f47c71ed0..4413bd4e6320 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -195,6 +195,12 @@ pub fn default() -> HashMap { "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "C-t" | "t" => transpose_view, + "r" => { "Window resize mode" sticky=true + "h" | "left" => shrink_view_width, + "l" | "right" => grow_view_width, + "j" | "down" => shrink_view_height, + "k" | "up" => grow_view_height, + }, "f" => goto_file_hsplit, "F" => goto_file_vsplit, "C-q" | "q" => wclose, @@ -262,6 +268,12 @@ pub fn default() -> HashMap { "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "C-t" | "t" => transpose_view, + "r" => { "Window resize mode" sticky=true + "h" | "left" => shrink_view_width, + "l" | "right" => grow_view_width, + "j" | "down" => shrink_view_height, + "k" | "up" => grow_view_height, + }, "f" => goto_file_hsplit, "F" => goto_file_vsplit, "C-q" | "q" => wclose, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 3e7956802c13..e0e536589eec 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -11,7 +11,7 @@ use crate::{ input::KeyEvent, register::Registers, theme::{self, Theme}, - tree::{self, Tree}, + tree::{self, Dimension, Resize, Tree}, Document, DocumentId, View, ViewId, }; use helix_event::dispatch; @@ -2236,6 +2236,10 @@ impl Editor { self.tree.transpose(); } + pub fn resize_view(&mut self, resize_type: Resize, dimension: Dimension, count: usize) { + self.tree.resize_view(resize_type, dimension, count); + } + pub fn should_close(&self) -> bool { self.tree.is_empty() } diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 2c1ddbd78551..04ea7e074cf3 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -1,5 +1,6 @@ use crate::{graphics::Rect, View, ViewId}; use slotmap::SlotMap; +use std::ops::{Index, IndexMut}; // the dimensions are recomputed on window resize/tree change. // @@ -29,6 +30,16 @@ pub enum Content { Container(Box), } +pub enum Resize { + Shrink, + Grow, +} + +pub enum Dimension { + Width, + Height, +} + impl Node { pub fn container(layout: Layout) -> Self { Self { @@ -63,15 +74,118 @@ pub enum Direction { #[derive(Debug)] pub struct Container { layout: Layout, - children: Vec, + children: Children, area: Rect, } +#[derive(Debug)] +struct Children { + pub views: Vec, + pub bounds: Vec, +} + +impl Children { + fn new() -> Self { + Children { + views: Vec::new(), + bounds: Vec::new(), + } + } + + pub fn iter(&self) -> std::slice::Iter<'_, ViewId> { + self.views.iter() + } + + pub fn len(&self) -> usize { + debug_assert_eq!(self.views.len(), self.bounds.len()); + self.views.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Children { + fn push(&mut self, node: ViewId) { + self.views.push(node); + + let portion = 1.0 / self.views.len() as f64; + + let prev = 1.0 - portion; + for bound in &mut self.bounds { + bound.portion *= prev; + } + + self.bounds.push(ContainerBounds { portion }); + } + + fn insert(&mut self, index: usize, node: ViewId) { + self.views.insert(index, node); + + let portion = 1.0 / self.views.len() as f64; + + let prev = 1.0 - portion; + for bound in &mut self.bounds { + bound.portion *= prev; + } + + self.bounds.insert(index, ContainerBounds { portion }); + } + + fn pop(&mut self) -> Option { + let bound = self.bounds.pop()?; + + let prev = 1.0 - bound.portion; + for bound in &mut self.bounds { + bound.portion /= prev; + } + + self.views.pop() + } + + fn remove(&mut self, index: usize) -> ViewId { + let bound = self.bounds.remove(index); + + let prev = 1.0 - bound.portion; + for bound in &mut self.bounds { + bound.portion /= prev; + } + + self.views.remove(index) + } +} + +impl Index for Children +where + Idx: std::slice::SliceIndex<[ViewId]>, +{ + type Output = Idx::Output; + fn index(&self, index: Idx) -> &Self::Output { + self.views.index(index) + } +} + +impl IndexMut for Children +where + Idx: std::slice::SliceIndex<[ViewId]>, +{ + #[inline(always)] + fn index_mut(&mut self, index: Idx) -> &mut Self::Output { + self.views.index_mut(index) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ContainerBounds { + portion: f64, +} + impl Container { pub fn new(layout: Layout) -> Self { Self { layout, - children: Vec::new(), + children: Children::new(), area: Rect::default(), } } @@ -383,11 +497,12 @@ impl Tree { Layout::Horizontal => { let len = container.children.len(); - let height = area.height / len as u16; - let mut child_y = area.y; for (i, child) in container.children.iter().enumerate() { + let bounds = container.children.bounds[i]; + let height = (area.height as f64 * bounds.portion).floor() as u16; + let mut area = Rect::new( container.area.x, child_y, @@ -396,7 +511,7 @@ impl Tree { ); child_y += height; - // last child takes the remaining width because we can get uneven + // last child takes the remaining height because we can get uneven // space from rounding if i == len - 1 { area.height = container.area.y + container.area.height - area.y; @@ -413,11 +528,13 @@ impl Tree { let total_gap = inner_gap * len_u16.saturating_sub(2); let used_area = area.width.saturating_sub(total_gap); - let width = used_area / len_u16; let mut child_x = area.x; for (i, child) in container.children.iter().enumerate() { + let bounds = container.children.bounds[i]; + let width = (used_area as f64 * bounds.portion).floor() as u16; + let mut area = Rect::new( child_x, container.area.y, @@ -475,7 +592,7 @@ impl Tree { // It's possible to move in the desired direction within // the parent container so an attempt is made to find the // correct child. - match self.find_child(id, &parent_container.children, direction) { + match self.find_child(id, &parent_container.children.views, direction) { // Child is found, search is ended Some(id) => Some(id), // A child is not found. This could be because of either two scenarios @@ -596,6 +713,85 @@ impl Tree { } } + fn find_container(&mut self, layout: Layout) -> Option<(&mut Container, usize)> { + let mut focus = self.focus; + let mut parent = self.nodes[focus].parent; + + let mut found = false; + while parent != focus { + let Content::Container(container) = &self.nodes[parent].content else { + unreachable!("parent should be a container"); + }; + + if container.layout == layout { + found = true; + break; + } + + focus = parent; + parent = self.nodes[focus].parent; + } + + if found { + let Content::Container(container) = &mut self.nodes[parent].content else { + unreachable!("parent should be a container"); + }; + + let idx = container.children.iter().position(|node| *node == focus)?; + Some((container, idx)) + } else { + None + } + } + + pub fn resize_view(&mut self, resize_type: Resize, dimension: Dimension, count: usize) { + let layout = match dimension { + Dimension::Width => Layout::Vertical, + Dimension::Height => Layout::Horizontal, + }; + + if let Some((container, idx)) = self.find_container(layout) { + if container.children.len() == 1 { + return; + } + + let diff = match dimension { + Dimension::Width => 1.0 / container.area.width as f64 * 2.0, + Dimension::Height => 1.0 / container.area.height as f64 * 2.0, + }; + let diff = match resize_type { + Resize::Shrink => -diff, + Resize::Grow => diff, + }; + let diff = diff * count as f64; + + let min = 0.05; + let max = 1.0 - ((container.children.len() - 1) as f64 * min); + + let bounds = &mut container.children.bounds; + if bounds[idx].portion <= min && bounds[idx].portion >= max { + return; + } + + let portion = bounds[idx].portion; + bounds[idx].portion = f64::clamp(portion + diff, min, max); + + let diff = bounds[idx].portion - portion; + if let Some(prev) = idx.checked_sub(1) { + if let Some(next) = bounds.get_mut(idx + 1) { + next.portion -= diff / 2.0; + bounds[prev].portion -= diff / 2.0; + } else { + bounds[prev].portion -= diff; + } + } else if let Some(next) = bounds.get_mut(idx + 1) { + next.portion -= diff; + } + + self.recalculate(); + } + } + pub fn swap_split_in_direction(&mut self, direction: Direction) -> Option<()> { let focus = self.focus; let target = self.find_split_in_direction(focus, direction)?;