use ratatui::layout::Rect;
use std::collections::HashMap;
use super::{cmdline::SearchDirection, dashboard::TableMode};
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
pub struct PaneId(pub uuid::Uuid);
impl PaneId {
pub fn new() -> Self {
Self(uuid::Uuid::new_v4())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaneType {
TableList,
SchemaPicker,
TableView,
SchemaView,
QueryEditor,
QueryResults,
}
impl std::fmt::Display for PaneType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PaneType::TableList => write!(f, "list"),
PaneType::SchemaPicker => write!(f, "schemapicker"),
PaneType::TableView => write!(f, "table"),
PaneType::SchemaView => write!(f, "schema"),
PaneType::QueryEditor => write!(f, "editor"),
PaneType::QueryResults => write!(f, "queryresults"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryEntry {
pub kind: PaneType,
pub bound_table: Option<String>,
pub bound_query_idx: Option<usize>,
}
pub fn fuzzy_match(text: &str, query: &str) -> bool {
if query.is_empty() {
return true;
}
let text_lower = text.to_lowercase();
let query_lower = query.to_lowercase();
let mut text_chars = text_lower.chars();
for q in query_lower.chars() {
loop {
match text_chars.next() {
Some(c) if c == q => break,
Some(_) => continue,
None => return false,
}
}
}
true
}
pub fn fuzzy_indices(text: &str, query: &str) -> Vec<usize> {
if query.is_empty() {
return vec![];
}
let lower_text: Vec<char> = text.to_lowercase().chars().collect();
let lower_query: Vec<char> = query.to_lowercase().chars().collect();
let text_chars: Vec<char> = text.chars().collect();
let mut indices = Vec::new();
let mut t = 0usize;
for &q in &lower_query {
while t < lower_text.len() && lower_text[t] != q {
t += 1;
}
if t >= lower_text.len() {
break;
}
let offset: usize = text_chars.iter().take(t).map(|c| c.len_utf8()).sum();
indices.push(offset);
t += 1;
}
indices
}
#[derive(Debug, Clone)]
pub struct SearchState {
pub query: String,
pub direction: SearchDirection,
pub matches: Vec<usize>,
pub current_idx: usize,
}
#[derive(Debug, Clone)]
pub struct LiveSearchState {
pub query: String,
pub direction: SearchDirection,
pub matches: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryEditorSnapshot {
pub text: Vec<String>,
pub cursor: (usize, usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingInsertRow {
pub position: usize,
pub values: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayRowRef {
Existing(usize),
PendingInsert(usize),
}
#[derive(Debug, Clone)]
pub struct Pane {
pub id: PaneId,
pub kind: PaneType,
pub display_id: usize,
pub bound_table: Option<String>,
pub nav_cursor: usize,
pub nav_offset: usize,
pub row_cursor: usize,
pub row_offset: usize,
pub cursor_col: usize,
pub col_offset: usize,
pub mode: TableMode,
pub last_search: Option<SearchState>,
pub live_search: Option<LiveSearchState>,
pub visual_anchor: Option<usize>,
pub pending_updates: Vec<(usize, usize, String)>,
pub pending_deletes: Vec<String>,
pub pending_inserts: Vec<PendingInsertRow>,
pub filter: Option<String>,
pub sort_col: Option<String>,
pub sort_desc: bool,
pub selected_cols: Option<Vec<String>>,
pub query_text: Vec<String>,
pub query_cursor: (usize, usize),
pub query_scroll_offset: usize,
pub query_row_offset: usize,
pub autocomplete_idx: usize,
pub autocomplete_matches: Vec<String>,
pub autocomplete_selected: Option<usize>,
pub query_pending_key: Option<char>,
pub query_pending_count: Option<usize>,
pub query_last_find: Option<(char, char)>,
pub query_visual_anchor: Option<(usize, usize)>,
pub query_visual_line_mode: bool,
pub query_yank_register: String,
pub query_yank_linewise: bool,
pub query_yank_highlight_ranges: Vec<(usize, usize, usize)>,
pub query_yank_highlight_at: Option<std::time::Instant>,
pub query_undo_stack: Vec<QueryEditorSnapshot>,
pub query_redo_stack: Vec<QueryEditorSnapshot>,
pub bound_query_idx: Option<usize>,
pub query_result_count: usize,
pub area: Option<Rect>,
pub history: Vec<HistoryEntry>,
pub history_pos: usize,
}
impl Pane {
pub fn new(id: PaneId, kind: PaneType, display_id: usize) -> Self {
Self {
id,
kind: kind.clone(),
display_id,
bound_table: None,
nav_cursor: 0,
nav_offset: 0,
row_cursor: 0,
row_offset: 0,
cursor_col: 0,
col_offset: 0,
mode: TableMode::Normal,
last_search: None,
live_search: None,
visual_anchor: None,
pending_updates: Vec::new(),
pending_deletes: Vec::new(),
pending_inserts: Vec::new(),
filter: None,
sort_col: None,
sort_desc: false,
selected_cols: None,
query_text: vec![String::new()],
query_cursor: (0, 0),
query_scroll_offset: 0,
query_row_offset: 0,
autocomplete_idx: 0,
autocomplete_matches: Vec::new(),
autocomplete_selected: None,
query_pending_key: None,
query_pending_count: None,
query_last_find: None,
query_visual_anchor: None,
query_visual_line_mode: false,
query_yank_register: String::new(),
query_yank_linewise: false,
query_yank_highlight_ranges: Vec::new(),
query_yank_highlight_at: None,
query_undo_stack: Vec::new(),
query_redo_stack: Vec::new(),
bound_query_idx: None,
query_result_count: 0,
area: None,
history: vec![HistoryEntry {
kind,
bound_table: None,
bound_query_idx: None,
}],
history_pos: 0,
}
}
fn push_history(&mut self) {
let entry = HistoryEntry {
kind: self.kind.clone(),
bound_table: self.bound_table.clone(),
bound_query_idx: self.bound_query_idx,
};
if self.history_pos + 1 < self.history.len() {
self.history.truncate(self.history_pos + 1);
}
if self.history.last() != Some(&entry) {
self.history.push(entry);
self.history_pos = self.history.len() - 1;
}
}
fn restore_from_history(&mut self) {
if let Some(entry) = self.history.get(self.history_pos) {
self.kind = entry.kind.clone();
self.bound_table = entry.bound_table.clone();
self.bound_query_idx = entry.bound_query_idx;
}
}
pub fn go_back(&mut self) -> bool {
if self.history_pos == 0 {
return false;
}
self.history_pos -= 1;
self.restore_from_history();
true
}
pub fn go_forward(&mut self) -> bool {
if self.history_pos + 1 >= self.history.len() {
return false;
}
self.history_pos += 1;
self.restore_from_history();
true
}
pub fn reset_to_list(&mut self) {
self.kind = PaneType::TableList;
self.bound_table = None;
self.nav_cursor = 0;
self.nav_offset = 0;
self.row_cursor = 0;
self.row_offset = 0;
self.cursor_col = 0;
self.col_offset = 0;
self.mode = TableMode::Normal;
self.last_search = None;
self.live_search = None;
self.pending_updates.clear();
self.pending_deletes.clear();
self.pending_inserts.clear();
self.filter = None;
self.sort_col = None;
self.sort_desc = false;
self.autocomplete_matches.clear();
self.autocomplete_selected = None;
self.query_pending_key = None;
self.query_pending_count = None;
self.query_last_find = None;
self.query_visual_anchor = None;
self.query_visual_line_mode = false;
self.query_yank_register.clear();
self.query_yank_linewise = false;
self.query_yank_highlight_ranges.clear();
self.query_yank_highlight_at = None;
self.query_undo_stack.clear();
self.query_redo_stack.clear();
self.push_history();
}
pub fn set_table_view(&mut self, table_name: String) {
self.kind = PaneType::TableView;
self.bound_table = Some(table_name);
self.row_cursor = 0;
self.row_offset = 0;
self.cursor_col = 0;
self.col_offset = 0;
self.mode = TableMode::Normal;
self.last_search = None; self.live_search = None;
self.visual_anchor = None;
self.pending_updates.clear();
self.pending_deletes.clear();
self.pending_inserts.clear();
self.filter = None;
self.sort_col = None;
self.sort_desc = false;
self.push_history();
}
pub fn set_schema_view(&mut self, table_name: String) {
self.kind = PaneType::SchemaView;
self.bound_table = Some(table_name);
self.nav_cursor = 0;
self.nav_offset = 0;
self.push_history();
}
pub fn set_schema_picker(&mut self) {
self.kind = PaneType::SchemaPicker;
self.bound_table = None;
self.nav_cursor = 0;
self.nav_offset = 0;
self.mode = TableMode::Normal;
self.last_search = None;
self.live_search = None;
self.push_history();
}
pub fn set_query_editor(&mut self) {
self.kind = PaneType::QueryEditor;
self.query_text = vec![String::new()];
self.query_cursor = (0, 0);
self.query_scroll_offset = 0;
self.query_row_offset = 0;
self.autocomplete_matches.clear();
self.autocomplete_selected = None;
self.autocomplete_idx = 0;
self.query_pending_key = None;
self.query_pending_count = None;
self.query_last_find = None;
self.query_visual_anchor = None;
self.query_visual_line_mode = false;
self.query_yank_highlight_ranges.clear();
self.query_yank_highlight_at = None;
self.query_undo_stack.clear();
self.query_redo_stack.clear();
self.push_history();
}
pub fn set_query_results(&mut self, idx: usize) {
self.kind = PaneType::QueryResults;
self.bound_query_idx = Some(idx);
self.push_history();
}
pub fn total_table_rows(&self, loaded_row_count: usize) -> usize {
loaded_row_count + self.pending_inserts.len()
}
pub fn display_row_ref(
&self,
loaded_row_count: usize,
display_row: usize,
) -> Option<DisplayRowRef> {
let total = self.total_table_rows(loaded_row_count);
if display_row >= total {
return None;
}
let mut inserts: Vec<(usize, usize)> = self
.pending_inserts
.iter()
.enumerate()
.map(|(idx, row)| (row.position, idx))
.collect();
inserts.sort_unstable_by_key(|(pos, _)| *pos);
let mut inserted_before = 0usize;
for (pos, idx) in inserts {
if pos == display_row {
return Some(DisplayRowRef::PendingInsert(idx));
}
if pos < display_row {
inserted_before += 1;
} else {
break;
}
}
let loaded_idx = display_row.saturating_sub(inserted_before);
if loaded_idx < loaded_row_count {
Some(DisplayRowRef::Existing(loaded_idx))
} else {
None
}
}
pub fn stage_insert_row(&mut self, display_index: usize, col_count: usize) -> usize {
for row in &mut self.pending_inserts {
if row.position >= display_index {
row.position += 1;
}
}
self.pending_inserts.push(PendingInsertRow {
position: display_index,
values: vec![String::new(); col_count],
});
self.pending_inserts.sort_unstable_by_key(|r| r.position);
self.pending_inserts
.iter()
.position(|r| r.position == display_index)
.unwrap_or_else(|| self.pending_inserts.len().saturating_sub(1))
}
pub fn remove_pending_insert(&mut self, insert_idx: usize) {
if insert_idx >= self.pending_inserts.len() {
return;
}
let removed_pos = self.pending_inserts[insert_idx].position;
self.pending_inserts.remove(insert_idx);
for row in &mut self.pending_inserts {
if row.position > removed_pos {
row.position = row.position.saturating_sub(1);
}
}
}
pub fn row_next(&mut self, max: usize) {
if max > 0 {
self.row_cursor = (self.row_cursor + 1).min(max.saturating_sub(1));
}
}
pub fn row_prev(&mut self) {
self.row_cursor = self.row_cursor.saturating_sub(1);
}
pub fn row_top(&mut self) {
self.row_cursor = 0;
}
pub fn row_bottom(&mut self, max: usize) {
if max > 0 {
self.row_cursor = max.saturating_sub(1);
}
}
pub fn sync_row_offset(&mut self, viewport: usize) {
if self.row_cursor < self.row_offset {
self.row_offset = self.row_cursor;
} else if self.row_cursor >= self.row_offset + viewport {
self.row_offset = self.row_cursor + 1 - viewport;
}
}
pub fn col_right(&mut self, max: usize) {
if max > 0 {
self.cursor_col = (self.cursor_col + 1).min(max.saturating_sub(1));
}
}
pub fn col_left(&mut self) {
self.cursor_col = self.cursor_col.saturating_sub(1);
}
pub fn col_first(&mut self) {
self.cursor_col = 0;
}
pub fn col_last(&mut self, max: usize) {
if max > 0 {
self.cursor_col = max.saturating_sub(1);
}
}
pub fn sync_col_offset(&mut self, viewport: usize) {
if self.cursor_col < self.col_offset {
self.col_offset = self.cursor_col;
} else if self.cursor_col >= self.col_offset + viewport {
self.col_offset = self.cursor_col + 1 - viewport;
}
}
pub fn sync_query_row_offset(&mut self, viewport: usize) {
let (row, _) = self.query_cursor;
if row < self.query_row_offset {
self.query_row_offset = row;
} else if row >= self.query_row_offset + viewport {
self.query_row_offset = row + 1 - viewport;
}
}
pub fn push_query_snapshot(&mut self) {
let snapshot = QueryEditorSnapshot {
text: self.query_text.clone(),
cursor: self.query_cursor,
};
if self.query_undo_stack.last() != Some(&snapshot) {
self.query_undo_stack.push(snapshot);
}
self.query_redo_stack.clear();
}
pub fn query_undo(&mut self) -> bool {
let Some(snapshot) = self.query_undo_stack.pop() else {
return false;
};
let current = QueryEditorSnapshot {
text: self.query_text.clone(),
cursor: self.query_cursor,
};
self.query_redo_stack.push(current);
self.query_text = if snapshot.text.is_empty() {
vec![String::new()]
} else {
snapshot.text
};
self.query_cursor = snapshot.cursor;
self.query_pending_key = None;
self.query_pending_count = None;
self.query_last_find = None;
self.query_visual_anchor = None;
self.query_visual_line_mode = false;
self.query_yank_highlight_ranges.clear();
self.query_yank_highlight_at = None;
self.autocomplete_matches.clear();
self.autocomplete_selected = None;
true
}
pub fn query_redo(&mut self) -> bool {
let Some(snapshot) = self.query_redo_stack.pop() else {
return false;
};
let current = QueryEditorSnapshot {
text: self.query_text.clone(),
cursor: self.query_cursor,
};
self.query_undo_stack.push(current);
self.query_text = if snapshot.text.is_empty() {
vec![String::new()]
} else {
snapshot.text
};
self.query_cursor = snapshot.cursor;
self.query_pending_key = None;
self.query_pending_count = None;
self.query_last_find = None;
self.query_visual_anchor = None;
self.query_visual_line_mode = false;
self.query_yank_highlight_ranges.clear();
self.query_yank_highlight_at = None;
self.autocomplete_matches.clear();
self.autocomplete_selected = None;
true
}
pub fn nav_next(&mut self, max: usize) {
if max > 0 {
self.nav_cursor = (self.nav_cursor + 1).min(max.saturating_sub(1));
}
}
pub fn nav_prev(&mut self) {
self.nav_cursor = self.nav_cursor.saturating_sub(1);
}
pub fn nav_top(&mut self) {
self.nav_cursor = 0;
}
pub fn nav_bottom(&mut self, max: usize) {
if max > 0 {
self.nav_cursor = max.saturating_sub(1);
}
}
pub fn sync_nav_offset(&mut self, viewport: usize) {
if self.nav_cursor < self.nav_offset {
self.nav_offset = self.nav_cursor;
} else if self.nav_cursor >= self.nav_offset + viewport {
self.nav_offset = self.nav_cursor + 1 - viewport;
}
}
}
#[derive(Debug, Clone)]
pub enum LayoutNode {
Leaf(PaneId),
HSplit {
ratio: f32, left: Box<LayoutNode>,
right: Box<LayoutNode>,
},
VSplit {
ratio: f32, top: Box<LayoutNode>,
bottom: Box<LayoutNode>,
},
}
impl LayoutNode {
pub fn leaf(id: PaneId) -> Self {
LayoutNode::Leaf(id)
}
pub fn hsplit(left: LayoutNode, right: LayoutNode) -> Self {
LayoutNode::HSplit {
ratio: 0.5,
left: Box::new(left),
right: Box::new(right),
}
}
pub fn vsplit(top: LayoutNode, bottom: LayoutNode) -> Self {
LayoutNode::VSplit {
ratio: 0.5,
top: Box::new(top),
bottom: Box::new(bottom),
}
}
pub fn count_leaves(&self) -> usize {
match self {
LayoutNode::Leaf(_) => 1,
LayoutNode::HSplit { left, right, .. } => left.count_leaves() + right.count_leaves(),
LayoutNode::VSplit { top, bottom, .. } => top.count_leaves() + bottom.count_leaves(),
}
}
pub fn has_vsplit(&self) -> bool {
match self {
LayoutNode::Leaf(_) => false,
LayoutNode::VSplit { .. } => true,
LayoutNode::HSplit { left, right, .. } => left.has_vsplit() || right.has_vsplit(),
}
}
pub fn contains(&self, target: PaneId) -> bool {
match self {
LayoutNode::Leaf(id) => *id == target,
LayoutNode::HSplit { left, right, .. } => {
left.contains(target) || right.contains(target)
}
LayoutNode::VSplit { top, bottom, .. } => {
top.contains(target) || bottom.contains(target)
}
}
}
pub fn find_split_for(&mut self, target: PaneId) -> Option<(bool, bool, &mut f32)> {
match self {
LayoutNode::Leaf(_) => None,
LayoutNode::HSplit { ratio, left, right } => {
if left.contains(target) {
if let LayoutNode::Leaf(_) = **left {
Some((true, true, ratio))
} else {
left.find_split_for(target)
}
} else if right.contains(target) {
if let LayoutNode::Leaf(_) = **right {
Some((true, false, ratio))
} else {
right.find_split_for(target)
}
} else {
None
}
}
LayoutNode::VSplit { ratio, top, bottom } => {
if top.contains(target) {
if let LayoutNode::Leaf(_) = **top {
Some((false, true, ratio))
} else {
top.find_split_for(target)
}
} else if bottom.contains(target) {
if let LayoutNode::Leaf(_) = **bottom {
Some((false, false, ratio))
} else {
bottom.find_split_for(target)
}
} else {
None
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct PaneTree {
pub root: LayoutNode,
pub panes: HashMap<PaneId, Pane>,
pub active_pane: PaneId,
next_display_id: usize,
recycled_ids: Vec<usize>,
pub fullscreen_pane: Option<PaneId>,
}
impl PaneTree {
pub fn new(initial: PaneType) -> Self {
let id = PaneId::new();
let mut panes = HashMap::new();
panes.insert(id, Pane::new(id, initial, 1));
Self {
root: LayoutNode::leaf(id),
panes,
active_pane: id,
next_display_id: 2,
recycled_ids: Vec::new(),
fullscreen_pane: None,
}
}
pub fn toggle_fullscreen(&mut self) {
if let Some(fs) = self.fullscreen_pane {
if fs == self.active_pane {
self.fullscreen_pane = None;
return;
}
}
self.fullscreen_pane = Some(self.active_pane);
}
pub fn exit_fullscreen(&mut self) {
self.fullscreen_pane = None;
}
pub fn is_fullscreen(&self) -> bool {
self.fullscreen_pane.is_some()
}
pub fn alloc_display_id(&mut self) -> usize {
if let Some(id) = self.recycled_ids.pop() {
id
} else {
let id = self.next_display_id;
self.next_display_id += 1;
id
}
}
pub fn active_mut(&mut self) -> Option<&mut Pane> {
self.panes.get_mut(&self.active_pane)
}
pub fn active(&self) -> Option<&Pane> {
self.panes.get(&self.active_pane)
}
pub fn pane_count(&self) -> usize {
self.panes.len()
}
const MIN_RATIO: f32 = 0.1;
const MAX_RATIO: f32 = 0.9;
pub fn resize_active(&mut self, delta: i32) -> Result<(), &'static str> {
if self.pane_count() <= 1 {
return Err("cannot resize single pane");
}
let delta_f = delta as f32 / 100.0;
let target = self.active_pane;
match self.root.find_split_for(target) {
Some((_, is_first_child, ratio)) => {
if is_first_child {
*ratio = (*ratio + delta_f).clamp(Self::MIN_RATIO, Self::MAX_RATIO);
} else {
*ratio = (*ratio - delta_f).clamp(Self::MIN_RATIO, Self::MAX_RATIO);
}
Ok(())
}
None => Err("no split found for active pane"),
}
}
pub fn split_active_v(&mut self, new_kind: PaneType) -> Result<PaneId, &'static str> {
if self.pane_count() >= 8 {
return Err("maximum pane count (8) reached");
}
self.exit_fullscreen();
let new_id = PaneId::new();
let display_id = self.alloc_display_id();
self.panes
.insert(new_id, Pane::new(new_id, new_kind, display_id));
self.replace_leaf_with_split(self.active_pane, true, new_id);
self.active_pane = new_id;
Ok(new_id)
}
pub fn split_active_h(&mut self, new_kind: PaneType) -> Result<PaneId, &'static str> {
if self.pane_count() >= 8 {
return Err("maximum pane count (8) reached");
}
self.exit_fullscreen();
let new_id = PaneId::new();
let display_id = self.alloc_display_id();
self.panes
.insert(new_id, Pane::new(new_id, new_kind, display_id));
self.replace_leaf_with_split(self.active_pane, false, new_id);
self.active_pane = new_id;
Ok(new_id)
}
pub fn replace_leaf_with_split(&mut self, target: PaneId, is_hsplit: bool, new_id: PaneId) {
self.root = Self::replace_leaf_recursive(
std::mem::replace(&mut self.root, LayoutNode::leaf(new_id)),
target,
is_hsplit,
new_id,
);
}
fn replace_leaf_recursive(
node: LayoutNode,
target: PaneId,
is_hsplit: bool,
new_id: PaneId,
) -> LayoutNode {
match node {
LayoutNode::Leaf(id) if id == target => {
if is_hsplit {
LayoutNode::hsplit(LayoutNode::Leaf(id), LayoutNode::Leaf(new_id))
} else {
LayoutNode::vsplit(LayoutNode::Leaf(id), LayoutNode::Leaf(new_id))
}
}
LayoutNode::HSplit { ratio, left, right } => {
let new_left = Self::replace_leaf_recursive(*left, target, is_hsplit, new_id);
let new_right = Self::replace_leaf_recursive(*right, target, is_hsplit, new_id);
LayoutNode::HSplit {
ratio,
left: Box::new(new_left),
right: Box::new(new_right),
}
}
LayoutNode::VSplit { ratio, top, bottom } => {
let new_top = Self::replace_leaf_recursive(*top, target, is_hsplit, new_id);
let new_bottom = Self::replace_leaf_recursive(*bottom, target, is_hsplit, new_id);
LayoutNode::VSplit {
ratio,
top: Box::new(new_top),
bottom: Box::new(new_bottom),
}
}
other => other,
}
}
pub fn close_active(&mut self) -> bool {
if self.pane_count() <= 1 {
self.panes.clear();
self.fullscreen_pane = None;
return true;
}
let target = self.active_pane;
if self.fullscreen_pane == Some(target) {
self.fullscreen_pane = None;
}
if let Some(pane) = self.panes.remove(&target) {
self.recycled_ids.push(pane.display_id);
}
let (new_root, sibling) = Self::remove_leaf_recursive(self.root.clone(), target);
self.root = new_root.unwrap_or_else(|| LayoutNode::Leaf(target));
self.active_pane = sibling
.or_else(|| self.panes.keys().next().copied())
.unwrap_or(target);
false
}
pub fn close_by_display_id(&mut self, display_id: usize) -> bool {
let target = self
.panes
.iter()
.find(|(_, p)| p.display_id == display_id)
.map(|(id, _)| *id);
let Some(target) = target else { return false };
if self.pane_count() <= 1 {
self.panes.clear();
self.fullscreen_pane = None;
return true;
}
if self.fullscreen_pane == Some(target) {
self.fullscreen_pane = None;
}
if let Some(pane) = self.panes.remove(&target) {
self.recycled_ids.push(pane.display_id);
}
let (new_root, sibling) = Self::remove_leaf_recursive(self.root.clone(), target);
self.root = new_root.unwrap_or_else(|| LayoutNode::Leaf(target));
self.active_pane = sibling
.or_else(|| self.panes.keys().next().copied())
.unwrap_or(target);
false
}
fn remove_leaf_recursive(
node: LayoutNode,
target: PaneId,
) -> (Option<LayoutNode>, Option<PaneId>) {
match node {
LayoutNode::Leaf(id) if id == target => (None, None),
leaf @ LayoutNode::Leaf(_) => (Some(leaf), None),
LayoutNode::HSplit { ratio, left, right } => {
let right_sibling = first_leaf_id(&right);
let left_sibling = first_leaf_id(&left);
let (new_left, s1) = Self::remove_leaf_recursive(*left, target);
if new_left.is_none() {
return (Some(*right), right_sibling);
}
let (new_right, s2) = Self::remove_leaf_recursive(*right, target);
if new_right.is_none() {
return (new_left, left_sibling);
}
(
Some(LayoutNode::HSplit {
ratio,
left: Box::new(new_left.unwrap()),
right: Box::new(new_right.unwrap()),
}),
s1.or(s2),
)
}
LayoutNode::VSplit { ratio, top, bottom } => {
let bottom_sibling = first_leaf_id(&bottom);
let top_sibling = first_leaf_id(&top);
let (new_top, s1) = Self::remove_leaf_recursive(*top, target);
if new_top.is_none() {
return (Some(*bottom), bottom_sibling);
}
let (new_bottom, s2) = Self::remove_leaf_recursive(*bottom, target);
if new_bottom.is_none() {
return (new_top, top_sibling);
}
(
Some(LayoutNode::VSplit {
ratio,
top: Box::new(new_top.unwrap()),
bottom: Box::new(new_bottom.unwrap()),
}),
s1.or(s2),
)
}
}
}
pub fn navigate(&mut self, direction: PaneDirection) {
let Some(active) = self.active() else { return };
let Some(active_area) = active.area else {
return;
};
let mut best: Option<(PaneId, u16)> = None;
for (id, pane) in &self.panes {
if *id == self.active_pane {
continue;
}
let Some(area) = pane.area else { continue };
let (is_adjacent, distance) = match direction {
PaneDirection::Left => {
let other_right = area.x.saturating_add(area.width);
let active_left = active_area.x;
if other_right <= active_left {
let overlap_top = area.y.max(active_area.y);
let overlap_bottom =
(area.y + area.height).min(active_area.y + active_area.height);
if overlap_bottom > overlap_top {
(true, active_left - other_right)
} else {
(false, 0)
}
} else {
(false, 0)
}
}
PaneDirection::Right => {
let other_left = area.x;
let active_right = active_area.x.saturating_add(active_area.width);
if other_left >= active_right {
let overlap_top = area.y.max(active_area.y);
let overlap_bottom =
(area.y + area.height).min(active_area.y + active_area.height);
if overlap_bottom > overlap_top {
(true, other_left - active_right)
} else {
(false, 0)
}
} else {
(false, 0)
}
}
PaneDirection::Up => {
let other_bottom = area.y.saturating_add(area.height);
let active_top = active_area.y;
if other_bottom <= active_top {
let overlap_left = area.x.max(active_area.x);
let overlap_right =
(area.x + area.width).min(active_area.x + active_area.width);
if overlap_right > overlap_left {
(true, active_top - other_bottom)
} else {
(false, 0)
}
} else {
(false, 0)
}
}
PaneDirection::Down => {
let other_top = area.y;
let active_bottom = active_area.y.saturating_add(active_area.height);
if other_top >= active_bottom {
let overlap_left = area.x.max(active_area.x);
let overlap_right =
(area.x + area.width).min(active_area.x + active_area.width);
if overlap_right > overlap_left {
(true, other_top - active_bottom)
} else {
(false, 0)
}
} else {
(false, 0)
}
}
};
if is_adjacent {
if best.map_or(true, |(_, d)| distance < d) {
best = Some((*id, distance));
}
}
}
if let Some((id, _)) = best {
self.active_pane = id;
}
}
pub fn collect_leaves(&self) -> Vec<PaneId> {
let mut out = Vec::new();
Self::collect_leaves_recursive(&self.root, &mut out);
out
}
fn collect_leaves_recursive(node: &LayoutNode, out: &mut Vec<PaneId>) {
match node {
LayoutNode::Leaf(id) => out.push(*id),
LayoutNode::HSplit { left, right, .. } => {
Self::collect_leaves_recursive(left, out);
Self::collect_leaves_recursive(right, out);
}
LayoutNode::VSplit { top, bottom, .. } => {
Self::collect_leaves_recursive(top, out);
Self::collect_leaves_recursive(bottom, out);
}
}
}
pub fn compute_areas(&mut self, area: Rect) {
Self::compute_areas_recursive(&self.root, area, &mut self.panes);
}
fn compute_areas_recursive(node: &LayoutNode, area: Rect, panes: &mut HashMap<PaneId, Pane>) {
match node {
LayoutNode::Leaf(id) => {
if let Some(pane) = panes.get_mut(id) {
pane.area = Some(area);
}
}
LayoutNode::HSplit {
ratio, left, right, ..
} => {
let split_x = area.x + (area.width as f32 * ratio.max(0.01).min(0.99)) as u16;
let left_area = Rect {
x: area.x,
y: area.y,
width: split_x.saturating_sub(area.x),
height: area.height,
};
let right_area = Rect {
x: split_x,
y: area.y,
width: (area.x + area.width).saturating_sub(split_x),
height: area.height,
};
Self::compute_areas_recursive(left, left_area, panes);
Self::compute_areas_recursive(right, right_area, panes);
}
LayoutNode::VSplit {
ratio, top, bottom, ..
} => {
let split_y = area.y + (area.height as f32 * ratio.max(0.01).min(0.99)) as u16;
let top_area = Rect {
x: area.x,
y: area.y,
width: area.width,
height: split_y.saturating_sub(area.y),
};
let bottom_area = Rect {
x: area.x,
y: split_y,
width: area.width,
height: (area.y + area.height).saturating_sub(split_y),
};
Self::compute_areas_recursive(top, top_area, panes);
Self::compute_areas_recursive(bottom, bottom_area, panes);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaneDirection {
Left,
Down,
Up,
Right,
}
fn first_leaf_id(node: &LayoutNode) -> Option<PaneId> {
match node {
LayoutNode::Leaf(id) => Some(*id),
LayoutNode::HSplit { left, .. } => first_leaf_id(left),
LayoutNode::VSplit { top, .. } => first_leaf_id(top),
}
}