#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HandoffState {
Idle,
Previewing,
Settling,
RemoteRendering,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageSource {
Local,
Remote,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HandoffEvent {
OrientationChanged,
SettleElapsed { worker_available: bool },
RemoteStreamStarted,
RemoteDone,
RemoteFailed,
SceneInvalidated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HandoffAction {
DiscardLocalPreview,
SendRenderRequestToWorker,
SendCancelToWorker,
DiscardRemotePartial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HandoffMachine {
state: HandoffState,
served_by: ImageSource,
}
impl Default for HandoffMachine {
fn default() -> Self {
Self::new()
}
}
impl HandoffMachine {
#[must_use]
pub const fn new() -> Self {
Self {
state: HandoffState::Idle,
served_by: ImageSource::Local,
}
}
#[must_use]
pub const fn state(self) -> HandoffState {
self.state
}
#[must_use]
pub const fn served_by(self) -> ImageSource {
self.served_by
}
pub fn handle(&mut self, event: HandoffEvent) -> Vec<HandoffAction> {
use HandoffAction::{
DiscardLocalPreview, DiscardRemotePartial, SendCancelToWorker,
SendRenderRequestToWorker,
};
use HandoffEvent::{
OrientationChanged, RemoteDone, RemoteFailed, RemoteStreamStarted, SettleElapsed,
};
use HandoffState::{Cancelled, Idle, Previewing, RemoteRendering, Settling};
let (next_state, actions) = match (self.state, event) {
(Idle | Cancelled | Previewing, OrientationChanged) => (Previewing, vec![]),
(
Previewing,
SettleElapsed {
worker_available: true,
},
) => (
Settling,
vec![DiscardLocalPreview, SendRenderRequestToWorker],
),
(
Previewing,
SettleElapsed {
worker_available: false,
},
) => {
(Idle, vec![])
}
(Settling, RemoteStreamStarted) => (RemoteRendering, vec![]),
(Settling | RemoteRendering, OrientationChanged) => {
(Cancelled, vec![SendCancelToWorker, DiscardRemotePartial])
}
(RemoteRendering, RemoteDone) | (Settling | RemoteRendering, RemoteFailed) => {
(Idle, vec![])
}
(state, _) => (state, vec![]),
};
self.state = next_state;
if matches!(event, HandoffEvent::RemoteDone) && next_state == HandoffState::Idle {
self.served_by = ImageSource::Remote;
} else if matches!(event, HandoffEvent::OrientationChanged)
&& next_state == HandoffState::Previewing
{
self.served_by = ImageSource::Local;
} else if matches!(event, HandoffEvent::SceneInvalidated) {
self.served_by = ImageSource::Local;
}
actions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_idle_and_local() {
let m = HandoffMachine::new();
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(m.served_by(), ImageSource::Local);
}
#[test]
fn dragging_from_idle_enters_previewing_with_no_actions() {
let mut m = HandoffMachine::new();
let actions = m.handle(HandoffEvent::OrientationChanged);
assert_eq!(m.state(), HandoffState::Previewing);
assert_eq!(actions, Vec::new());
assert_eq!(m.served_by(), ImageSource::Local);
}
#[test]
fn continuing_to_drag_stays_in_previewing() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
for _ in 0..5 {
let actions = m.handle(HandoffEvent::OrientationChanged);
assert_eq!(m.state(), HandoffState::Previewing);
assert_eq!(actions, Vec::new());
}
}
#[test]
fn settling_with_no_worker_falls_back_to_idle_with_no_actions() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
let actions = m.handle(HandoffEvent::SettleElapsed {
worker_available: false,
});
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(actions, Vec::new());
assert_eq!(m.served_by(), ImageSource::Local);
}
#[test]
fn settling_with_a_worker_discards_the_local_preview_and_dispatches() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
let actions = m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
assert_eq!(m.state(), HandoffState::Settling);
assert_eq!(
actions,
vec![
HandoffAction::DiscardLocalPreview,
HandoffAction::SendRenderRequestToWorker,
]
);
}
#[test]
fn the_full_happy_path_ends_remote_and_idle() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
let started_actions = m.handle(HandoffEvent::RemoteStreamStarted);
assert_eq!(m.state(), HandoffState::RemoteRendering);
assert_eq!(started_actions, Vec::new());
let done_actions = m.handle(HandoffEvent::RemoteDone);
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(done_actions, Vec::new());
assert_eq!(
m.served_by(),
ImageSource::Remote,
"a normally-completed remote render must be reflected in served_by"
);
}
#[test]
fn resuming_the_drag_mid_remote_render_cancels_and_discards() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
m.handle(HandoffEvent::RemoteStreamStarted);
assert_eq!(m.state(), HandoffState::RemoteRendering);
let cancel_actions = m.handle(HandoffEvent::OrientationChanged);
assert_eq!(m.state(), HandoffState::Cancelled);
assert_eq!(
cancel_actions,
vec![
HandoffAction::SendCancelToWorker,
HandoffAction::DiscardRemotePartial,
]
);
assert_eq!(m.served_by(), ImageSource::Local);
let resume_actions = m.handle(HandoffEvent::OrientationChanged);
assert_eq!(m.state(), HandoffState::Previewing);
assert_eq!(resume_actions, Vec::new());
}
#[test]
fn resuming_the_drag_during_settling_also_cancels_and_discards() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
assert_eq!(m.state(), HandoffState::Settling);
let actions = m.handle(HandoffEvent::OrientationChanged);
assert_eq!(m.state(), HandoffState::Cancelled);
assert_eq!(
actions,
vec![
HandoffAction::SendCancelToWorker,
HandoffAction::DiscardRemotePartial,
]
);
}
#[test]
fn a_failed_connection_during_settling_falls_back_to_idle() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
let actions = m.handle(HandoffEvent::RemoteFailed);
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(actions, Vec::new());
assert_eq!(m.served_by(), ImageSource::Local);
}
#[test]
fn a_failure_mid_stream_falls_back_to_idle_and_leaves_served_by_local() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
m.handle(HandoffEvent::RemoteStreamStarted);
let actions = m.handle(HandoffEvent::RemoteFailed);
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(actions, Vec::new());
assert_eq!(m.served_by(), ImageSource::Local);
}
#[test]
fn a_second_drag_after_a_completed_remote_render_starts_a_fresh_cycle() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
m.handle(HandoffEvent::RemoteStreamStarted);
m.handle(HandoffEvent::RemoteDone);
assert_eq!(m.served_by(), ImageSource::Remote);
m.handle(HandoffEvent::OrientationChanged);
assert_eq!(m.state(), HandoffState::Previewing);
assert_eq!(
m.served_by(),
ImageSource::Local,
"a fresh drag must flip the displayed-source indicator back to Local \
immediately, not wait for the next remote completion"
);
}
#[test]
fn scene_invalidated_after_a_completed_remote_render_flips_served_by_to_local() {
let mut m = HandoffMachine::new();
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
m.handle(HandoffEvent::RemoteStreamStarted);
m.handle(HandoffEvent::RemoteDone);
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(m.served_by(), ImageSource::Remote);
let actions = m.handle(HandoffEvent::SceneInvalidated);
assert_eq!(
m.state(),
HandoffState::Idle,
"a non-drag scene change must not move the handoff state machine at all"
);
assert_eq!(actions, Vec::new());
assert_eq!(
m.served_by(),
ImageSource::Local,
"served_by must stop claiming Remote once local tracing has taken back over"
);
}
#[test]
fn scene_invalidated_elsewhere_is_a_harmless_no_op() {
let mut m = HandoffMachine::new();
let actions = m.handle(HandoffEvent::SceneInvalidated);
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(actions, Vec::new());
assert_eq!(m.served_by(), ImageSource::Local);
m.handle(HandoffEvent::OrientationChanged);
m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
m.handle(HandoffEvent::RemoteStreamStarted);
assert_eq!(m.state(), HandoffState::RemoteRendering);
let actions = m.handle(HandoffEvent::SceneInvalidated);
assert_eq!(
m.state(),
HandoffState::RemoteRendering,
"SceneInvalidated must not interrupt an in-flight remote render -- only a \
real OrientationChanged (via SendCancelToWorker) does that"
);
assert_eq!(actions, Vec::new());
assert_eq!(m.served_by(), ImageSource::Local);
}
#[test]
fn stray_events_in_unexpected_states_are_harmless_no_ops() {
let mut m = HandoffMachine::new();
assert_eq!(m.handle(HandoffEvent::RemoteDone), Vec::new());
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(m.handle(HandoffEvent::RemoteFailed), Vec::new());
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(m.handle(HandoffEvent::RemoteStreamStarted), Vec::new());
assert_eq!(m.state(), HandoffState::Idle);
assert_eq!(
m.handle(HandoffEvent::SettleElapsed {
worker_available: true
}),
Vec::new()
);
assert_eq!(m.state(), HandoffState::Idle);
}
#[test]
fn every_state_and_event_combination_terminates_without_panicking() {
let events = [
HandoffEvent::OrientationChanged,
HandoffEvent::SettleElapsed {
worker_available: true,
},
HandoffEvent::SettleElapsed {
worker_available: false,
},
HandoffEvent::RemoteStreamStarted,
HandoffEvent::RemoteDone,
HandoffEvent::RemoteFailed,
HandoffEvent::SceneInvalidated,
];
for &event in &events {
let mut m = HandoffMachine::new();
m.handle(event); m.handle(HandoffEvent::OrientationChanged);
m.handle(event); m.handle(HandoffEvent::SettleElapsed {
worker_available: true,
});
m.handle(event); m.handle(HandoffEvent::RemoteStreamStarted);
m.handle(event); }
}
}