aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-09-13 02:20:23 +0300
committerGitHub <noreply@github.com>2026-09-13 02:20:23 +0300
commit5eafc612674c4ac8f7c938769159a3cfd22e52d4 (patch)
tree3eaadedd0cbb6c2ca7ef2ae2da31771e824df124 /src/client
parente3a46f4f89142d929bf35c31aac85e5f374d8dcf (diff)
fix: honor space row gaps across machines (#3740)
* fix: honor space row gaps across machines refs #3738 * test: cover scrolling with machine space gaps refs #3738 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src/client')
-rw-r--r--src/client/shell/endpoint_sidebar.rs25
-rw-r--r--src/client/shell/tests/endpoints.rs109
2 files changed, 130 insertions, 4 deletions
diff --git a/src/client/shell/endpoint_sidebar.rs b/src/client/shell/endpoint_sidebar.rs
index 193d75d1..bce484a3 100644
--- a/src/client/shell/endpoint_sidebar.rs
+++ b/src/client/shell/endpoint_sidebar.rs
@@ -314,7 +314,20 @@ pub(super) fn render_expanded(
}
})
.collect::<Vec<_>>();
- let gaps = vec![0; rows.len()];
+ let gaps = rows
+ .iter()
+ .enumerate()
+ .map(|(index, row)| match (row, rows.get(index + 1)) {
+ (
+ Row::Workspace { endpoint, .. },
+ Some(Row::Workspace {
+ endpoint: next_endpoint,
+ entry,
+ }),
+ ) if endpoint == next_endpoint => u16::from(!entry.indented) * config.spaces.row_gap,
+ _ => 0,
+ })
+ .collect::<Vec<_>>();
if std::mem::take(state.reveal_navigation_workspace) {
let selected_row = rows.iter().position(|row| match row {
Row::Workspace { endpoint, entry } => {
@@ -355,7 +368,7 @@ pub(super) fn render_expanded(
let show_scrollbar = metrics.max_offset_from_bottom > 0 && body.width > 1;
let content_width = body.width.saturating_sub(u16::from(show_scrollbar));
let mut y = body.y;
- for row in rows.iter().skip(*state.workspace_scroll) {
+ for (row_index, row) in rows.iter().enumerate().skip(*state.workspace_scroll) {
match row {
Row::Endpoint(index) => {
if y >= body.bottom() {
@@ -383,7 +396,9 @@ pub(super) fn render_expanded(
),
endpoint_id: endpoint.endpoint_id.clone(),
});
- y = y.saturating_add(1);
+ y = y
+ .saturating_add(1)
+ .saturating_add(gaps.get(row_index).copied().unwrap_or(0));
}
Row::Workspace { endpoint, entry } => {
let endpoint = &state.endpoints[*endpoint];
@@ -460,7 +475,9 @@ pub(super) fn render_expanded(
indented: entry.indented,
group_toggle,
});
- y = y.saturating_add(height);
+ y = y
+ .saturating_add(height)
+ .saturating_add(gaps.get(row_index).copied().unwrap_or(0));
}
}
}
diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs
index a3a80979..2e5e87ac 100644
--- a/src/client/shell/tests/endpoints.rs
+++ b/src/client/shell/tests/endpoints.rs
@@ -438,6 +438,115 @@ fn saved_machine_preserves_endpoint_scoped_worktree_collapses() {
}
#[test]
+fn expanded_machine_sidebar_applies_space_row_gap_within_each_machine() {
+ let (mut state, remote_id) = state_with_remote();
+ state.config.spaces.row_gap = 1;
+
+ let add_second_workspace = |snapshot: &mut ClientShellSnapshot| {
+ let mut workspace = snapshot.workspaces[0].clone();
+ workspace.workspace_id = "ws_2".into();
+ workspace.active_tab_id = "tab_2".into();
+ workspace.number = 2;
+ workspace.label = "second-workspace".into();
+ workspace.focused = false;
+ snapshot.workspaces.push(workspace);
+ };
+ let mut local = snapshot();
+ add_second_workspace(&mut local);
+ state.set_snapshot(Box::new(local));
+ let mut remote = snapshot();
+ remote.boot_id = "remote-boot".into();
+ remote.workspaces[0].worktree = Some(ClientShellWorktree {
+ key: "repo".into(),
+ label: "repo".into(),
+ is_linked_worktree: false,
+ });
+ add_second_workspace(&mut remote);
+ remote.workspaces[1].worktree = Some(ClientShellWorktree {
+ key: "repo".into(),
+ label: "repo".into(),
+ is_linked_worktree: true,
+ });
+ let mut third = remote.workspaces[1].clone();
+ third.workspace_id = "ws_3".into();
+ third.number = 3;
+ third.label = "third-workspace".into();
+ third.worktree = None;
+ remote.workspaces.push(third);
+ state.set_endpoint_snapshot(&remote_id, Box::new(remote));
+
+ state.compose(100, 40).expect("combined endpoint frame");
+ let local_workspaces = state
+ .hits
+ .workspaces
+ .iter()
+ .filter(|hit| hit.endpoint_id.is_local())
+ .collect::<Vec<_>>();
+ assert_eq!(local_workspaces.len(), 2);
+ assert_eq!(
+ local_workspaces[1].rect.y,
+ local_workspaces[0].rect.bottom() + 1
+ );
+
+ let local_machine = state
+ .hits
+ .machines
+ .iter()
+ .find(|hit| hit.endpoint_id.is_local())
+ .expect("local machine");
+ let remote_machine = state
+ .hits
+ .machines
+ .iter()
+ .find(|hit| hit.endpoint_id == remote_id)
+ .expect("remote machine");
+ assert_eq!(local_workspaces[0].rect.y, local_machine.rect.bottom());
+ assert_eq!(remote_machine.rect.y, local_workspaces[1].rect.bottom());
+
+ let remote_workspaces = state
+ .hits
+ .workspaces
+ .iter()
+ .filter(|hit| hit.endpoint_id == remote_id)
+ .collect::<Vec<_>>();
+ assert_eq!(remote_workspaces.len(), 3);
+ assert_eq!(remote_workspaces[0].rect.y, remote_machine.rect.bottom());
+ assert_eq!(
+ remote_workspaces[1].rect.y,
+ remote_workspaces[0].rect.bottom()
+ );
+ assert_eq!(
+ remote_workspaces[2].rect.y,
+ remote_workspaces[1].rect.bottom() + 1
+ );
+
+ state.workspace_scroll = usize::MAX;
+ state.compose(100, 18).expect("scrolled endpoint frame");
+ let metrics = state
+ .hits
+ .workspace_scroll_metrics
+ .expect("workspace scroll metrics");
+ assert!(metrics.max_offset_from_bottom > 0);
+ assert_eq!(metrics.offset_from_bottom, 0);
+ assert_eq!(state.workspace_scroll, metrics.max_offset_from_bottom);
+ let visible_remote = state
+ .hits
+ .workspaces
+ .iter()
+ .filter(|hit| hit.endpoint_id == remote_id)
+ .collect::<Vec<_>>();
+ assert_eq!(visible_remote.len(), 3);
+ let gap_y = visible_remote[1].rect.bottom();
+ assert_eq!(visible_remote[2].rect.y, gap_y + 1);
+ assert!(visible_remote[2].rect.bottom() <= state.hits.workspace_body.bottom());
+ assert!(state
+ .hits
+ .workspaces
+ .iter()
+ .all(|hit| gap_y < hit.rect.top() || gap_y >= hit.rect.bottom()));
+}
+
+#[test]
fn active_workspace_is_the_only_highlight_when_machine_is_expanded() {
let (mut state, endpoint_id) = state_with_remote();
assert!(state.activate_endpoint_projection(&endpoint_id));