Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions crates/bitty-terminal/src/chrome_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,21 @@ pub(crate) fn close_focused_leaf(layout: &mut LayoutNode, focused: ViewId) -> bo
bitty_runtime::remove_leaf(layout, focused)
}

/// Pick the tiled survivor that should receive focus after closing `focused`.
///
/// The layout's depth-first leaf order is stable across layout shapes. Focus
/// the predecessor in that order, wrapping to the last surviving leaf when
/// the first leaf is closed.
fn previous_leaf_after_close(layout: &LayoutNode, focused: ViewId) -> Option<ViewId> {
let leaf_ids = layout.leaf_ids();
let position = leaf_ids.iter().position(|id| *id == focused)?;
if position > 0 {
Some(leaf_ids[position - 1])
} else {
leaf_ids.into_iter().rev().find(|id| *id != focused)
}
}

/// Record a candidate resize target: path to the deepest split whose axis
/// matches the resize direction and whose subtree holds focus, plus its ratio
/// and whether focus sits in its first child.
Expand Down Expand Up @@ -1548,6 +1563,13 @@ impl TerminalApp {
}
let mut layout = self.runtime.layout().clone();
if close_focused_leaf(&mut layout, focused) {
if let Some(previous) =
previous_leaf_after_close(self.runtime.layout(), focused)
{
// Set focus before the closing layout is installed so
// primary ownership re-homes to this same survivor.
self.runtime.set_focus(previous);
}
// CTX-0359: an explicit close is the only layout change
// allowed to re-home the primary owner; a plain
// `set_layout` (zoom, restore) must preserve it.
Expand Down Expand Up @@ -5226,6 +5248,54 @@ mod tests {
assert_eq!(two.leaf_count(), 1);
}

#[test]
fn chrome_close_view_focuses_previous_leaf_in_stable_order() {
use bitty_config::ChromeAction;

let mut app = workspace_test_app();
let layout = LayoutNode::split(
SplitAxis::Horizontal,
0.5,
two_pane_layout(),
LayoutNode::leaf(View::new(ViewId::new(3), 80, 24)),
);
app.runtime.set_layout(layout);
assert!(app.runtime.set_focus(ViewId::new(2)));

app.apply_chrome_action(ChromeAction::CloseView);

assert_eq!(
app.runtime.layout().leaf_ids(),
vec![ViewId::new(1), ViewId::new(3)]
);
assert_eq!(app.runtime.focused_view(), Some(ViewId::new(1)));
assert_eq!(app.runtime.primary_view(), Some(ViewId::new(1)));
}

#[test]
fn chrome_close_view_wraps_first_leaf_focus_to_last_survivor() {
use bitty_config::ChromeAction;

let mut app = workspace_test_app();
let layout = LayoutNode::split(
SplitAxis::Horizontal,
0.5,
two_pane_layout(),
LayoutNode::leaf(View::new(ViewId::new(3), 80, 24)),
);
app.runtime.set_layout(layout);
assert!(app.runtime.set_focus(ViewId::new(1)));

app.apply_chrome_action(ChromeAction::CloseView);

assert_eq!(
app.runtime.layout().leaf_ids(),
vec![ViewId::new(2), ViewId::new(3)]
);
assert_eq!(app.runtime.focused_view(), Some(ViewId::new(3)));
assert_eq!(app.runtime.primary_view(), Some(ViewId::new(3)));
}

// CTX-0257 workspace ops entry (DEC-0034): keys + tabline through the
// chrome arms, headless (no window).
// -----------------------------------------------------------------------
Expand Down
Loading