aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell_runtime.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/shell_runtime.rs')
-rw-r--r--src/client/shell_runtime.rs739
1 files changed, 739 insertions, 0 deletions
diff --git a/src/client/shell_runtime.rs b/src/client/shell_runtime.rs
new file mode 100644
index 00000000..8a4af0b7
--- /dev/null
+++ b/src/client/shell_runtime.rs
@@ -0,0 +1,739 @@
+use super::*;
+
+pub(super) fn dispatch_client_shell_actions(
+ actions: Vec<shell::ClientShellAction>,
+ endpoint_commands: &mut endpoint_commands::EndpointCommands,
+ endpoints: &mut endpoint::EndpointRegistry,
+ mut shell: Option<&mut shell::ClientShellState>,
+ detached_process_children: &mut Vec<std::process::Child>,
+ event_tx: &tokio::sync::mpsc::Sender<ClientLoopEvent>,
+) -> Result<(Vec<crossterm::event::MouseEvent>, bool), ClientError> {
+ let mut replay_mouse = Vec::new();
+ let mut repaint = false;
+ for action in actions {
+ match action {
+ shell::ClientShellAction::Endpoint {
+ endpoint_id,
+ boot_id,
+ request,
+ } => {
+ if let Some(connection) = endpoints.connection(&endpoint_id).filter(|_| {
+ endpoints.active_id() == &endpoint_id && endpoints.active_surface_available()
+ }) {
+ endpoint_commands.enqueue(endpoint_id, connection.generation, boot_id, request);
+ } else if let Some(shell) = shell.as_deref_mut() {
+ repaint |= shell.cancel_endpoint_request(&request.id);
+ }
+ }
+ shell::ClientShellAction::ClipboardWrite(bytes) => {
+ crate::selection::write_osc52_bytes(&bytes);
+ }
+ shell::ClientShellAction::Request(request) => {
+ if endpoints.active_surface_available() {
+ write_to_server(endpoints, &request).map_err(ClientError::ConnectionLost)?;
+ }
+ }
+ shell::ClientShellAction::ActivateEndpoint {
+ endpoint_id,
+ target,
+ } => {
+ let _ = event_tx.try_send(ClientLoopEvent::ActivateEndpoint {
+ endpoint_id,
+ target,
+ force: false,
+ });
+ }
+ shell::ClientShellAction::OpenSafeWebUrl(url) => {
+ if crate::app::actions::safe_web_url(&url).is_some() {
+ match crate::platform::open_url(&url) {
+ Ok(Some(child)) => detached_process_children.push(child),
+ Ok(None) => {}
+ Err(err) => warn!(err = %err, url = %url, "failed to open pane URL"),
+ }
+ }
+ }
+ shell::ClientShellAction::ReplayMouse(events) => replay_mouse.extend(events),
+ shell::ClientShellAction::Keybind(action) => {
+ debug!(
+ ?action,
+ "client shell action awaits its presentation family"
+ );
+ }
+ }
+ }
+ // A source-off-first handoff leaves the registry's committed identity pointing at a
+ // deliberately surface-inactive source. Do not drain its retained queue into a server that
+ // must reject it; completion below resumes the committed owner's lane.
+ if endpoints.active_surface_available() {
+ let active_endpoint = endpoints.active_id().clone();
+ let cancelled = endpoint_commands.send_next(&active_endpoint, endpoints);
+ if let Some(shell) = shell {
+ for request_id in cancelled {
+ repaint |= shell.cancel_endpoint_request(&request_id);
+ }
+ }
+ }
+ Ok((replay_mouse, repaint))
+}
+
+pub(super) fn client_shell_resize_message(
+ shell: &shell::ClientShellState,
+ cols: u16,
+ rows: u16,
+ cell_width_px: u32,
+ cell_height_px: u32,
+ pixel_mouse: bool,
+) -> ClientMessage {
+ ClientMessage::ClientShellResize {
+ cell_width_px,
+ cell_height_px,
+ surface_size: shell.surface_size(cols, rows),
+ pixel_mouse,
+ }
+}
+
+pub(super) fn sync_client_shell_keyboard_report_all(
+ state: &mut ClientState,
+) -> Result<(), ClientError> {
+ let Some(shell) = state.shell.as_ref() else {
+ return Ok(());
+ };
+ let desired = state.pane_keyboard_report_all || shell.host_keyboard_report_all_requested();
+ if desired == state.keyboard_report_all_active {
+ return Ok(());
+ }
+ crate::terminal_modes::set_host_kitty_keyboard_report_all(&mut io::stdout(), desired)
+ .map_err(ClientError::ConnectionFailed)?;
+ state.keyboard_report_all_active = desired;
+ Ok(())
+}
+
+pub(super) fn clear_endpoint_host_effects(
+ state: &mut ClientState,
+ host_mouse_capture_active: &std::sync::atomic::AtomicBool,
+ host_sgr_pixels_active: &std::sync::atomic::AtomicBool,
+) {
+ state.endpoint_mouse_capture_requested = false;
+ state.endpoint_sgr_pixels_requested = false;
+ let enabled = if state.shell.is_some() {
+ state.shell_mouse_capture_preference
+ } else {
+ state.direct_mouse_capture_preference
+ };
+ let sgr_pixels = super::effective_sgr_pixel_mouse(enabled, false, state.pixel_geometry_exact);
+ if enabled != state.mouse_capture_active
+ || sgr_pixels != host_sgr_pixels_active.load(std::sync::atomic::Ordering::Acquire)
+ {
+ let _ = super::set_mouse_capture(enabled, sgr_pixels);
+ }
+ state.mouse_capture_active = enabled;
+ host_mouse_capture_active.store(enabled, std::sync::atomic::Ordering::Release);
+ host_sgr_pixels_active.store(sgr_pixels, std::sync::atomic::Ordering::Release);
+
+ state.pane_keyboard_report_all = false;
+ let _ = sync_client_shell_keyboard_report_all(state);
+ let _ = crate::terminal_effects::write_window_title(&mut std::io::stdout(), None);
+}
+
+pub(super) fn apply_client_shell_input_source_changes(
+ state: &mut ClientState,
+ prefix_input_source: &mut impl crate::platform::PrefixInputSource,
+) {
+ let changes = state
+ .shell
+ .as_mut()
+ .map(shell::ClientShellState::take_input_source_changes)
+ .unwrap_or_default();
+ for active in changes {
+ if active {
+ prefix_input_source.switch_to_ascii();
+ } else {
+ prefix_input_source.restore();
+ }
+ }
+}
+
+fn install_pending_activation(
+ state: &mut ClientState,
+ endpoint_commands: &mut endpoint_commands::EndpointCommands,
+ pending: &mut Option<endpoint::PendingEndpointActivation>,
+ next_surface_serial: &mut u64,
+ activation: endpoint::PendingEndpointActivation,
+) {
+ let retired = activation
+ .source_command_lane()
+ .map(|source| endpoint_commands.retire_lane(source))
+ .unwrap_or_default();
+ if let Some(shell) = state.shell.as_mut() {
+ for request_id in retired {
+ shell.cancel_endpoint_request(&request_id);
+ }
+ }
+ *next_surface_serial = next_surface_serial.saturating_add(1);
+ state.freeze_presentation();
+ *pending = Some(activation);
+}
+
+pub(super) fn begin_endpoint_activation(
+ state: &mut ClientState,
+ endpoints: &mut endpoint::EndpointRegistry,
+ endpoint_commands: &mut endpoint_commands::EndpointCommands,
+ pending: &mut Option<endpoint::PendingEndpointActivation>,
+ next_surface_serial: &mut u64,
+ endpoint_id: endpoint::ClientEndpointId,
+ target: Option<shell::ClientEndpointFocusTarget>,
+ force: bool,
+ now: std::time::Instant,
+ event_tx: &tokio::sync::mpsc::Sender<ClientLoopEvent>,
+) -> Result<(), ClientError> {
+ if let Some(activation) = pending.as_mut() {
+ if activation.can_retarget(&endpoint_id) {
+ let retarget_error = activation.retarget(target, endpoints).err();
+ if let Some(error) = retarget_error {
+ rollback_endpoint_activation(state, endpoints, pending, error, false);
+ }
+ } else {
+ // Once rollback starts, even a request for the original target is a new intent. It
+ // replaces the retained successor instead of mutating the transaction being retired.
+ let outcome = activation.supersede(endpoint_id, target, endpoints);
+ if let endpoint::ActivationRollback::Unavailable(message) = outcome {
+ *pending = None;
+ present_handoff_unavailable(state, message);
+ }
+ }
+ return Ok(());
+ }
+ let already_active = !force
+ && endpoints.active_id() == &endpoint_id
+ && endpoints
+ .connection(&endpoint_id)
+ .is_some_and(|connection| connection.surface_active);
+ if already_active {
+ if let (Some(shell), Some(target)) = (state.shell.as_mut(), target) {
+ let actions = shell.focus_endpoint_target(target);
+ let (_, repaint) = dispatch_client_shell_actions(
+ actions,
+ endpoint_commands,
+ endpoints,
+ Some(shell),
+ &mut state.detached_process_children,
+ event_tx,
+ )?;
+ if repaint {
+ if let Some(frame) = shell.compose(state.reported_size.0, state.reported_size.1) {
+ state.present_frame(frame);
+ }
+ }
+ }
+ return Ok(());
+ }
+ let Some(shell) = state.shell.as_ref() else {
+ return Ok(());
+ };
+ let resize = client_shell_resize_message(
+ shell,
+ state.reported_size.0,
+ state.reported_size.1,
+ state.reported_cell_size.0,
+ state.reported_cell_size.1,
+ state.pixel_geometry_exact,
+ );
+ match endpoint::PendingEndpointActivation::begin(
+ shell,
+ endpoints,
+ endpoint_id.clone(),
+ target,
+ resize,
+ *next_surface_serial,
+ now,
+ ) {
+ Ok(activation) => install_pending_activation(
+ state,
+ endpoint_commands,
+ pending,
+ next_surface_serial,
+ activation,
+ ),
+ Err(endpoint::ActivationBeginError::Preflight(error)) => {
+ if let Some(shell) = state.shell.as_mut() {
+ shell.receive_endpoint_unavailable(format!(
+ "{}: {error}",
+ shell.endpoint_label(&endpoint_id)
+ ));
+ }
+ }
+ Err(endpoint::ActivationBeginError::Partial { activation, error }) => {
+ // A send error is not evidence that its peer did not observe the write. Freeze and
+ // retain the lifecycle object before rollback so no source or target output can be
+ // projected until one ownership path has been proved again.
+ install_pending_activation(
+ state,
+ endpoint_commands,
+ pending,
+ next_surface_serial,
+ *activation,
+ );
+ rollback_endpoint_activation(
+ state,
+ endpoints,
+ pending,
+ format!(
+ "{}: {error}",
+ state
+ .shell
+ .as_ref()
+ .map(|shell| shell.endpoint_label(&endpoint_id).to_owned())
+ .unwrap_or_else(|| format!("{endpoint_id:?}"))
+ ),
+ false,
+ );
+ }
+ }
+ Ok(())
+}
+
+pub(super) fn complete_endpoint_activation(
+ state: &mut ClientState,
+ endpoints: &mut endpoint::EndpointRegistry,
+ pending: &mut Option<endpoint::PendingEndpointActivation>,
+ endpoint_commands: &mut endpoint_commands::EndpointCommands,
+) -> Result<Option<ClientLoopEvent>, ClientError> {
+ let sync_endpoint = pending
+ .as_ref()
+ .and_then(endpoint::PendingEndpointActivation::presentation_sync_endpoint)
+ .cloned();
+ if let Some(endpoint_id) = sync_endpoint.as_ref() {
+ state.replay_host_theme(endpoints, endpoint_id);
+ }
+ let completion = {
+ let Some(activation) = pending.as_mut() else {
+ return Ok(None);
+ };
+ let Some(shell) = state.shell.as_mut() else {
+ return Ok(None);
+ };
+ match activation.complete(shell, endpoints) {
+ Ok(completion) => completion,
+ Err(error) => {
+ shell.receive_endpoint_unavailable(error);
+ return Ok(None);
+ }
+ }
+ };
+
+ if matches!(
+ completion,
+ endpoint::ActivationCompletion::AwaitingPresentationSync { .. }
+ ) {
+ #[cfg(unix)]
+ if let endpoint::ActivationCompletion::AwaitingPresentationSync { previous, endpoint } =
+ &completion
+ {
+ if previous != endpoint {
+ state.retire_endpoint_graphics(previous);
+ }
+ }
+ // The coherent target frame can replace the frozen source now, but the registry keeps
+ // pane input disabled until a second projection epoch has replayed host modes/effects.
+ state.unfreeze_presentation();
+ let (cleanup, frame) = {
+ let shell = state.shell.as_mut().expect("checked client shell");
+ (
+ shell.take_pending_graphics_cleanup(),
+ shell.compose(state.reported_size.0, state.reported_size.1),
+ )
+ };
+ state.present_graphics(&cleanup);
+ if let Some(frame) = frame {
+ state.present_frame(frame);
+ }
+ return Ok(None);
+ }
+ if completion == endpoint::ActivationCompletion::AwaitingPresentationEffects {
+ return Ok(None);
+ }
+
+ let _ = pending.take();
+ endpoints.unfreeze_input();
+ let successor = match completion {
+ endpoint::ActivationCompletion::RestoredSource {
+ error,
+ successor: next,
+ ..
+ } => {
+ if next.is_none() {
+ if let Some(shell) = state.shell.as_mut() {
+ shell.receive_endpoint_unavailable(error);
+ }
+ }
+ next
+ }
+ endpoint::ActivationCompletion::Activated => None,
+ endpoint::ActivationCompletion::AwaitingPresentationSync { .. }
+ | endpoint::ActivationCompletion::AwaitingPresentationEffects => unreachable!(),
+ };
+ state.unfreeze_presentation();
+ if successor.is_none() {
+ let active_endpoint = endpoints.active_id().clone();
+ let cancelled = endpoint_commands.send_next(&active_endpoint, endpoints);
+ if let Some(shell) = state.shell.as_mut() {
+ for request_id in cancelled {
+ shell.cancel_endpoint_request(&request_id);
+ }
+ }
+ }
+ let (cleanup, frame) = {
+ let shell = state.shell.as_mut().expect("checked client shell");
+ (
+ shell.take_pending_graphics_cleanup(),
+ shell.compose(state.reported_size.0, state.reported_size.1),
+ )
+ };
+ state.present_graphics(&cleanup);
+ if let Some(frame) = frame {
+ state.present_frame(frame);
+ }
+ if let Some(intent) = successor {
+ return Ok(Some(ClientLoopEvent::ActivateEndpoint {
+ endpoint_id: intent.endpoint_id,
+ target: intent.target,
+ force: true,
+ }));
+ }
+ Ok(None)
+}
+
+pub(super) fn present_handoff_unavailable(state: &mut ClientState, message: String) {
+ // An unavailable committed endpoint has no presentation lease. Keep all pane input and late
+ // source output blocked, while allowing this client-owned chrome frame through the freeze.
+ state.freeze_presentation();
+ let frame = state.shell.as_mut().and_then(|shell| {
+ shell.receive_endpoint_unavailable(message);
+ shell.compose(state.reported_size.0, state.reported_size.1)
+ });
+ if let Some(frame) = frame {
+ state.present_frozen_chrome(frame);
+ }
+}
+
+pub(super) fn rollback_endpoint_activation(
+ state: &mut ClientState,
+ endpoints: &mut endpoint::EndpointRegistry,
+ pending: &mut Option<endpoint::PendingEndpointActivation>,
+ error: String,
+ source_release_rejected: bool,
+) {
+ let Some(activation) = pending.as_mut() else {
+ return;
+ };
+ match activation.rollback(endpoints, error.clone(), source_release_rejected) {
+ endpoint::ActivationRollback::Pending => state.freeze_presentation(),
+ endpoint::ActivationRollback::Unavailable(message) => {
+ *pending = None;
+ // No endpoint has been proven safe to present. Keep pane input frozen, but render
+ // the client-owned unavailable chrome rather than silently swallowing the error.
+ present_handoff_unavailable(state, message);
+ }
+ }
+}
+
+pub(super) fn handle_endpoint_disconnect(
+ state: &mut ClientState,
+ endpoints: &mut endpoint::EndpointRegistry,
+ endpoint_commands: &mut endpoint_commands::EndpointCommands,
+ supervisors: &mut endpoint::EndpointSupervisors,
+ pending_activation: &mut Option<endpoint::PendingEndpointActivation>,
+ endpoint_id: &endpoint::ClientEndpointId,
+ generation: u64,
+ now: std::time::Instant,
+ notice: &str,
+) -> bool {
+ supervisors.disconnected(endpoint_id, generation, now);
+ #[cfg(unix)]
+ state.retire_endpoint_graphics(endpoint_id);
+ if pending_activation
+ .as_ref()
+ .is_some_and(|pending| pending.involves_endpoint(endpoint_id))
+ {
+ let outcome = pending_activation
+ .as_mut()
+ .expect("checked pending activation")
+ .endpoint_disconnected(
+ endpoints,
+ endpoint_id,
+ format!("endpoint connection was lost while activating {notice}"),
+ );
+ match outcome {
+ endpoint::ActivationRollback::Pending => {}
+ endpoint::ActivationRollback::Unavailable(error) => {
+ *pending_activation = None;
+ present_handoff_unavailable(state, error);
+ }
+ }
+ }
+ let endpoint_was_active = endpoints.active_id() == endpoint_id;
+ let cancelled = endpoint_commands.disconnect(endpoint_id);
+ let unavailable = state.shell.as_mut().and_then(|shell| {
+ for request_id in cancelled {
+ shell.cancel_endpoint_request(&request_id);
+ }
+ shell.mark_endpoint_disconnected(endpoint_id);
+ endpoint_was_active.then(|| format!("{} {notice}", shell.endpoint_label(endpoint_id)))
+ });
+ if let Some(message) = unavailable {
+ present_handoff_unavailable(state, message);
+ } else if let Some(frame) = state
+ .shell
+ .as_mut()
+ .and_then(|shell| shell.compose(state.reported_size.0, state.reported_size.1))
+ {
+ state.present_frame(frame);
+ }
+ endpoint_was_active
+}
+
+pub(super) fn handle_endpoint_attention(
+ state: &mut ClientState,
+ endpoints: &mut endpoint::EndpointRegistry,
+ endpoint_commands: &mut endpoint_commands::EndpointCommands,
+ supervisors: &mut endpoint::EndpointSupervisors,
+ pending_activation: &mut Option<endpoint::PendingEndpointActivation>,
+ endpoint_id: &endpoint::ClientEndpointId,
+ generation: u64,
+ now: std::time::Instant,
+ message: String,
+) -> bool {
+ endpoints.disconnect(endpoint_id);
+ supervisors.record_status(
+ endpoint_id,
+ generation,
+ endpoint::ClientEndpointStatus::Attention,
+ now,
+ );
+ #[cfg(unix)]
+ state.retire_endpoint_graphics(endpoint_id);
+ if pending_activation
+ .as_ref()
+ .is_some_and(|pending| pending.involves_endpoint(endpoint_id))
+ {
+ let outcome = pending_activation
+ .as_mut()
+ .expect("checked pending activation")
+ .endpoint_disconnected(
+ endpoints,
+ endpoint_id,
+ "endpoint reported attention while activating".into(),
+ );
+ if let endpoint::ActivationRollback::Unavailable(error) = outcome {
+ *pending_activation = None;
+ present_handoff_unavailable(state, error);
+ }
+ }
+ let endpoint_was_active = endpoints.active_id() == endpoint_id;
+ let cancelled = endpoint_commands.disconnect(endpoint_id);
+ let unavailable = state.shell.as_mut().and_then(|shell| {
+ for request_id in cancelled {
+ shell.cancel_endpoint_request(&request_id);
+ }
+ shell.set_endpoint_status(endpoint_id, endpoint::ClientEndpointStatus::Attention);
+ endpoint_was_active.then(|| format!("{}: {message}", shell.endpoint_label(endpoint_id)))
+ });
+ if let Some(message) = unavailable {
+ present_handoff_unavailable(state, message);
+ } else if let Some(frame) = state
+ .shell
+ .as_mut()
+ .and_then(|shell| shell.compose(state.reported_size.0, state.reported_size.1))
+ {
+ state.present_frame(frame);
+ }
+ endpoint_was_active
+}
+
+pub(super) fn install_client_shell_snapshot(
+ state: &mut ClientState,
+ endpoint_id: &endpoint::ClientEndpointId,
+ snapshot: Box<crate::protocol::ClientShellSnapshot>,
+ projection_pending: bool,
+ endpoints: &mut endpoint::EndpointRegistry,
+ prefix_input_source: &mut impl crate::platform::PrefixInputSource,
+) -> Result<(), ClientError> {
+ let Some(connection) = endpoints.connection(endpoint_id) else {
+ return Ok(());
+ };
+ let generation = connection.generation;
+ let project_snapshot =
+ !projection_pending && endpoints.active_id() == endpoint_id && connection.surface_active;
+ let (composed, resize, graphics_cleanup) = if let Some(shell) = &mut state.shell {
+ let waits_for_selected_surface = projection_pending
+ || (endpoints.active_id() == endpoint_id
+ && !project_snapshot
+ && shell.has_presented_surface());
+ let previous_size = shell.surface_size(state.reported_size.0, state.reported_size.1);
+ if !waits_for_selected_surface {
+ shell.set_endpoint_status(endpoint_id, endpoint::ClientEndpointStatus::Online);
+ }
+ if project_snapshot {
+ shell.set_endpoint_snapshot_for_generation(endpoint_id, generation, snapshot);
+ } else {
+ shell.cache_endpoint_snapshot_inactive_for_generation(
+ endpoint_id,
+ generation,
+ snapshot,
+ );
+ }
+ let graphics_cleanup = shell.take_pending_graphics_cleanup();
+ let next_size = shell.surface_size(state.reported_size.0, state.reported_size.1);
+ (
+ shell.compose(state.reported_size.0, state.reported_size.1),
+ (previous_size != next_size).then(|| {
+ client_shell_resize_message(
+ shell,
+ state.reported_size.0,
+ state.reported_size.1,
+ state.reported_cell_size.0,
+ state.reported_cell_size.1,
+ state.pixel_geometry_exact,
+ )
+ }),
+ graphics_cleanup,
+ )
+ } else {
+ (None, None, Vec::new())
+ };
+ apply_client_shell_input_source_changes(state, prefix_input_source);
+ state.present_graphics(&graphics_cleanup);
+ if let Some(resize) = resize {
+ endpoints.send_to(endpoint_id, &resize);
+ }
+ if let Some(frame) = composed {
+ if projection_pending {
+ state.present_frame(frame);
+ } else {
+ state.present_frozen_chrome(frame);
+ }
+ }
+ Ok(())
+}
+
+pub(super) fn finish_client_shell_input(
+ state: &mut ClientState,
+ outcome: shell::ClientShellInput,
+ frame: Option<FrameData>,
+ endpoints: &mut endpoint::EndpointRegistry,
+ pending_activation: &mut Option<endpoint::PendingEndpointActivation>,
+ endpoint_commands: &mut endpoint_commands::EndpointCommands,
+ prefix_input_source: &mut impl crate::platform::PrefixInputSource,
+ event_tx: &tokio::sync::mpsc::Sender<ClientLoopEvent>,
+) -> Result<bool, ClientError> {
+ apply_client_shell_input_source_changes(state, prefix_input_source);
+ if outcome.detach {
+ let _ = write_to_server(endpoints, &ClientMessage::Detach);
+ return Ok(true);
+ }
+ if outcome.resize {
+ let shell = state.shell.as_ref().expect("shell mode remains active");
+ let resize = client_shell_resize_message(
+ shell,
+ state.reported_size.0,
+ state.reported_size.1,
+ state.reported_cell_size.0,
+ state.reported_cell_size.1,
+ state.pixel_geometry_exact,
+ );
+ if let Some(activation) = pending_activation.as_mut() {
+ if let Err(error) = activation.update_resize(resize, endpoints) {
+ rollback_endpoint_activation(state, endpoints, pending_activation, error, false);
+ }
+ } else {
+ let _ = write_to_server(endpoints, &resize);
+ }
+ }
+ #[cfg(not(windows))]
+ if outcome.query_host_appearance {
+ query_host_terminal_appearance();
+ }
+ if outcome.query_host_theme {
+ query_host_terminal_theme();
+ }
+ sync_client_shell_keyboard_report_all(state)?;
+ let (replay, dispatch_repaint) = dispatch_client_shell_actions(
+ outcome.actions,
+ endpoint_commands,
+ endpoints,
+ state.shell.as_mut(),
+ &mut state.detached_process_children,
+ event_tx,
+ )?;
+ let frame = if dispatch_repaint {
+ state
+ .shell
+ .as_mut()
+ .and_then(|shell| shell.compose(state.reported_size.0, state.reported_size.1))
+ } else {
+ frame
+ };
+ debug_assert!(
+ replay.is_empty(),
+ "mouse replay only follows endpoint results"
+ );
+ let active_endpoint_online = state
+ .shell
+ .as_ref()
+ .is_none_or(|shell| shell.endpoint_is_online(endpoints.active_id()))
+ && endpoints.active_surface_available();
+ for request in outcome.requests {
+ if let ClientMessage::ClientShellHostTheme { update } = &request {
+ state.record_host_theme_update(update);
+ if let Some(activation) = pending_activation.as_mut() {
+ if let Err(error) = activation.update_host_theme(update.clone(), endpoints) {
+ rollback_endpoint_activation(
+ state,
+ endpoints,
+ pending_activation,
+ error,
+ false,
+ );
+ }
+ continue;
+ }
+ }
+ // Host focus belongs to a pending target even when the source has gone offline or has
+ // already had its surface revoked. Route it before the ordinary source-online gate.
+ if let ClientMessage::ClientShellFocus { focused } = request {
+ if let Some(activation) = pending_activation.as_mut() {
+ if let Err(error) = activation.update_host_focus(focused, endpoints) {
+ rollback_endpoint_activation(
+ state,
+ endpoints,
+ pending_activation,
+ error,
+ false,
+ );
+ }
+ continue;
+ }
+ if active_endpoint_online {
+ write_to_server(endpoints, &ClientMessage::ClientShellFocus { focused })
+ .map_err(ClientError::ConnectionLost)?;
+ }
+ continue;
+ }
+ if !active_endpoint_online {
+ continue;
+ }
+ if pending_activation.is_some() {
+ // Pane input and non-focus host effects do not cross the frozen handoff boundary.
+ continue;
+ }
+ write_to_server(endpoints, &request).map_err(ClientError::ConnectionLost)?;
+ }
+ if let Some(frame) = frame {
+ if pending_activation.is_some() {
+ state.present_frame(frame);
+ } else {
+ state.present_frozen_chrome(frame);
+ }
+ }
+ Ok(false)
+}