use std::collections::HashMap;
use crate::buffer_ref::BufferRef;
use crate::editor::Buffer;
use super::{App, Toast};
pub type PaneId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitDir {
Vertical,
Horizontal,
}
pub use crate::action::FocusDir;
#[derive(Debug, Clone)]
pub enum PaneLayout {
Leaf(PaneId),
Split {
dir: SplitDir,
children: Vec<PaneLayout>,
ratios: Vec<f32>,
},
}
impl PaneLayout {
pub fn find_leaf_mut(&mut self, id: PaneId) -> Option<&mut PaneLayout> {
match self {
PaneLayout::Leaf(pid) if *pid == id => Some(self),
PaneLayout::Leaf(_) => None,
PaneLayout::Split { children, .. } => {
for c in children {
if let Some(found) = c.find_leaf_mut(id) {
return Some(found);
}
}
None
}
}
}
pub fn leaves(&self) -> Vec<PaneId> {
let mut out = Vec::new();
self.collect_leaves(&mut out);
out
}
fn collect_leaves(&self, out: &mut Vec<PaneId>) {
match self {
PaneLayout::Leaf(id) => out.push(*id),
PaneLayout::Split { children, .. } => {
for c in children {
c.collect_leaves(out);
}
}
}
}
pub fn remove_leaf(&mut self, target: PaneId) -> Option<PaneId> {
enum RemoveResult {
NotFound,
RemoveSelf,
Removed(Option<PaneId>),
}
fn rightmost_leaf(node: &PaneLayout) -> PaneId {
match node {
PaneLayout::Leaf(id) => *id,
PaneLayout::Split { children, .. } => {
rightmost_leaf(children.last().expect("split has >= 1 child"))
}
}
}
fn walk(node: &mut PaneLayout, target: PaneId) -> RemoveResult {
match node {
PaneLayout::Leaf(id) if *id == target => RemoveResult::RemoveSelf,
PaneLayout::Leaf(_) => RemoveResult::NotFound,
PaneLayout::Split {
children, ratios, ..
} => {
for i in 0..children.len() {
match walk(&mut children[i], target) {
RemoveResult::NotFound => continue,
RemoveResult::RemoveSelf => {
children.remove(i);
ratios.remove(i);
let sum: f32 = ratios.iter().sum();
if sum > 0.0 {
for r in ratios.iter_mut() {
*r /= sum;
}
}
let neighbor = if children.is_empty() {
None
} else {
let pick = if i < children.len() { i } else { i - 1 };
Some(rightmost_leaf(&children[pick]))
};
return RemoveResult::Removed(neighbor);
}
RemoveResult::Removed(n) => return RemoveResult::Removed(n),
}
}
RemoveResult::NotFound
}
}
}
let neighbor = match walk(self, target) {
RemoveResult::Removed(n) => n,
_ => return None,
};
collapse_singletons(self);
neighbor
}
pub fn split_at(&mut self, dir: SplitDir, new_id: PaneId, place: SplitPlace) {
let existing = std::mem::replace(self, PaneLayout::Leaf(0));
let new = PaneLayout::Leaf(new_id);
let (children, ratios) = match place {
SplitPlace::After => (vec![existing, new], vec![0.5, 0.5]),
SplitPlace::Before => (vec![new, existing], vec![0.5, 0.5]),
};
*self = PaneLayout::Split {
dir,
children,
ratios,
};
}
}
#[derive(Debug, Clone, Copy)]
pub enum SplitPlace {
After,
#[allow(dead_code)]
Before,
}
fn collapse_singletons(node: &mut PaneLayout) {
loop {
let collapsed = match node {
PaneLayout::Leaf(_) => None,
PaneLayout::Split { children, .. } if children.len() == 1 => Some(children.remove(0)),
PaneLayout::Split { children, .. } => {
for c in children.iter_mut() {
collapse_singletons(c);
}
None
}
};
match collapsed {
None => break,
Some(replacement) => {
*node = replacement;
}
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PaneRect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
pub type PaneRectMap = HashMap<PaneId, PaneRect>;
pub const INITIAL_PANE_ID: PaneId = 0;
pub const NEXT_PANE_ID_SEED: PaneId = 1;
impl App {
pub fn split_window(&mut self, dir: SplitDir) {
let new_pane_id = self.mint_pane_id();
let active_pane_id = self.active_pane;
let shared_ref = self.active_ref();
self.pane_refs.insert(active_pane_id, shared_ref);
let leaf = self
.layout
.find_leaf_mut(active_pane_id)
.expect("active pane must be in the layout tree");
leaf.split_at(dir, new_pane_id, SplitPlace::After);
self.active_pane = new_pane_id;
self.push_toast(Toast::info(format!(
"split ({})",
match dir {
SplitDir::Vertical => "vertical",
SplitDir::Horizontal => "horizontal",
},
)));
}
pub fn buffer_for_pane(&self, id: PaneId) -> Option<&Buffer> {
if id == self.active_pane {
return Some(&self.buffer);
}
let pane_ref = self.pane_refs.get(&id)?;
if *pane_ref == self.active_ref() {
return Some(&self.buffer);
}
self.parked_buffers.get(pane_ref)
}
pub fn ref_used_by_inactive_pane(&self, r: &BufferRef) -> bool {
self.pane_refs.values().any(|v| v == r)
}
pub fn close_window(&mut self) {
if self.pane_count() <= 1 {
self.push_toast(Toast::error("only one pane (use :q to quit)"));
return;
}
let closing_id = self.active_pane;
let neighbor = match self.layout.remove_leaf(closing_id) {
Some(n) => n,
None => {
self.push_toast(Toast::error("layout has no neighbour to close into"));
return;
}
};
let neighbour_ref = self
.pane_refs
.remove(&neighbor)
.expect("neighbour leaf must have a buffer_ref entry");
let closing_ref = self.active_ref();
if neighbour_ref == closing_ref {
self.active_pane = neighbor;
self.push_toast(Toast::info("pane closed"));
return;
}
let neighbour_buf = self
.parked_buffers
.remove(&neighbour_ref)
.expect("neighbour buffer must be parked");
let mut closed_buffer = std::mem::replace(&mut self.buffer, neighbour_buf);
self.current_scratch_id = match &neighbour_ref {
BufferRef::Scratch(id) => Some(*id),
_ => None,
};
if self.ref_used_by_inactive_pane(&closing_ref) {
self.parked_buffers.insert(closing_ref, closed_buffer);
} else {
closed_buffer.highlighter = None;
self.sleeping
.insert(closing_ref, super::SleepingBuffer::freeze(closed_buffer));
}
self.active_pane = neighbor;
self.lsp.detach_current();
self.lsp.set_last_synced_version(self.buffer.version);
if let Some(path) = self.buffer.path.clone() {
if self.buffer.highlighter.is_none() {
self.spawn_highlighter_worker(&path);
}
self.spawn_lsp_worker(&path);
}
self.push_toast(Toast::info("pane closed"));
}
pub fn focus_window(&mut self, dir: FocusDir) {
let Some(target) = self.pane_in_direction(dir) else {
return;
};
self.focus_pane(target);
}
pub fn cycle_window(&mut self) {
let leaves = self.layout.leaves();
if leaves.len() <= 1 {
return;
}
let idx = leaves
.iter()
.position(|id| *id == self.active_pane)
.unwrap_or(0);
let next = leaves[(idx + 1) % leaves.len()];
self.focus_pane(next);
}
pub fn pane_count(&self) -> usize {
self.layout.leaves().len()
}
pub(super) fn focus_pane(&mut self, target: PaneId) {
if target == self.active_pane {
return;
}
let target_ref = match self.pane_refs.get(&target).cloned() {
Some(r) => r,
None => return,
};
let prev_id = self.active_pane;
let prev_ref = self.active_ref();
if target_ref == prev_ref {
self.pane_refs.remove(&target);
self.pane_refs.insert(prev_id, prev_ref);
self.active_pane = target;
self.record_opened(target_ref);
return;
}
self.pane_refs.remove(&target);
let Some(target_buffer) = self.parked_buffers.remove(&target_ref) else {
self.pane_refs.insert(target, target_ref);
return;
};
let prev_buffer = std::mem::replace(&mut self.buffer, target_buffer);
self.current_scratch_id = match &target_ref {
BufferRef::Scratch(id) => Some(*id),
_ => None,
};
self.parked_buffers.insert(prev_ref.clone(), prev_buffer);
self.pane_refs.insert(prev_id, prev_ref);
self.active_pane = target;
self.lsp.detach_current();
self.lsp.set_last_synced_version(self.buffer.version);
if let Some(path) = self.buffer.path.clone() {
if self.buffer.highlighter.is_none() {
self.spawn_highlighter_worker(&path);
}
self.spawn_lsp_worker(&path);
}
self.record_opened(target_ref);
}
fn pane_in_direction(&self, dir: FocusDir) -> Option<PaneId> {
let rects = self.last_pane_rects.borrow();
let active = rects.get(&self.active_pane).copied()?;
let active_cx = active.x + active.width / 2;
let active_cy = active.y + active.height / 2;
let mut best: Option<(PaneId, i32)> = None;
for (&id, &rect) in rects.iter() {
if id == self.active_pane {
continue;
}
let matches_dir = match dir {
FocusDir::Left => rect.x + rect.width <= active.x,
FocusDir::Right => rect.x >= active.x + active.width,
FocusDir::Up => rect.y + rect.height <= active.y,
FocusDir::Down => rect.y >= active.y + active.height,
};
if !matches_dir {
continue;
}
let cx = rect.x + rect.width / 2;
let cy = rect.y + rect.height / 2;
let dist: i32 = match dir {
FocusDir::Left | FocusDir::Right => {
(cx as i32 - active_cx as i32).abs() * 2 + (cy as i32 - active_cy as i32).abs()
}
FocusDir::Up | FocusDir::Down => {
(cy as i32 - active_cy as i32).abs() * 2 + (cx as i32 - active_cx as i32).abs()
}
};
match best {
Some((_, b)) if dist >= b => {}
_ => best = Some((id, dist)),
}
}
best.map(|(id, _)| id)
}
pub(super) fn mint_pane_id(&mut self) -> PaneId {
let id = self.next_pane_id;
self.next_pane_id = self.next_pane_id.saturating_add(1);
id
}
}