Skip to content
Closed
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
165 changes: 164 additions & 1 deletion cmux-tui/crates/cmux-tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9955,6 +9955,7 @@ impl App {
if !self.tree.select_surface(target.surface) {
return Ok(());
}
self.commit_workspace_preview();
self.follow_sidebar_workspace(target.workspace);
self.pane_focus_history.record(target.pane);
self.claim_active_terminal_geometry(true);
Expand Down Expand Up @@ -9989,6 +9990,7 @@ impl App {
if !self.prepare_pty_input_before_mutation() {
return Ok(());
}
self.commit_workspace_preview();
self.follow_sidebar_workspace(workspace);
self.tree.active_workspace = workspace;
if let Some(workspace_view) = self.tree.workspaces.get_mut(workspace) {
Expand Down Expand Up @@ -10098,7 +10100,18 @@ impl App {
}

fn focus_rail(&mut self, kind: RailKind) {
if kind != RailKind::Workspace {
// A workspace preview is a navigation transaction across the
// workspace-dependent rails. Keep it while moving between the
// workspace, tabs, and projection rails so those child views follow
// the highlighted workspace. A machine rail or pane transition ends
// the transaction.
let keep_workspace_preview = self.workspace_preview.is_some()
&& matches!(
self.focus,
FocusTarget::WorkspaceRail | FocusTarget::TabsRail | FocusTarget::ProjectionRail(_)
)
&& matches!(kind, RailKind::Workspace | RailKind::Tabs | RailKind::Projection(_));
if !keep_workspace_preview {
self.cancel_workspace_preview();
}
self.focus = match kind {
Expand Down Expand Up @@ -40892,6 +40905,44 @@ mod tests {
}
}

#[test]
fn workspace_sidebar_row_click_commits_hover_preview() {
let mux = Mux::new("workspace-sidebar-preview-row-click-test", SurfaceOptions::default());
let first = mux.new_workspace(Some("Alpha".into()), Some((80, 24))).unwrap();
let second = mux.new_workspace(Some("Beta".into()), Some((80, 24))).unwrap();
mux.select_workspace(Some(0), None);
let mut app = test_app(Session::Local(mux.clone()));
app.sidebar_view = SidebarView::Workspaces;
app.replace_tree(app.session.tree());
app.tree.active_workspace = 0;
app.sidebar_workspace_selection = 0;
app.workspace_rail_selection = WorkspaceRailSelection::Workspace;
app.focus = FocusTarget::WorkspaceRail;
app.sync_layout((100, 20));

let mut terminal = Terminal::new(TestBackend::new(100, 20)).unwrap();
terminal.draw(|frame| crate::ui::draw(&mut app, frame)).unwrap();
let row = app
.hits
.iter()
.find_map(|(rect, hit)| {
matches!(hit, super::Hit::Workspace { index: 1, .. }).then_some(*rect)
})
.expect("second workspace hit");

app.handle_hover_with_admission(row.x, row.y, KeyModifiers::NONE, None).unwrap();
assert!(app.workspace_preview.is_some());
app.handle_left_down(row.x, row.y, KeyModifiers::NONE).unwrap();

assert_eq!(app.tree.active_workspace, 1);
assert_eq!(app.workspace_preview, None);
assert_eq!(app.focus, FocusTarget::Pane);

for surface in [first.id, second.id] {
mux.close_surface(surface).unwrap();
}
}

#[test]
fn workspace_sidebar_preview_commits_when_pane_is_clicked() {
let mux = Mux::new("workspace-sidebar-preview-pane-click-test", SurfaceOptions::default());
Expand Down Expand Up @@ -41008,6 +41059,118 @@ mod tests {
}
}

#[test]
fn workspace_preview_survives_workspace_dependent_rail_navigation() {
let mux = Mux::new("workspace-sidebar-preview-child-rail-test", SurfaceOptions::default());
let first = mux.new_workspace(Some("Alpha".into()), Some((80, 24))).unwrap();
let second = mux.new_workspace(Some("Beta".into()), Some((80, 24))).unwrap();
let tree = Session::Local(mux.clone()).tree();
let first_workspace = tree.workspaces[0].id;
let second_workspace = tree.workspaces[1].id;
let mut app = test_app(Session::Local(mux.clone()));
app.sidebar_view = SidebarView::Workspaces;
app.config.sidebar.columns_explicit = true;
app.config.sidebar.columns = vec![
crate::config::SidebarColumn {
kind: SidebarColumnKind::Workspaces,
width: 24,
max_width: 0,
},
crate::config::SidebarColumn { kind: SidebarColumnKind::Tabs, width: 24, max_width: 0 },
];
app.config.sidebar.views = app
.config
.sidebar
.columns
.iter()
.map(|column| SidebarViewSpec::legacy(column.kind, column.width, column.max_width))
.collect();
app.config.sidebar.views_explicit = true;
app.replace_tree(app.session.tree());
app.tree.active_workspace = 0;
app.sidebar_workspace_selection = 1;
app.workspace_rail_selection = WorkspaceRailSelection::Workspace;
app.workspace_preview =
Some(WorkspacePreview { origin: first_workspace, target: second_workspace });
app.tree.active_workspace = 1;
app.focus = FocusTarget::WorkspaceRail;
app.sync_layout((100, 20));

app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)).unwrap();

assert_eq!(app.focus, FocusTarget::TabsRail);
assert_eq!(
app.workspace_preview,
Some(WorkspacePreview { origin: first_workspace, target: second_workspace })
);
assert_eq!(app.sidebar_workspace_selection, 1);
assert!(app.sidebar_tab_targets().iter().all(|target| target.workspace == 1));
assert!(app.sidebar_tab_targets().iter().any(|target| target.surface == second.id));

app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)).unwrap();

assert_eq!(app.focus, FocusTarget::Pane);
assert_eq!(app.workspace_preview, None);
assert_eq!(app.tree.active_workspace, 0);

for surface in [first.id, second.id] {
mux.close_surface(surface).unwrap();
}
}

#[test]
fn workspace_preview_commits_when_tabs_rail_target_is_activated() {
let mux =
Mux::new("workspace-sidebar-preview-tabs-activate-test", SurfaceOptions::default());
let first = mux.new_workspace(Some("Alpha".into()), Some((80, 24))).unwrap();
let second = mux.new_workspace(Some("Beta".into()), Some((80, 24))).unwrap();
let tree = Session::Local(mux.clone()).tree();
let first_workspace = tree.workspaces[0].id;
let second_workspace = tree.workspaces[1].id;
let mut app = test_app(Session::Local(mux.clone()));
app.sidebar_view = SidebarView::Workspaces;
app.config.sidebar.columns_explicit = true;
app.config.sidebar.columns = vec![
crate::config::SidebarColumn {
kind: SidebarColumnKind::Workspaces,
width: 24,
max_width: 0,
},
crate::config::SidebarColumn { kind: SidebarColumnKind::Tabs, width: 24, max_width: 0 },
];
app.config.sidebar.views = app
.config
.sidebar
.columns
.iter()
.map(|column| SidebarViewSpec::legacy(column.kind, column.width, column.max_width))
.collect();
app.config.sidebar.views_explicit = true;
app.replace_tree(app.session.tree());
app.tree.active_workspace = 0;
app.sidebar_workspace_selection = 1;
app.workspace_rail_selection = WorkspaceRailSelection::Workspace;
app.workspace_preview =
Some(WorkspacePreview { origin: first_workspace, target: second_workspace });
app.tree.active_workspace = 1;
app.focus = FocusTarget::WorkspaceRail;
app.sync_layout((100, 20));

app.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)).unwrap();
assert_eq!(app.focus, FocusTarget::TabsRail);
assert!(app.workspace_preview.is_some());

app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)).unwrap();

assert_eq!(app.focus, FocusTarget::Pane);
assert_eq!(app.workspace_preview, None);
assert_eq!(app.tree.active_workspace, 1);

for surface in [first.id, second.id] {
mux.close_surface(surface).unwrap();
}
}

#[test]
fn workspace_preview_reconciliation_aligns_selection_when_a_workspace_disappears() {
let mux = Mux::new("workspace-sidebar-preview-reconcile-test", SurfaceOptions::default());
Expand Down
Loading