diff options
Diffstat (limited to 'src/client/shell')
| -rw-r--r-- | src/client/shell/endpoint_navigation.rs | 48 | ||||
| -rw-r--r-- | src/client/shell/mobile.rs | 12 | ||||
| -rw-r--r-- | src/client/shell/tests/endpoint_requests.rs | 40 | ||||
| -rw-r--r-- | src/client/shell/tests/endpoints.rs | 65 | ||||
| -rw-r--r-- | src/client/shell/tests/mobile.rs | 25 |
5 files changed, 156 insertions, 34 deletions
diff --git a/src/client/shell/endpoint_navigation.rs b/src/client/shell/endpoint_navigation.rs index 38ad0621..542bf3f9 100644 --- a/src/client/shell/endpoint_navigation.rs +++ b/src/client/shell/endpoint_navigation.rs @@ -35,23 +35,11 @@ impl ClientShellState { press: ClientWorkspacePress, outcome: &mut ClientShellInput, ) { - if press.endpoint_id == self.active_endpoint_id { - self.push_endpoint_method( - crate::api::schema::Method::WorkspaceFocus(crate::api::schema::WorkspaceTarget { - workspace_id: press.workspace_id, - }), - outcome, - ); - } else if self.endpoint_is_online(&press.endpoint_id) { - outcome.actions.push(ClientShellAction::ActivateEndpoint { - endpoint_id: press.endpoint_id, - target: Some(ClientEndpointFocusTarget::Workspace(press.workspace_id)), - }); - } else { - let label = self.endpoint_label(&press.endpoint_id).to_owned(); - self.receive_endpoint_unavailable(format!("{label} is not ready")); - outcome.repaint = true; - } + self.focus_or_activate( + press.endpoint_id, + ClientEndpointFocusTarget::Workspace(press.workspace_id), + outcome, + ); } pub(super) fn handle_endpoint_machine_click( @@ -68,12 +56,16 @@ impl ClientShellState { return false; }; let endpoint_id = hit.endpoint_id.clone(); - if super::contains(hit.collapse_toggle, point) || endpoint_id == self.active_endpoint_id { + let collapse_toggle = super::contains(hit.collapse_toggle, point); + if collapse_toggle || endpoint_id == self.active_endpoint_id { if !self.collapsed_endpoints.remove(&endpoint_id) { - self.collapsed_endpoints.insert(endpoint_id); + self.collapsed_endpoints.insert(endpoint_id.clone()); } outcome.repaint = true; - } else if self.endpoint_is_online(&endpoint_id) { + if !collapse_toggle && endpoint_id.is_local() { + self.activate_endpoint(endpoint_id, outcome); + } + } else if endpoint_id.is_local() || self.endpoint_is_online(&endpoint_id) { outcome.actions.push(ClientShellAction::ActivateEndpoint { endpoint_id, target: None, @@ -236,13 +228,16 @@ impl ClientShellState { endpoint_id: ClientEndpointId, outcome: &mut ClientShellInput, ) -> bool { - if !self.endpoint_is_online(&endpoint_id) { + let online = self.endpoint_is_online(&endpoint_id); + if !online && !endpoint_id.is_local() { let label = self.endpoint_label(&endpoint_id).to_owned(); self.receive_endpoint_unavailable(format!("{label} is not ready")); outcome.repaint = true; return false; } - if endpoint_id != self.active_endpoint_id { + if (endpoint_id.is_local() && (self.multi_endpoint_active() || !online)) + || endpoint_id != self.active_endpoint_id + { outcome.actions.push(ClientShellAction::ActivateEndpoint { endpoint_id, target: None, @@ -257,13 +252,18 @@ impl ClientShellState { target: ClientEndpointFocusTarget, outcome: &mut ClientShellInput, ) -> bool { - if !self.endpoint_is_online(&endpoint_id) { + let online = self.endpoint_is_online(&endpoint_id); + if !online && !endpoint_id.is_local() { let label = self.endpoint_label(&endpoint_id).to_owned(); self.receive_endpoint_unavailable(format!("{label} is not ready")); outcome.repaint = true; return false; } - if endpoint_id == self.active_endpoint_id { + // Local can still be displayed while a remote activation is pending. + // Route explicit selections through the runtime so they can cancel that handoff. + if endpoint_id == self.active_endpoint_id + && !(endpoint_id.is_local() && (self.multi_endpoint_active() || !online)) + { let method = match target { ClientEndpointFocusTarget::Workspace(workspace_id) => { crate::api::schema::Method::WorkspaceFocus( diff --git a/src/client/shell/mobile.rs b/src/client/shell/mobile.rs index 185bef51..8f06f54b 100644 --- a/src/client/shell/mobile.rs +++ b/src/client/shell/mobile.rs @@ -1000,16 +1000,12 @@ impl ClientShellState { if endpoint_id == self.active_endpoint_id { self.mode = ClientShellMode::Terminal; self.navigate_workspace_id = None; - } else if self.endpoint_is_online(&endpoint_id) { + if endpoint_id.is_local() { + self.activate_endpoint(endpoint_id, outcome); + } + } else if self.activate_endpoint(endpoint_id, outcome) { self.mode = ClientShellMode::Terminal; self.navigate_workspace_id = None; - outcome.actions.push(ClientShellAction::ActivateEndpoint { - endpoint_id, - target: None, - }); - } else { - let label = self.endpoint_label(&endpoint_id).to_owned(); - self.receive_endpoint_unavailable(format!("{label} is not ready")); } } Some(ClientMobileTarget::NewWorkspace) => { diff --git a/src/client/shell/tests/endpoint_requests.rs b/src/client/shell/tests/endpoint_requests.rs index b8805596..1f51f06c 100644 --- a/src/client/shell/tests/endpoint_requests.rs +++ b/src/client/shell/tests/endpoint_requests.rs @@ -259,6 +259,42 @@ impl crate::client::endpoint::EndpointTransport for TestTransport { } #[test] +fn local_selection_is_scheduled_ahead_of_a_full_event_queue() { + use crate::client::{ + endpoint::{EndpointNegotiation, EndpointRegistry}, + endpoint_commands::EndpointCommands, + ClientLoopEvent, + }; + let mut endpoints = EndpointRegistry::new( + TestTransport { fail: false }, + 1, + EndpointNegotiation::default(), + ); + let mut commands = EndpointCommands::default(); + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + tx.try_send(ClientLoopEvent::Timer).unwrap(); + let mut scheduled = None; + crate::client::shell_runtime::dispatch_client_shell_actions( + vec![ClientShellAction::ActivateEndpoint { + endpoint_id: ClientEndpointId::Local, + target: Some(ClientEndpointFocusTarget::Workspace("ws_1".into())), + }], + &mut commands, + &mut endpoints, + None, + &mut Vec::new(), + &mut scheduled, + ) + .unwrap(); + let next = scheduled.take().or_else(|| rx.try_recv().ok()); + assert!(matches!(next, Some(ClientLoopEvent::ActivateEndpoint { + endpoint_id: ClientEndpointId::Local, + target: Some(ClientEndpointFocusTarget::Workspace(id)), .. + }) if id == "ws_1")); + assert!(matches!(rx.try_recv(), Ok(ClientLoopEvent::Timer))); +} + +#[test] fn dispatcher_cancels_worktree_requests_on_frozen_surface_or_failed_send() { use crate::client::endpoint::{EndpointNegotiation, EndpointRegistry}; use crate::client::endpoint_commands::EndpointCommands; @@ -272,14 +308,14 @@ fn dispatcher_cancels_worktree_requests_on_frozen_surface_or_failed_send() { ); endpoints.set_surface_active(&ClientEndpointId::Local, fail_send); let mut commands = EndpointCommands::default(); - let (tx, _rx) = tokio::sync::mpsc::channel(16); + let mut scheduled = None; let (replay, repaint) = crate::client::shell_runtime::dispatch_client_shell_actions( actions, &mut commands, &mut endpoints, Some(&mut state), &mut Vec::new(), - &tx, + &mut scheduled, ) .unwrap(); assert!(repaint); diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs index de7abb3d..5c00e645 100644 --- a/src/client/shell/tests/endpoints.rs +++ b/src/client/shell/tests/endpoints.rs @@ -1398,6 +1398,71 @@ fn clicking_remote_machine_name_requests_activation_without_mutating_projection( } #[test] +fn clicking_local_can_cancel_a_remote_switch_while_local_is_still_displayed() { + for workspace in [false, true] { + let (mut state, remote) = state_with_remote(); + state.compose(100, 28).unwrap(); + let mut pending = ClientShellInput::default(); + assert!(state.activate_endpoint(remote, &mut pending)); + assert_eq!(state.active_endpoint_id, ClientEndpointId::Local); + let rect = if workspace { + state + .hits + .workspaces + .iter() + .find(|hit| hit.endpoint_id.is_local()) + .unwrap() + .rect + } else { + state + .hits + .machines + .iter() + .find(|hit| hit.endpoint_id.is_local()) + .unwrap() + .rect + }; + let outcome = state.handle_raw_events(vec![ + RawInputEvent::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: rect.x + 5, + row: rect.y, + modifiers: KeyModifiers::empty(), + }), + RawInputEvent::Mouse(MouseEvent { + kind: MouseEventKind::Up(MouseButton::Left), + column: rect.x + 5, + row: rect.y, + modifiers: KeyModifiers::empty(), + }), + ]); + assert!( + matches!(outcome.actions.as_slice(), [ClientShellAction::ActivateEndpoint { + endpoint_id: ClientEndpointId::Local, target, + }] if target.is_some() == workspace) + ); + } +} + +#[test] +fn reconnecting_local_selection_still_reaches_the_runtime() { + let (mut state, _) = state_with_remote(); + state.mark_endpoint_disconnected(&ClientEndpointId::Local); + let mut outcome = ClientShellInput::default(); + state.focus_or_activate( + ClientEndpointId::Local, + ClientEndpointFocusTarget::Workspace("local-workspace".into()), + &mut outcome, + ); + assert!( + matches!(outcome.actions.as_slice(), [ClientShellAction::ActivateEndpoint { + endpoint_id: ClientEndpointId::Local, + target: Some(ClientEndpointFocusTarget::Workspace(id)), + }] if id == "local-workspace") + ); +} + +#[test] fn machine_arrow_toggles_inactive_machine_without_switching() { for sidebar_collapsed in [false, true] { for status in [ diff --git a/src/client/shell/tests/mobile.rs b/src/client/shell/tests/mobile.rs index 0c6f1908..4dac5490 100644 --- a/src/client/shell/tests/mobile.rs +++ b/src/client/shell/tests/mobile.rs @@ -119,6 +119,31 @@ fn mobile_switcher_can_activate_an_online_saved_machine() { }] if activated == &endpoint_id )); + // Local is still projected until the remote handoff completes. + state.mode = ClientShellMode::Navigate; + state.compose(44, 30).unwrap(); + let local = state + .hits + .mobile_targets + .iter() + .find_map(|(rect, target)| { + matches!(target, ClientMobileTarget::Machine(ClientEndpointId::Local)).then_some(*rect) + }) + .unwrap(); + let outcome = state.handle_raw_events(vec![RawInputEvent::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: local.x, + row: local.y, + modifiers: KeyModifiers::empty(), + })]); + assert!(matches!( + outcome.actions.as_slice(), + [ClientShellAction::ActivateEndpoint { + endpoint_id: ClientEndpointId::Local, + target: None, + }] + )); + state.set_endpoint_status(&endpoint_id, ClientEndpointStatus::Online); assert!(state.activate_endpoint_projection(&endpoint_id)); let mut remote_surface = surface(); |
