pub use hjkl_layout::{Axis, LayoutRect, LayoutTree, SplitDir, Tab, WindowId};
pub use hjkl_layout::Window as AppWindow;
pub use hjkl_layout::Window;
#[inline]
pub fn rect_to_layout(r: ratatui::layout::Rect) -> LayoutRect {
LayoutRect::new(r.x, r.y, r.width, r.height)
}
#[allow(dead_code)]
#[inline]
pub fn layout_to_rect(r: LayoutRect) -> ratatui::layout::Rect {
ratatui::layout::Rect {
x: r.x,
y: r.y,
width: r.w,
height: r.h,
}
}
use super::App;
impl App {
pub(crate) fn dispatch_window_action(
&mut self,
action: crate::keymap_actions::AppAction,
count: usize,
) {
use crate::keymap_actions::AppAction;
match action {
AppAction::FocusLeft => self.focus_left(),
AppAction::FocusBelow => self.focus_below(),
AppAction::FocusAbove => self.focus_above(),
AppAction::FocusRight => self.focus_right(),
AppAction::FocusNext => self.focus_next(),
AppAction::FocusPrev => self.focus_previous(),
AppAction::CloseFocusedWindow => self.close_focused_window(),
AppAction::OnlyFocusedWindow => self.only_focused_window(),
AppAction::SwapWithSibling => self.swap_with_sibling(),
AppAction::MoveWindowToNewTab => match self.move_window_to_new_tab() {
Ok(()) => {
self.bus.info("moved window to new tab");
}
Err(msg) => {
self.bus.error(msg.to_string());
}
},
AppAction::NewSplit => self.dispatch_ex("new"),
AppAction::ResizeHeight(delta) => self.resize_height(delta * count as i32),
AppAction::ResizeWidth(delta) => self.resize_width(delta * count as i32),
AppAction::EqualizeLayout => self.equalize_layout(),
AppAction::MaximizeHeight => self.maximize_height(),
AppAction::MaximizeWidth => self.maximize_width(),
AppAction::TmuxNavigate(dir) => self.dispatch_tmux_navigate(dir),
_ => {}
}
}
pub(crate) fn dispatch_tmux_navigate(&mut self, dir: super::NavDir) {
use super::NavDir;
let focused = self.focused_window();
let neighbour = match dir {
NavDir::Left => self.layout().neighbor_left(focused),
NavDir::Down => self.layout().neighbor_below(focused),
NavDir::Up => self.layout().neighbor_above(focused),
NavDir::Right => self.layout().neighbor_right(focused),
};
if neighbour.is_some() {
match dir {
NavDir::Left => self.focus_left(),
NavDir::Down => self.focus_below(),
NavDir::Up => self.focus_above(),
NavDir::Right => self.focus_right(),
}
} else if std::env::var("TMUX").is_ok() {
let flag = match dir {
NavDir::Left => "-L",
NavDir::Down => "-D",
NavDir::Up => "-U",
NavDir::Right => "-R",
};
let _ = std::process::Command::new("tmux")
.args(["select-pane", flag])
.status();
}
}
pub fn focus_below(&mut self) {
let fw = self.focused_window();
if let Some(target) = self.layout().neighbor_below(fw) {
self.switch_focus(target);
}
}
pub fn focus_above(&mut self) {
let fw = self.focused_window();
if let Some(target) = self.layout().neighbor_above(fw) {
self.switch_focus(target);
}
}
pub fn focus_left(&mut self) {
let fw = self.focused_window();
if let Some(target) = self.layout().neighbor_left(fw) {
self.switch_focus(target);
}
}
pub fn focus_right(&mut self) {
let fw = self.focused_window();
if let Some(target) = self.layout().neighbor_right(fw) {
self.switch_focus(target);
}
}
pub fn focus_next(&mut self) {
let fw = self.focused_window();
if let Some(target) = self.layout().next_leaf(fw) {
self.switch_focus(target);
}
}
pub fn focus_previous(&mut self) {
let fw = self.focused_window();
if let Some(target) = self.layout().prev_leaf(fw) {
self.switch_focus(target);
}
}
pub fn only_focused_window(&mut self) {
let focused = self.focused_window();
let all_leaves = self.layout().leaves();
for id in all_leaves {
if id != focused {
self.windows[id] = None;
}
}
*self.layout_mut() = LayoutTree::Leaf(focused);
self.bus.info("only");
}
pub fn swap_with_sibling(&mut self) {
let focused = self.focused_window();
if self.layout_mut().swap_with_sibling(focused) {
self.bus.info("swap");
}
}
pub fn move_window_to_new_tab(&mut self) -> Result<(), &'static str> {
let focused = self.focused_window();
if self.layout().leaves().len() <= 1 {
return Err("E1: only one window in this tab");
}
self.sync_viewport_from_editor();
let new_focus_in_old_tab = self
.layout_mut()
.remove_leaf(focused)
.map_err(|_| "remove_leaf failed")?;
self.tabs[self.active_tab].focused_window = new_focus_in_old_tab;
let new_tab = Tab::new(LayoutTree::Leaf(focused), focused);
self.tabs.push(new_tab);
self.active_tab = self.tabs.len() - 1;
self.sync_viewport_to_editor();
Ok(())
}
pub fn close_focused_window(&mut self) {
if self.is_cmdline_win_focused() {
self.close_cmdline_window();
return;
}
let focused = self.focused_window();
match self.layout_mut().remove_leaf(focused) {
Err(_) => {
self.bus.error("E444: Cannot close last window");
}
Ok(new_focus) => {
self.windows[focused] = None;
self.set_focused_window(new_focus);
self.sync_viewport_to_editor();
self.bus.info("window closed");
}
}
}
pub fn resize_height(&mut self, delta: i32) {
let fw = self.focused_window();
if let Some((ratio, Some(rect), in_a)) = self
.layout_mut()
.enclosing_split_mut(fw, SplitDir::Horizontal)
{
let parent_h = rect.h as i32;
if parent_h < 2 {
return;
}
let current_focused_height = if in_a {
(parent_h as f32 * *ratio) as i32
} else {
(parent_h as f32 * (1.0 - *ratio)) as i32
};
let new_focused = (current_focused_height + delta).clamp(1, parent_h - 1);
let new_ratio = if in_a {
new_focused as f32 / parent_h as f32
} else {
(parent_h - new_focused) as f32 / parent_h as f32
};
*ratio = new_ratio.clamp(0.01, 0.99);
}
}
pub fn resize_width(&mut self, delta: i32) {
let fw = self.focused_window();
if let Some((ratio, Some(rect), in_a)) = self
.layout_mut()
.enclosing_split_mut(fw, SplitDir::Vertical)
{
let parent_w = rect.w as i32;
if parent_w < 2 {
return;
}
let current_focused_width = if in_a {
(parent_w as f32 * *ratio) as i32
} else {
(parent_w as f32 * (1.0 - *ratio)) as i32
};
let new_focused = (current_focused_width + delta).clamp(1, parent_w - 1);
let new_ratio = if in_a {
new_focused as f32 / parent_w as f32
} else {
(parent_w - new_focused) as f32 / parent_w as f32
};
*ratio = new_ratio.clamp(0.01, 0.99);
}
}
pub fn equalize_layout(&mut self) {
self.layout_mut().equalize_all();
}
pub(crate) fn resize_split_to(
&mut self,
orientation: super::mouse::SplitOrientation,
split_origin: u16,
split_total: u16,
split_pos: u16,
) {
let min_size = match orientation {
super::mouse::SplitOrientation::Vertical => super::SPLIT_MIN_SIZE_COLS,
super::mouse::SplitOrientation::Horizontal => super::SPLIT_MIN_SIZE_ROWS,
};
if split_total < min_size * 2 + 1 {
return; }
let clamped = split_pos.clamp(min_size, split_total.saturating_sub(min_size + 1));
let new_ratio = clamped as f32 / split_total as f32;
let new_ratio = new_ratio.clamp(0.01, 0.99);
let dir = match orientation {
super::mouse::SplitOrientation::Vertical => SplitDir::Vertical,
super::mouse::SplitOrientation::Horizontal => SplitDir::Horizontal,
};
fn update_matching(
node: &mut LayoutTree,
dir: SplitDir,
origin: u16,
total: u16,
new_ratio: f32,
) {
if let LayoutTree::Split {
dir: my_dir,
ratio,
a,
b,
last_rect,
} = node
{
if *my_dir == dir
&& let Some(r) = last_rect
{
let (rect_origin, rect_total) = match dir.axis() {
Axis::Col => (r.x, r.w),
Axis::Row => (r.y, r.h),
};
if rect_origin == origin && rect_total == total {
*ratio = new_ratio;
return; }
}
update_matching(a, dir, origin, total, new_ratio);
update_matching(b, dir, origin, total, new_ratio);
}
}
update_matching(self.layout_mut(), dir, split_origin, split_total, new_ratio);
}
pub(crate) fn equalize_split(&mut self) {
self.equalize_layout();
}
pub fn maximize_height(&mut self) {
let focused = self.focused_window();
self.layout_mut()
.for_each_ancestor(focused, &mut |dir, ratio, in_a, rect| {
if dir != SplitDir::Horizontal {
return;
}
if let Some(r) = rect {
let h = r.h as f32;
if h < 2.0 {
return;
}
let max_branch = (h - 1.0) / h;
let min_branch = 1.0 / h;
*ratio = if in_a { max_branch } else { min_branch };
}
});
}
pub fn maximize_width(&mut self) {
let focused = self.focused_window();
self.layout_mut()
.for_each_ancestor(focused, &mut |dir, ratio, in_a, rect| {
if dir != SplitDir::Vertical {
return;
}
if let Some(r) = rect {
let w = r.w as f32;
if w < 2.0 {
return;
}
let max_branch = (w - 1.0) / w;
let min_branch = 1.0 / w;
*ratio = if in_a { max_branch } else { min_branch };
}
});
}
}