use hjkl_layout::{Fixed, LayoutTree, SplitDir};
use super::window::{self, WindowId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockKind {
Explorer,
Quickfix,
Loclist,
}
#[derive(Debug, Clone)]
pub struct Dock {
pub win_id: WindowId,
pub kind: DockKind,
}
pub const DOCK_MIN_WIDTH: u16 = 12;
pub fn clamp_dock_width(width: u16, terminal_width: u16) -> u16 {
let max = (terminal_width / 2).max(DOCK_MIN_WIDTH);
width.clamp(DOCK_MIN_WIDTH, max)
}
pub const DOCK_MIN_HEIGHT: u16 = 3;
pub fn clamp_dock_height(height: u16, terminal_height: u16) -> u16 {
let max = (terminal_height / 2).max(DOCK_MIN_HEIGHT);
height.clamp(DOCK_MIN_HEIGHT, max)
}
fn resize_dock_leaf(node: &mut LayoutTree, id: WindowId, cells: u16) {
let LayoutTree::Split { fixed, a, b, .. } = node else {
return;
};
if matches!(a.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id) {
*fixed = Some(Fixed::First(cells));
return;
}
if matches!(b.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id) {
*fixed = Some(Fixed::Second(cells));
return;
}
resize_dock_leaf(a, id, cells);
resize_dock_leaf(b, id, cells);
}
impl super::App {
pub(crate) fn install_left_dock(&mut self, slot_idx: usize, kind: DockKind) -> WindowId {
let win_id = self.next_window_id;
self.next_window_id += 1;
self.windows.push(Some(window::Window::new(slot_idx)));
let cells = self.left_dock_cells();
let tree = self.layout_mut();
let rest = std::mem::replace(tree, LayoutTree::Leaf(win_id));
*tree = LayoutTree::split_fixed(
SplitDir::Vertical,
0.5,
Fixed::First(cells),
LayoutTree::Leaf(win_id),
rest,
);
self.tabs[self.active_tab].left_dock = Some(Dock { win_id, kind });
win_id
}
pub(crate) fn teardown_left_dock(&mut self) -> Option<Dock> {
let dock = self.tabs[self.active_tab].left_dock.take()?;
self.dispose_dock_window(&dock);
Some(dock)
}
pub(crate) fn install_bottom_dock(&mut self, slot_idx: usize, kind: DockKind) -> WindowId {
let win_id = self.next_window_id;
self.next_window_id += 1;
self.windows.push(Some(window::Window::new(slot_idx)));
let cells = self.bottom_dock_cells();
let explorer_win = self.tabs[self.active_tab]
.left_dock
.as_ref()
.map(|d| d.win_id);
let root = self.layout_mut();
let main = match root {
LayoutTree::Split { a, b, .. }
if explorer_win
.is_some_and(|e| matches!(a.as_ref(), LayoutTree::Leaf(l) if *l == e)) =>
{
b.as_mut()
}
other => other,
};
let rest = std::mem::replace(main, LayoutTree::Leaf(win_id));
*main = LayoutTree::split_fixed(
SplitDir::Horizontal,
0.5,
Fixed::Second(cells),
rest,
LayoutTree::Leaf(win_id),
);
self.tabs[self.active_tab].bottom_dock = Some(Dock { win_id, kind });
win_id
}
pub(crate) fn teardown_bottom_dock(&mut self) -> Option<Dock> {
let dock = self.tabs[self.active_tab].bottom_dock.take()?;
self.dispose_dock_window(&dock);
Some(dock)
}
fn dispose_dock_window(&mut self, dock: &Dock) {
let slot_idx = self
.windows
.get(dock.win_id)
.and_then(|w| w.as_ref())
.map(|w| w.slot);
if let Some(i) = (0..self.tabs.len()).find(|&i| self.tabs[i].layout.contains(dock.win_id)) {
if let Ok(new_focus) = self.tabs[i].layout.remove_leaf(dock.win_id) {
if self.tabs[i].focused_window == dock.win_id {
self.tabs[i].focused_window = new_focus;
}
} else if self.tabs[i].focused_window == dock.win_id {
let fallback = self.tabs[i].layout.leaves().into_iter().next().unwrap_or(0);
self.tabs[i].focused_window = fallback;
}
}
self.windows[dock.win_id] = None;
self.window_folds.remove(&dock.win_id);
self.window_editors.remove(&dock.win_id);
if let Some(slot_idx) = slot_idx
&& slot_idx < self.slots.len()
{
self.slots.remove(slot_idx);
self.reindex_after_slot_removal(slot_idx);
}
}
pub(crate) fn dispose_tab_docks(&mut self, tab_idx: usize) {
let left = self.tabs[tab_idx].left_dock.take();
let bottom = self.tabs[tab_idx].bottom_dock.take();
self.tabs[tab_idx].explorer = None;
if let Some(d) = left {
self.dispose_dock_window(&d);
}
if let Some(d) = bottom {
self.dispose_dock_window(&d);
}
}
pub(crate) fn close_bottom_dock(&mut self) {
let was_focused = self
.bottom_dock()
.is_some_and(|d| d.win_id == self.focused_window());
if self.teardown_bottom_dock().is_none() {
return;
}
if was_focused {
let target = self
.editor_target_window()
.or_else(|| self.layout().leaves().into_iter().next())
.unwrap_or_else(|| self.focused_window());
self.set_focused_window(target);
self.sync_viewport_to_editor();
}
}
pub(crate) fn reindex_after_slot_removal(&mut self, removed_idx: usize) {
let slot_count = self.slots.len();
for win in self.windows.iter_mut().flatten() {
if win.slot == removed_idx {
win.slot = 0;
} else if win.slot > removed_idx {
win.slot -= 1;
}
win.slot = win.slot.min(slot_count.saturating_sub(1));
}
self.prev_active = match self.prev_active {
Some(p) if p == removed_idx => None,
Some(p) if p > removed_idx => Some(p - 1),
other => other,
};
}
pub(crate) fn close_left_dock(&mut self) {
let Some(kind) = self.left_dock().map(|d| d.kind) else {
return;
};
match kind {
DockKind::Explorer => self.toggle_explorer(),
DockKind::Quickfix | DockKind::Loclist => {}
}
}
pub(crate) fn left_dock(&self) -> Option<&Dock> {
self.tabs[self.active_tab].left_dock.as_ref()
}
pub(crate) fn bottom_dock(&self) -> Option<&Dock> {
self.tabs[self.active_tab].bottom_dock.as_ref()
}
pub(crate) fn dock_pins(&self) -> Vec<WindowId> {
let tab = &self.tabs[self.active_tab];
tab.left_dock
.iter()
.chain(tab.bottom_dock.iter())
.map(|d| d.win_id)
.collect()
}
pub(crate) fn regular_leaf_count(&self) -> usize {
self.layout()
.leaves()
.into_iter()
.filter(|&id| !self.is_dock_window(id))
.count()
}
pub(crate) fn is_left_dock(&self, id: WindowId) -> bool {
self.left_dock().is_some_and(|d| d.win_id == id)
}
pub(crate) fn is_bottom_dock(&self, id: WindowId) -> bool {
self.bottom_dock().is_some_and(|d| d.win_id == id)
}
pub(crate) fn is_dock_window(&self, id: WindowId) -> bool {
self.is_left_dock(id) || self.is_bottom_dock(id)
}
pub(crate) fn slot_is_special(&self, idx: usize) -> bool {
self.slots
.get(idx)
.is_some_and(super::BufferSlot::is_special)
}
pub(crate) fn real_slot_count(&self) -> usize {
self.slots.iter().filter(|s| !s.is_special()).count()
}
fn left_dock_cells(&self) -> u16 {
let terminal_w = self.last_frame_rect.map_or(80, |r| r.width);
clamp_dock_width(self.config.explorer.width, terminal_w)
}
fn bottom_dock_cells(&self) -> u16 {
let terminal_h = self.last_frame_rect.map_or(24, |r| r.height);
clamp_dock_height(self.config.panel.height, terminal_h)
}
pub(crate) fn sync_dock_fixed_sizes(&mut self) {
let left_cells = self.left_dock_cells();
let bottom_cells = self.bottom_dock_cells();
for tab in &mut self.tabs {
if let Some(d) = tab.left_dock.as_ref() {
resize_dock_leaf(&mut tab.layout, d.win_id, left_cells);
}
if let Some(d) = tab.bottom_dock.as_ref() {
resize_dock_leaf(&mut tab.layout, d.win_id, bottom_cells);
}
}
}
pub(crate) fn resize_dock_width_by(&mut self, delta: i32) {
let terminal_w = self.last_frame_rect.map_or(80, |r| r.width);
let current = self.config.explorer.width as i32;
let candidate = (current + delta).clamp(0, u16::MAX as i32) as u16;
self.config.explorer.width = clamp_dock_width(candidate, terminal_w);
self.sync_dock_fixed_sizes();
}
pub(crate) fn persist_dock_width(&mut self) {
let Some(path) = self.config_path.clone() else {
return;
};
let width = self.config.explorer.width;
if let Err(e) = hjkl_config::write_key_at(&path, "explorer.width", width as i64) {
self.bus.warn(format!("couldn't save explorer width: {e}"));
}
}
pub(crate) fn resize_dock_height_by(&mut self, delta: i32) {
let terminal_h = self.last_frame_rect.map_or(24, |r| r.height);
let current = self.config.panel.height as i32;
let candidate = (current + delta).clamp(0, u16::MAX as i32) as u16;
self.config.panel.height = clamp_dock_height(candidate, terminal_h);
self.sync_dock_fixed_sizes();
}
pub(crate) fn persist_dock_height(&mut self) {
let Some(path) = self.config_path.clone() else {
return;
};
let height = self.config.panel.height;
if let Err(e) = hjkl_config::write_key_at(&path, "panel.height", height as i64) {
self.bus.warn(format!("couldn't save panel height: {e}"));
}
}
pub(crate) fn persist_explorer_open(&mut self) {
let Some(path) = self.config_path.clone() else {
return;
};
let open = self.tabs[self.active_tab].explorer.is_some();
if let Err(e) = hjkl_config::write_key_at(&path, "explorer.open", open) {
self.bus
.warn(format!("couldn't save explorer open state: {e}"));
}
}
pub(crate) fn restore_dock_state_from_config(&mut self) {
if self.config.explorer.open && self.tabs[self.active_tab].explorer.is_none() {
self.toggle_explorer();
if let Some(target) = self.editor_target_window() {
self.switch_focus(target);
}
}
}
}