aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/shell')
-rw-r--r--src/client/shell/endpoint_navigation.rs48
-rw-r--r--src/client/shell/mobile.rs12
-rw-r--r--src/client/shell/tests/endpoint_requests.rs40
-rw-r--r--src/client/shell/tests/endpoints.rs65
-rw-r--r--src/client/shell/tests/mobile.rs25
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();