1use crate::action::{ActionEnvelope, UpdateTextInput};
10use crate::async_runtime::{
11 JobRef, JobRequestPayload, JobSpec, ResourceExecutionContext, ServiceBindings,
12 ServiceCommandPayload, ServiceSpec, ServiceStartPayload, ServiceStopPayload, ServiceType,
13};
14use crate::capability::CapabilityInvocationPayload;
15use crate::capability::{CapabilityType, OperationCapability};
16use crate::env::RouteLocation;
17use crate::navigation::NavigationCommand;
18use fission_ir::WidgetId;
19use serde::{Deserialize, Serialize};
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
26pub struct ReqId(pub u64);
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct ResourceId(pub u64);
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub enum ScrollAxis {
38 Vertical,
40 Horizontal,
42 Both,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
48pub enum ScrollAlignment {
49 Start,
51 Center,
53 End,
55 Nearest,
57 Fraction(f32),
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum ScrollBehavior {
67 Instant,
69 Smooth,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub struct ScrollIntoViewRequest {
95 pub container: Option<WidgetId>,
97 pub target: WidgetId,
99 pub axis: ScrollAxis,
101 pub alignment: ScrollAlignment,
103 pub padding: [f32; 4],
105 pub behavior: ScrollBehavior,
107 pub if_needed: bool,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub enum RuntimeEffect {
114 Cancel { req_id: u64 },
116 ReleaseResource { resource_id: u64 },
118 ScrollIntoView(ScrollIntoViewRequest),
120 Navigate(NavigationCommand),
122 SelectionRegion {
124 region_id: WidgetId,
125 command: crate::SelectionRegionCommand,
126 },
127 TextEditing {
129 input_id: WidgetId,
130 command: crate::TextEditingCommand,
131 },
132 TextScroll {
134 input_id: WidgetId,
135 command: crate::TextScrollCommand,
136 },
137 TextFormValidation { form_id: String },
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
146pub enum Effect {
147 Runtime(RuntimeEffect),
149 Capability(CapabilityInvocationPayload),
151 Job(JobRequestPayload),
153 StartService(ServiceStartPayload),
155 ServiceCommand(ServiceCommandPayload),
157 StopService(ServiceStopPayload),
159}
160
161#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
176pub struct EffectEnvelope {
177 pub req_id: u64,
179 pub effect: Effect,
181 pub on_ok: Option<ActionEnvelope>,
183 pub on_err: Option<ActionEnvelope>,
185 pub service_bindings: Option<ServiceBindings>,
187 pub resource: Option<ResourceExecutionContext>,
189}
190
191#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
211pub enum ActionInput {
212 None,
214 RouteChanged { location: RouteLocation },
216 JobOk {
218 job_name: String,
219 req_id: u64,
220 payload: Vec<u8>,
221 },
222 JobErr {
224 job_name: String,
225 req_id: u64,
226 payload: Option<Vec<u8>>,
227 message: Option<String>,
228 },
229 ServiceStarted {
231 service_name: String,
232 slot_key: String,
233 instance_id: u64,
234 },
235 ServiceStartFailed {
237 service_name: String,
238 slot_key: String,
239 payload: Option<Vec<u8>>,
240 message: Option<String>,
241 },
242 ServiceEvent {
244 service_name: String,
245 slot_key: String,
246 instance_id: u64,
247 payload: Vec<u8>,
248 },
249 ServiceStopped {
251 service_name: String,
252 slot_key: String,
253 instance_id: u64,
254 },
255 ServiceCommandOk {
257 service_name: String,
258 slot_key: String,
259 instance_id: u64,
260 req_id: u64,
261 payload: Option<Vec<u8>>,
262 },
263 ServiceCommandErr {
265 service_name: String,
266 slot_key: String,
267 instance_id: u64,
268 req_id: u64,
269 payload: Option<Vec<u8>>,
270 message: Option<String>,
271 },
272 CapabilityOk {
274 capability: String,
275 req_id: u64,
276 payload: Vec<u8>,
277 },
278 CapabilityErr {
280 capability: String,
281 req_id: u64,
282 payload: Option<Vec<u8>>,
283 message: Option<String>,
284 },
285 TimerTick { payload: Vec<u8> },
287 Pointer {
289 x: f32,
290 y: f32,
291 delta_x: f32,
292 delta_y: f32,
293 },
294 TextChanged(UpdateTextInput),
299 TextSelectionChanged(crate::action::UpdateTextSelection),
301 ViewportInteraction(crate::input::viewport::ViewportInteraction),
303 CanvasInteraction(crate::input::canvas::CanvasInteraction),
305 Drop {
307 paths: Vec<String>,
308 x: f32,
309 y: f32,
310 modifiers: u8,
313 },
314 InternalDrop {
316 payload: Vec<u8>,
317 x: f32,
318 y: f32,
319 modifiers: u8,
322 },
323 ScopedRaw {
325 scope_id: u128,
326 target: WidgetId,
327 input: Box<ActionInput>,
328 },
329}
330
331impl ActionInput {
332 pub fn encode_opaque(&self) -> Result<Vec<u8>, ActionInputCodecError> {
335 serde_json::to_vec(self).map_err(ActionInputCodecError)
336 }
337
338 pub fn decode_opaque(bytes: &[u8]) -> Result<Self, ActionInputCodecError> {
340 serde_json::from_slice(bytes).map_err(ActionInputCodecError)
341 }
342
343 pub fn scoped_raw(scope_id: u128, target: WidgetId, input: ActionInput) -> Self {
344 Self::ScopedRaw {
345 scope_id,
346 target: target.into(),
347 input: Box::new(input),
348 }
349 }
350
351 pub fn action_scope_id(&self) -> Option<u128> {
352 match self {
353 ActionInput::ScopedRaw { scope_id, .. } => Some(*scope_id),
354 _ => None,
355 }
356 }
357
358 pub fn scoped_target(&self) -> Option<WidgetId> {
359 match self {
360 ActionInput::ScopedRaw { target, .. } => Some(*target),
361 _ => None,
362 }
363 }
364
365 pub fn unscoped(&self) -> &ActionInput {
366 match self {
367 ActionInput::ScopedRaw { input, .. } => input.unscoped(),
368 _ => self,
369 }
370 }
371
372 pub fn as_bytes(&self) -> Option<&[u8]> {
373 match self.unscoped() {
374 ActionInput::JobOk { payload, .. } => Some(payload),
375 ActionInput::CapabilityOk { payload, .. } => Some(payload),
376 ActionInput::TimerTick { payload } => Some(payload),
377 ActionInput::InternalDrop { payload, .. } => Some(payload),
378 _ => None,
379 }
380 }
381
382 pub fn as_pointer(&self) -> Option<(f32, f32, f32, f32)> {
383 match self.unscoped() {
384 ActionInput::Pointer {
385 x,
386 y,
387 delta_x,
388 delta_y,
389 } => Some((*x, *y, *delta_x, *delta_y)),
390 ActionInput::Drop { x, y, .. } => Some((*x, *y, 0.0, 0.0)),
391 ActionInput::InternalDrop { x, y, .. } => Some((*x, *y, 0.0, 0.0)),
392 _ => None,
393 }
394 }
395
396 pub fn text_change(&self) -> Option<&UpdateTextInput> {
401 match self.unscoped() {
402 ActionInput::TextChanged(change) => Some(change),
403 _ => None,
404 }
405 }
406
407 pub fn text_selection_change(&self) -> Option<&crate::action::UpdateTextSelection> {
408 match self.unscoped() {
409 ActionInput::TextSelectionChanged(change) => Some(change),
410 _ => None,
411 }
412 }
413
414 pub fn viewport_interaction(&self) -> Option<&crate::input::viewport::ViewportInteraction> {
416 match self.unscoped() {
417 ActionInput::ViewportInteraction(interaction) => Some(interaction),
418 _ => None,
419 }
420 }
421
422 pub fn canvas_interaction(&self) -> Option<&crate::input::canvas::CanvasInteraction> {
424 match self.unscoped() {
425 ActionInput::CanvasInteraction(interaction) => Some(interaction),
426 _ => None,
427 }
428 }
429
430 pub fn as_drop_paths(&self) -> Option<&[String]> {
431 match self.unscoped() {
432 ActionInput::Drop { paths, .. } => Some(paths),
433 _ => None,
434 }
435 }
436
437 pub fn as_internal_drop(&self) -> Option<&[u8]> {
438 match self.unscoped() {
439 ActionInput::InternalDrop { payload, .. } => Some(payload),
440 _ => None,
441 }
442 }
443
444 pub fn as_drop_modifiers(&self) -> Option<u8> {
449 match self.unscoped() {
450 ActionInput::Drop { modifiers, .. } => Some(*modifiers),
451 ActionInput::InternalDrop { modifiers, .. } => Some(*modifiers),
452 _ => None,
453 }
454 }
455
456 pub fn job_ok<J: JobSpec>(&self, job: JobRef<J>) -> Option<J::Ok> {
457 match self.unscoped() {
458 ActionInput::JobOk {
459 job_name, payload, ..
460 } if job_name == job.name => serde_json::from_slice(payload).ok(),
461 _ => None,
462 }
463 }
464
465 pub fn job_err<J: JobSpec>(&self, job: JobRef<J>) -> Option<J::Err> {
466 match self.unscoped() {
467 ActionInput::JobErr {
468 job_name,
469 payload: Some(payload),
470 ..
471 } if job_name == job.name => serde_json::from_slice(payload).ok(),
472 _ => None,
473 }
474 }
475
476 pub fn job_error_message<J: JobSpec>(&self, job: JobRef<J>) -> Option<&str> {
477 match self.unscoped() {
478 ActionInput::JobErr {
479 job_name,
480 message: Some(message),
481 ..
482 } if job_name == job.name => Some(message.as_str()),
483 _ => None,
484 }
485 }
486
487 pub fn capability_ok<C: OperationCapability>(
488 &self,
489 capability: CapabilityType<C>,
490 ) -> Option<C::Ok> {
491 match self.unscoped() {
492 ActionInput::CapabilityOk {
493 capability: actual,
494 payload,
495 ..
496 } if actual == capability.name => serde_json::from_slice(payload).ok(),
497 _ => None,
498 }
499 }
500
501 pub fn capability_error<C: OperationCapability>(
502 &self,
503 capability: CapabilityType<C>,
504 ) -> Option<C::Err> {
505 match self.unscoped() {
506 ActionInput::CapabilityErr {
507 capability: actual,
508 payload: Some(payload),
509 ..
510 } if actual == capability.name => serde_json::from_slice(payload).ok(),
511 _ => None,
512 }
513 }
514
515 pub fn capability_error_message<C: OperationCapability>(
516 &self,
517 capability: CapabilityType<C>,
518 ) -> Option<&str> {
519 match self.unscoped() {
520 ActionInput::CapabilityErr {
521 capability: actual,
522 message: Some(message),
523 ..
524 } if actual == capability.name => Some(message),
525 _ => None,
526 }
527 }
528
529 #[cfg(feature = "store")]
531 pub fn store_value<T: serde::de::DeserializeOwned>(
532 &self,
533 ) -> Option<Result<T, fission_store::StoreError>> {
534 self.capability_ok(crate::storage::STORE_GET).map(|value| {
535 value
536 .ok_or_else(|| {
537 fission_store::StoreError::new(
538 fission_store::StoreErrorKind::InvalidRequest,
539 "store key was not found",
540 )
541 })
542 .and_then(|value| value.decode())
543 })
544 }
545
546 #[cfg(feature = "store")]
547 pub fn store_error(&self) -> Option<fission_store::StoreError> {
548 self.capability_error(crate::storage::STORE_GET)
549 .or_else(|| self.capability_error(crate::storage::STORE_SET))
550 .or_else(|| self.capability_error(crate::storage::STORE_CONTAINS))
551 .or_else(|| self.capability_error(crate::storage::STORE_REMOVE))
552 .or_else(|| self.capability_error(crate::storage::STORE_BATCH))
553 .or_else(|| self.capability_error(crate::storage::STORE_LIST_PREFIX))
554 }
555
556 #[cfg(feature = "store")]
557 pub fn store_contains(&self) -> Option<bool> {
558 self.capability_ok(crate::storage::STORE_CONTAINS)
559 }
560
561 #[cfg(feature = "store")]
562 pub fn store_removed(&self) -> Option<bool> {
563 self.capability_ok(crate::storage::STORE_REMOVE)
564 }
565
566 #[cfg(feature = "store")]
567 pub fn store_batch_result(&self) -> Option<fission_store::StoreBatchResult> {
568 self.capability_ok(crate::storage::STORE_BATCH)
569 }
570
571 #[cfg(feature = "store")]
572 pub fn store_entries(&self) -> Option<Vec<fission_store::StoreEntry>> {
573 self.capability_ok(crate::storage::STORE_LIST_PREFIX)
574 }
575
576 #[cfg(feature = "store-sql")]
577 pub fn sql_rows(&self) -> Option<fission_store::SqlRows> {
578 self.capability_ok(crate::storage::SQL_QUERY)
579 }
580
581 #[cfg(feature = "store-sql")]
582 pub fn sql_execute_result(&self) -> Option<fission_store::SqlExecuteResult> {
583 self.capability_ok(crate::storage::SQL_EXECUTE)
584 }
585
586 #[cfg(feature = "store-sql")]
587 pub fn sql_transaction_result(&self) -> Option<fission_store::SqlTransactionResult> {
588 self.capability_ok(crate::storage::SQL_TRANSACTION)
589 }
590
591 #[cfg(feature = "store-sql")]
592 pub fn sql_migration_result(&self) -> Option<fission_store::SqlMigrationResult> {
593 self.capability_ok(crate::storage::SQL_MIGRATE)
594 }
595
596 #[cfg(feature = "store-sql")]
597 pub fn sql_error(&self) -> Option<fission_store::SqlError> {
598 self.capability_error(crate::storage::SQL_EXECUTE)
599 .or_else(|| self.capability_error(crate::storage::SQL_QUERY))
600 .or_else(|| self.capability_error(crate::storage::SQL_TRANSACTION))
601 .or_else(|| self.capability_error(crate::storage::SQL_MIGRATE))
602 }
603
604 pub fn service_event<S: ServiceSpec>(&self, service: ServiceType<S>) -> Option<S::Event> {
605 match self.unscoped() {
606 ActionInput::ServiceEvent {
607 service_name,
608 payload,
609 ..
610 } if service_name == service.name => serde_json::from_slice(payload).ok(),
611 _ => None,
612 }
613 }
614
615 pub fn service_start_err<S: ServiceSpec>(
616 &self,
617 service: ServiceType<S>,
618 ) -> Option<S::StartErr> {
619 match self.unscoped() {
620 ActionInput::ServiceStartFailed {
621 service_name,
622 payload: Some(payload),
623 ..
624 } if service_name == service.name => serde_json::from_slice(payload).ok(),
625 _ => None,
626 }
627 }
628
629 pub fn service_start_error_message<S: ServiceSpec>(
630 &self,
631 service: ServiceType<S>,
632 ) -> Option<&str> {
633 match self.unscoped() {
634 ActionInput::ServiceStartFailed {
635 service_name,
636 message: Some(message),
637 ..
638 } if service_name == service.name => Some(message.as_str()),
639 _ => None,
640 }
641 }
642
643 pub fn service_command_ok<S: ServiceSpec>(
644 &self,
645 service: ServiceType<S>,
646 ) -> Option<S::CommandOk> {
647 match self.unscoped() {
648 ActionInput::ServiceCommandOk {
649 service_name,
650 payload: Some(payload),
651 ..
652 } if service_name == service.name => serde_json::from_slice(payload).ok(),
653 _ => None,
654 }
655 }
656
657 pub fn service_command_err<S: ServiceSpec>(
658 &self,
659 service: ServiceType<S>,
660 ) -> Option<S::CommandErr> {
661 match self.unscoped() {
662 ActionInput::ServiceCommandErr {
663 service_name,
664 payload: Some(payload),
665 ..
666 } if service_name == service.name => serde_json::from_slice(payload).ok(),
667 _ => None,
668 }
669 }
670
671 pub fn timer_tick<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
672 match self.unscoped() {
673 ActionInput::TimerTick { payload } => serde_json::from_slice(payload).ok(),
674 _ => None,
675 }
676 }
677
678 pub fn service_slot_key(&self) -> Option<&str> {
679 match self.unscoped() {
680 ActionInput::ServiceStarted { slot_key, .. }
681 | ActionInput::ServiceStartFailed { slot_key, .. }
682 | ActionInput::ServiceEvent { slot_key, .. }
683 | ActionInput::ServiceStopped { slot_key, .. }
684 | ActionInput::ServiceCommandOk { slot_key, .. }
685 | ActionInput::ServiceCommandErr { slot_key, .. } => Some(slot_key.as_str()),
686 _ => None,
687 }
688 }
689
690 pub fn service_instance_id(&self) -> Option<u64> {
691 match self.unscoped() {
692 ActionInput::ServiceStarted { instance_id, .. }
693 | ActionInput::ServiceEvent { instance_id, .. }
694 | ActionInput::ServiceStopped { instance_id, .. }
695 | ActionInput::ServiceCommandOk { instance_id, .. }
696 | ActionInput::ServiceCommandErr { instance_id, .. } => Some(*instance_id),
697 _ => None,
698 }
699 }
700}
701
702#[derive(Debug)]
704pub struct ActionInputCodecError(serde_json::Error);
705
706impl std::fmt::Display for ActionInputCodecError {
707 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
708 formatter.write_str("action input codec failed")
709 }
710}
711
712impl std::error::Error for ActionInputCodecError {
713 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
714 Some(&self.0)
715 }
716}
717
718#[cfg(test)]
719mod action_input_codec_tests {
720 use super::*;
721 use crate::event::PointerKind;
722 use crate::input::canvas::{CanvasInteraction, CanvasInteractionKind, CanvasInteractionPhase};
723 use crate::input::viewport::{
724 ViewportInputKind, ViewportInteraction, ViewportInteractionPhase,
725 };
726 use fission_ir::{CanvasSelectionPolicy, ViewportTransform};
727 use fission_layout::{LayoutPoint, LayoutRect};
728
729 #[test]
730 fn opaque_codec_round_trips_full_width_scope_ids() {
731 let input = ActionInput::scoped_raw(
732 u128::MAX - 1,
733 WidgetId::from_u128(u128::MAX - 2),
734 ActionInput::TextChanged(UpdateTextInput {
735 node_id: WidgetId::from_u128(7),
736 new_text: "hello".into(),
737 new_caret: 4,
738 new_anchor: 1,
739 ..Default::default()
740 }),
741 );
742 let bytes = input.encode_opaque().expect("input should encode");
743 let decoded = ActionInput::decode_opaque(&bytes).expect("input should decode");
744 assert_eq!(decoded, input);
745 }
746
747 #[test]
748 fn opaque_codec_round_trips_viewport_interactions() {
749 let input = ActionInput::ViewportInteraction(ViewportInteraction {
750 node_id: WidgetId::from_u128(9),
751 phase: ViewportInteractionPhase::Update,
752 transform: ViewportTransform::new(12.0, -4.0, 1.5),
753 viewport_focal_point: LayoutPoint::new(40.0, 50.0),
754 world_focal_point: LayoutPoint::new(18.0, 36.0),
755 pan_delta: LayoutPoint::new(3.0, -2.0),
756 scale_factor: 1.1,
757 input_kind: ViewportInputKind::Touch,
758 modifiers: 1,
759 });
760
761 let bytes = input.encode_opaque().expect("input should encode");
762 let decoded = ActionInput::decode_opaque(&bytes).expect("input should decode");
763 assert_eq!(decoded, input);
764 }
765
766 #[test]
767 fn opaque_codec_round_trips_canvas_interactions() {
768 let input = ActionInput::CanvasInteraction(CanvasInteraction {
769 canvas_id: WidgetId::from_u128(10),
770 target_id: WidgetId::from_u128(11),
771 kind: CanvasInteractionKind::MoveNode { node_id: 12 },
772 selection_policy: CanvasSelectionPolicy::Toggle,
773 phase: CanvasInteractionPhase::Update,
774 input_kind: PointerKind::Mouse,
775 modifiers: 8,
776 screen_point: LayoutPoint::new(42.0, 24.0),
777 world_point: LayoutPoint::new(21.0, 12.0),
778 screen_delta: LayoutPoint::new(6.0, -4.0),
779 world_delta: LayoutPoint::new(3.0, -2.0),
780 bounds_before: Some(LayoutRect::new(1.0, 2.0, 30.0, 40.0)),
781 bounds_after: Some(LayoutRect::new(4.0, 0.0, 30.0, 40.0)),
782 marquee: None,
783 });
784
785 let bytes = input.encode_opaque().expect("input should encode");
786 let decoded = ActionInput::decode_opaque(&bytes).expect("input should decode");
787 assert_eq!(decoded, input);
788 }
789}