use super::window::{self, WindowId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DockKind {
Explorer,
Quickfix,
Loclist,
}
#[derive(Debug, Clone)]
pub(crate) struct Dock {
pub win_id: WindowId,
pub slot_idx: usize,
pub kind: DockKind,
}
pub(crate) const DOCK_MIN_WIDTH: u16 = 12;
pub(crate) 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(crate) const DOCK_MIN_HEIGHT: u16 = 3;
pub(crate) 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)
}
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)));
self.left_dock = Some(Dock {
win_id,
slot_idx,
kind,
});
win_id
}
pub(crate) fn teardown_left_dock(&mut self) -> Option<Dock> {
let dock = self.left_dock.take()?;
let slot_idx = self
.windows
.get(dock.win_id)
.and_then(|w| w.as_ref())
.map(|w| w.slot)
.unwrap_or(dock.slot_idx);
self.windows[dock.win_id] = None;
self.window_folds.remove(&dock.win_id);
self.window_editors.remove(&dock.win_id);
if slot_idx < self.slots.len() {
self.slots.remove(slot_idx);
self.reindex_after_slot_removal(slot_idx);
}
for i in 0..self.tabs.len() {
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;
}
}
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)));
self.bottom_dock = Some(Dock {
win_id,
slot_idx,
kind,
});
win_id
}
pub(crate) fn teardown_bottom_dock(&mut self) -> Option<Dock> {
let dock = self.bottom_dock.take()?;
let slot_idx = self
.windows
.get(dock.win_id)
.and_then(|w| w.as_ref())
.map(|w| w.slot)
.unwrap_or(dock.slot_idx);
self.windows[dock.win_id] = None;
self.window_folds.remove(&dock.win_id);
self.window_editors.remove(&dock.win_id);
if slot_idx < self.slots.len() {
self.slots.remove(slot_idx);
self.reindex_after_slot_removal(slot_idx);
}
for i in 0..self.tabs.len() {
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;
}
}
Some(dock)
}
pub(crate) fn close_bottom_dock(&mut self) {
let was_focused = self
.bottom_dock
.as_ref()
.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();
}
}
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.as_ref().map(|d| d.kind) else {
return;
};
match kind {
DockKind::Explorer => self.toggle_explorer(),
DockKind::Quickfix | DockKind::Loclist => {}
}
}
pub(crate) fn is_left_dock(&self, id: WindowId) -> bool {
self.left_dock.as_ref().is_some_and(|d| d.win_id == id)
}
pub(crate) fn is_bottom_dock(&self, id: WindowId) -> bool {
self.bottom_dock.as_ref().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 qf_dock_slot_idx(&self) -> Option<usize> {
let win_id = self.bottom_dock.as_ref()?.win_id;
self.windows.get(win_id)?.as_ref().map(|w| w.slot)
}
pub(crate) fn slot_is_special(&self, idx: usize) -> bool {
self.slots.get(idx).is_some_and(|s| s.is_explorer) || self.qf_dock_slot_idx() == Some(idx)
}
pub(crate) fn dock_neighbor_left(&self, fw: WindowId) -> Option<WindowId> {
let dock = self.left_dock.as_ref()?;
if fw == dock.win_id || !self.layout().contains(fw) {
return None;
}
Some(dock.win_id)
}
pub(crate) fn dock_neighbor_right(&self, fw: WindowId) -> Option<WindowId> {
if !self.is_left_dock(fw) {
return None;
}
if let Some(last) = self.last_regular_window
&& self.layout().contains(last)
{
return Some(last);
}
self.layout().leaves().into_iter().next()
}
pub(crate) fn dock_neighbor_down(&self, fw: WindowId) -> Option<WindowId> {
let dock = self.bottom_dock.as_ref()?;
if fw == dock.win_id || !self.layout().contains(fw) {
return None;
}
Some(dock.win_id)
}
pub(crate) fn dock_neighbor_up(&self, fw: WindowId) -> Option<WindowId> {
if !self.is_bottom_dock(fw) {
return None;
}
if let Some(last) = self.last_regular_window
&& self.layout().contains(last)
{
return Some(last);
}
self.layout().leaves().into_iter().next()
}
pub(crate) fn focus_cycle_order(&self) -> Vec<WindowId> {
let mut order = Vec::new();
if let Some(d) = &self.left_dock {
order.push(d.win_id);
}
order.extend(self.layout().leaves());
if let Some(d) = &self.bottom_dock {
order.push(d.win_id);
}
order
}
pub(crate) fn resize_dock_width_by(&mut self, delta: i32) {
let terminal_w = self.last_frame_rect.map(|r| r.width).unwrap_or(80);
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);
}
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(|r| r.height).unwrap_or(24);
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);
}
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.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.explorer.is_none() {
self.toggle_explorer();
if let Some(target) = self.editor_target_window() {
self.switch_focus(target);
}
}
}
}