use crate::action::{ActionEnvelope, UpdateTextInput};
use crate::async_runtime::{
JobRef, JobRequestPayload, JobSpec, ResourceExecutionContext, ServiceBindings,
ServiceCommandPayload, ServiceSpec, ServiceStartPayload, ServiceStopPayload, ServiceType,
};
use crate::capability::CapabilityInvocationPayload;
use crate::capability::{CapabilityType, OperationCapability};
use crate::env::RouteLocation;
use crate::navigation::NavigationCommand;
use fission_ir::WidgetId;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ReqId(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ResourceId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScrollAxis {
Vertical,
Horizontal,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ScrollAlignment {
Start,
Center,
End,
Nearest,
Fraction(f32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScrollBehavior {
Instant,
Smooth,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScrollIntoViewRequest {
pub container: Option<WidgetId>,
pub target: WidgetId,
pub axis: ScrollAxis,
pub alignment: ScrollAlignment,
pub padding: [f32; 4],
pub behavior: ScrollBehavior,
pub if_needed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RuntimeEffect {
Cancel { req_id: u64 },
ReleaseResource { resource_id: u64 },
ScrollIntoView(ScrollIntoViewRequest),
Navigate(NavigationCommand),
SelectionRegion {
region_id: WidgetId,
command: crate::SelectionRegionCommand,
},
TextEditing {
input_id: WidgetId,
command: crate::TextEditingCommand,
},
TextScroll {
input_id: WidgetId,
command: crate::TextScrollCommand,
},
TextFormValidation { form_id: String },
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Effect {
Runtime(RuntimeEffect),
Capability(CapabilityInvocationPayload),
Job(JobRequestPayload),
StartService(ServiceStartPayload),
ServiceCommand(ServiceCommandPayload),
StopService(ServiceStopPayload),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct EffectEnvelope {
pub req_id: u64,
pub effect: Effect,
pub on_ok: Option<ActionEnvelope>,
pub on_err: Option<ActionEnvelope>,
pub service_bindings: Option<ServiceBindings>,
pub resource: Option<ResourceExecutionContext>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ActionInput {
None,
RouteChanged { location: RouteLocation },
JobOk {
job_name: String,
req_id: u64,
payload: Vec<u8>,
},
JobErr {
job_name: String,
req_id: u64,
payload: Option<Vec<u8>>,
message: Option<String>,
},
ServiceStarted {
service_name: String,
slot_key: String,
instance_id: u64,
},
ServiceStartFailed {
service_name: String,
slot_key: String,
payload: Option<Vec<u8>>,
message: Option<String>,
},
ServiceEvent {
service_name: String,
slot_key: String,
instance_id: u64,
payload: Vec<u8>,
},
ServiceStopped {
service_name: String,
slot_key: String,
instance_id: u64,
},
ServiceCommandOk {
service_name: String,
slot_key: String,
instance_id: u64,
req_id: u64,
payload: Option<Vec<u8>>,
},
ServiceCommandErr {
service_name: String,
slot_key: String,
instance_id: u64,
req_id: u64,
payload: Option<Vec<u8>>,
message: Option<String>,
},
CapabilityOk {
capability: String,
req_id: u64,
payload: Vec<u8>,
},
CapabilityErr {
capability: String,
req_id: u64,
payload: Option<Vec<u8>>,
message: Option<String>,
},
TimerTick { payload: Vec<u8> },
Pointer {
x: f32,
y: f32,
delta_x: f32,
delta_y: f32,
},
TextChanged(UpdateTextInput),
TextSelectionChanged(crate::action::UpdateTextSelection),
ViewportInteraction(crate::input::viewport::ViewportInteraction),
CanvasInteraction(crate::input::canvas::CanvasInteraction),
Drop {
paths: Vec<String>,
x: f32,
y: f32,
modifiers: u8,
},
InternalDrop {
payload: Vec<u8>,
x: f32,
y: f32,
modifiers: u8,
},
ScopedRaw {
scope_id: u128,
target: WidgetId,
input: Box<ActionInput>,
},
}
impl ActionInput {
pub fn encode_opaque(&self) -> Result<Vec<u8>, ActionInputCodecError> {
serde_json::to_vec(self).map_err(ActionInputCodecError)
}
pub fn decode_opaque(bytes: &[u8]) -> Result<Self, ActionInputCodecError> {
serde_json::from_slice(bytes).map_err(ActionInputCodecError)
}
pub fn scoped_raw(scope_id: u128, target: WidgetId, input: ActionInput) -> Self {
Self::ScopedRaw {
scope_id,
target: target.into(),
input: Box::new(input),
}
}
pub fn action_scope_id(&self) -> Option<u128> {
match self {
ActionInput::ScopedRaw { scope_id, .. } => Some(*scope_id),
_ => None,
}
}
pub fn scoped_target(&self) -> Option<WidgetId> {
match self {
ActionInput::ScopedRaw { target, .. } => Some(*target),
_ => None,
}
}
pub fn unscoped(&self) -> &ActionInput {
match self {
ActionInput::ScopedRaw { input, .. } => input.unscoped(),
_ => self,
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self.unscoped() {
ActionInput::JobOk { payload, .. } => Some(payload),
ActionInput::CapabilityOk { payload, .. } => Some(payload),
ActionInput::TimerTick { payload } => Some(payload),
ActionInput::InternalDrop { payload, .. } => Some(payload),
_ => None,
}
}
pub fn as_pointer(&self) -> Option<(f32, f32, f32, f32)> {
match self.unscoped() {
ActionInput::Pointer {
x,
y,
delta_x,
delta_y,
} => Some((*x, *y, *delta_x, *delta_y)),
ActionInput::Drop { x, y, .. } => Some((*x, *y, 0.0, 0.0)),
ActionInput::InternalDrop { x, y, .. } => Some((*x, *y, 0.0, 0.0)),
_ => None,
}
}
pub fn text_change(&self) -> Option<&UpdateTextInput> {
match self.unscoped() {
ActionInput::TextChanged(change) => Some(change),
_ => None,
}
}
pub fn text_selection_change(&self) -> Option<&crate::action::UpdateTextSelection> {
match self.unscoped() {
ActionInput::TextSelectionChanged(change) => Some(change),
_ => None,
}
}
pub fn viewport_interaction(&self) -> Option<&crate::input::viewport::ViewportInteraction> {
match self.unscoped() {
ActionInput::ViewportInteraction(interaction) => Some(interaction),
_ => None,
}
}
pub fn canvas_interaction(&self) -> Option<&crate::input::canvas::CanvasInteraction> {
match self.unscoped() {
ActionInput::CanvasInteraction(interaction) => Some(interaction),
_ => None,
}
}
pub fn as_drop_paths(&self) -> Option<&[String]> {
match self.unscoped() {
ActionInput::Drop { paths, .. } => Some(paths),
_ => None,
}
}
pub fn as_internal_drop(&self) -> Option<&[u8]> {
match self.unscoped() {
ActionInput::InternalDrop { payload, .. } => Some(payload),
_ => None,
}
}
pub fn as_drop_modifiers(&self) -> Option<u8> {
match self.unscoped() {
ActionInput::Drop { modifiers, .. } => Some(*modifiers),
ActionInput::InternalDrop { modifiers, .. } => Some(*modifiers),
_ => None,
}
}
pub fn job_ok<J: JobSpec>(&self, job: JobRef<J>) -> Option<J::Ok> {
match self.unscoped() {
ActionInput::JobOk {
job_name, payload, ..
} if job_name == job.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn job_err<J: JobSpec>(&self, job: JobRef<J>) -> Option<J::Err> {
match self.unscoped() {
ActionInput::JobErr {
job_name,
payload: Some(payload),
..
} if job_name == job.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn job_error_message<J: JobSpec>(&self, job: JobRef<J>) -> Option<&str> {
match self.unscoped() {
ActionInput::JobErr {
job_name,
message: Some(message),
..
} if job_name == job.name => Some(message.as_str()),
_ => None,
}
}
pub fn capability_ok<C: OperationCapability>(
&self,
capability: CapabilityType<C>,
) -> Option<C::Ok> {
match self.unscoped() {
ActionInput::CapabilityOk {
capability: actual,
payload,
..
} if actual == capability.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn capability_error<C: OperationCapability>(
&self,
capability: CapabilityType<C>,
) -> Option<C::Err> {
match self.unscoped() {
ActionInput::CapabilityErr {
capability: actual,
payload: Some(payload),
..
} if actual == capability.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn capability_error_message<C: OperationCapability>(
&self,
capability: CapabilityType<C>,
) -> Option<&str> {
match self.unscoped() {
ActionInput::CapabilityErr {
capability: actual,
message: Some(message),
..
} if actual == capability.name => Some(message),
_ => None,
}
}
#[cfg(feature = "store")]
pub fn store_value<T: serde::de::DeserializeOwned>(
&self,
) -> Option<Result<T, fission_store::StoreError>> {
self.capability_ok(crate::storage::STORE_GET).map(|value| {
value
.ok_or_else(|| {
fission_store::StoreError::new(
fission_store::StoreErrorKind::InvalidRequest,
"store key was not found",
)
})
.and_then(|value| value.decode())
})
}
#[cfg(feature = "store")]
pub fn store_error(&self) -> Option<fission_store::StoreError> {
self.capability_error(crate::storage::STORE_GET)
.or_else(|| self.capability_error(crate::storage::STORE_SET))
.or_else(|| self.capability_error(crate::storage::STORE_CONTAINS))
.or_else(|| self.capability_error(crate::storage::STORE_REMOVE))
.or_else(|| self.capability_error(crate::storage::STORE_BATCH))
.or_else(|| self.capability_error(crate::storage::STORE_LIST_PREFIX))
}
#[cfg(feature = "store")]
pub fn store_contains(&self) -> Option<bool> {
self.capability_ok(crate::storage::STORE_CONTAINS)
}
#[cfg(feature = "store")]
pub fn store_removed(&self) -> Option<bool> {
self.capability_ok(crate::storage::STORE_REMOVE)
}
#[cfg(feature = "store")]
pub fn store_batch_result(&self) -> Option<fission_store::StoreBatchResult> {
self.capability_ok(crate::storage::STORE_BATCH)
}
#[cfg(feature = "store")]
pub fn store_entries(&self) -> Option<Vec<fission_store::StoreEntry>> {
self.capability_ok(crate::storage::STORE_LIST_PREFIX)
}
#[cfg(feature = "store-sql")]
pub fn sql_rows(&self) -> Option<fission_store::SqlRows> {
self.capability_ok(crate::storage::SQL_QUERY)
}
#[cfg(feature = "store-sql")]
pub fn sql_execute_result(&self) -> Option<fission_store::SqlExecuteResult> {
self.capability_ok(crate::storage::SQL_EXECUTE)
}
#[cfg(feature = "store-sql")]
pub fn sql_transaction_result(&self) -> Option<fission_store::SqlTransactionResult> {
self.capability_ok(crate::storage::SQL_TRANSACTION)
}
#[cfg(feature = "store-sql")]
pub fn sql_migration_result(&self) -> Option<fission_store::SqlMigrationResult> {
self.capability_ok(crate::storage::SQL_MIGRATE)
}
#[cfg(feature = "store-sql")]
pub fn sql_error(&self) -> Option<fission_store::SqlError> {
self.capability_error(crate::storage::SQL_EXECUTE)
.or_else(|| self.capability_error(crate::storage::SQL_QUERY))
.or_else(|| self.capability_error(crate::storage::SQL_TRANSACTION))
.or_else(|| self.capability_error(crate::storage::SQL_MIGRATE))
}
pub fn service_event<S: ServiceSpec>(&self, service: ServiceType<S>) -> Option<S::Event> {
match self.unscoped() {
ActionInput::ServiceEvent {
service_name,
payload,
..
} if service_name == service.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn service_start_err<S: ServiceSpec>(
&self,
service: ServiceType<S>,
) -> Option<S::StartErr> {
match self.unscoped() {
ActionInput::ServiceStartFailed {
service_name,
payload: Some(payload),
..
} if service_name == service.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn service_start_error_message<S: ServiceSpec>(
&self,
service: ServiceType<S>,
) -> Option<&str> {
match self.unscoped() {
ActionInput::ServiceStartFailed {
service_name,
message: Some(message),
..
} if service_name == service.name => Some(message.as_str()),
_ => None,
}
}
pub fn service_command_ok<S: ServiceSpec>(
&self,
service: ServiceType<S>,
) -> Option<S::CommandOk> {
match self.unscoped() {
ActionInput::ServiceCommandOk {
service_name,
payload: Some(payload),
..
} if service_name == service.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn service_command_err<S: ServiceSpec>(
&self,
service: ServiceType<S>,
) -> Option<S::CommandErr> {
match self.unscoped() {
ActionInput::ServiceCommandErr {
service_name,
payload: Some(payload),
..
} if service_name == service.name => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn timer_tick<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
match self.unscoped() {
ActionInput::TimerTick { payload } => serde_json::from_slice(payload).ok(),
_ => None,
}
}
pub fn service_slot_key(&self) -> Option<&str> {
match self.unscoped() {
ActionInput::ServiceStarted { slot_key, .. }
| ActionInput::ServiceStartFailed { slot_key, .. }
| ActionInput::ServiceEvent { slot_key, .. }
| ActionInput::ServiceStopped { slot_key, .. }
| ActionInput::ServiceCommandOk { slot_key, .. }
| ActionInput::ServiceCommandErr { slot_key, .. } => Some(slot_key.as_str()),
_ => None,
}
}
pub fn service_instance_id(&self) -> Option<u64> {
match self.unscoped() {
ActionInput::ServiceStarted { instance_id, .. }
| ActionInput::ServiceEvent { instance_id, .. }
| ActionInput::ServiceStopped { instance_id, .. }
| ActionInput::ServiceCommandOk { instance_id, .. }
| ActionInput::ServiceCommandErr { instance_id, .. } => Some(*instance_id),
_ => None,
}
}
}
#[derive(Debug)]
pub struct ActionInputCodecError(serde_json::Error);
impl std::fmt::Display for ActionInputCodecError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("action input codec failed")
}
}
impl std::error::Error for ActionInputCodecError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
#[cfg(test)]
mod action_input_codec_tests {
use super::*;
use crate::event::PointerKind;
use crate::input::canvas::{CanvasInteraction, CanvasInteractionKind, CanvasInteractionPhase};
use crate::input::viewport::{
ViewportInputKind, ViewportInteraction, ViewportInteractionPhase,
};
use fission_ir::{CanvasSelectionPolicy, ViewportTransform};
use fission_layout::{LayoutPoint, LayoutRect};
#[test]
fn opaque_codec_round_trips_full_width_scope_ids() {
let input = ActionInput::scoped_raw(
u128::MAX - 1,
WidgetId::from_u128(u128::MAX - 2),
ActionInput::TextChanged(UpdateTextInput {
node_id: WidgetId::from_u128(7),
new_text: "hello".into(),
new_caret: 4,
new_anchor: 1,
..Default::default()
}),
);
let bytes = input.encode_opaque().expect("input should encode");
let decoded = ActionInput::decode_opaque(&bytes).expect("input should decode");
assert_eq!(decoded, input);
}
#[test]
fn opaque_codec_round_trips_viewport_interactions() {
let input = ActionInput::ViewportInteraction(ViewportInteraction {
node_id: WidgetId::from_u128(9),
phase: ViewportInteractionPhase::Update,
transform: ViewportTransform::new(12.0, -4.0, 1.5),
viewport_focal_point: LayoutPoint::new(40.0, 50.0),
world_focal_point: LayoutPoint::new(18.0, 36.0),
pan_delta: LayoutPoint::new(3.0, -2.0),
scale_factor: 1.1,
input_kind: ViewportInputKind::Touch,
modifiers: 1,
});
let bytes = input.encode_opaque().expect("input should encode");
let decoded = ActionInput::decode_opaque(&bytes).expect("input should decode");
assert_eq!(decoded, input);
}
#[test]
fn opaque_codec_round_trips_canvas_interactions() {
let input = ActionInput::CanvasInteraction(CanvasInteraction {
canvas_id: WidgetId::from_u128(10),
target_id: WidgetId::from_u128(11),
kind: CanvasInteractionKind::MoveNode { node_id: 12 },
selection_policy: CanvasSelectionPolicy::Toggle,
phase: CanvasInteractionPhase::Update,
input_kind: PointerKind::Mouse,
modifiers: 8,
screen_point: LayoutPoint::new(42.0, 24.0),
world_point: LayoutPoint::new(21.0, 12.0),
screen_delta: LayoutPoint::new(6.0, -4.0),
world_delta: LayoutPoint::new(3.0, -2.0),
bounds_before: Some(LayoutRect::new(1.0, 2.0, 30.0, 40.0)),
bounds_after: Some(LayoutRect::new(4.0, 0.0, 30.0, 40.0)),
marquee: None,
});
let bytes = input.encode_opaque().expect("input should encode");
let decoded = ActionInput::decode_opaque(&bytes).expect("input should decode");
assert_eq!(decoded, input);
}
}