1use serde::Serialize;
8use serde_json::Value;
9
10#[derive(Debug, Clone, PartialEq)]
29pub enum CoalesceHint {
30 Replace,
34 Accumulate(Vec<String>),
40}
41
42#[derive(Debug, Serialize)]
56pub struct OutgoingEvent {
57 #[serde(rename = "type")]
59 pub message_type: &'static str,
60 pub session: String,
62 pub family: String,
64 pub id: String,
66 #[serde(skip_serializing_if = "Option::is_none")]
69 pub value: Option<Value>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub tag: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub modifiers: Option<KeyModifiers>,
76 #[serde(skip_serializing_if = "Option::is_none")]
80 pub captured: Option<bool>,
81 #[serde(skip)]
89 pub(crate) coalesce: Option<CoalesceHint>,
90}
91
92impl OutgoingEvent {
93 pub fn with_captured(mut self, captured: bool) -> Self {
95 self.captured = Some(captured);
96 self
97 }
98
99 pub fn with_session(mut self, session: impl Into<String>) -> Self {
101 self.session = session.into();
102 self
103 }
104
105 pub fn with_coalesce(mut self, hint: CoalesceHint) -> Self {
109 self.coalesce = Some(hint);
110 self
111 }
112
113 pub fn coalesce_hint(&self) -> Option<&CoalesceHint> {
118 self.coalesce.as_ref()
119 }
120
121 pub fn take_coalesce(&mut self) -> Option<CoalesceHint> {
123 self.coalesce.take()
124 }
125
126 pub fn with_value(mut self, value: Value) -> Self {
138 self.value = Some(value);
139 self
140 }
141}
142
143#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
148pub struct KeyModifiers {
149 #[serde(default)]
150 pub shift: bool,
152 #[serde(default)]
153 pub ctrl: bool,
155 #[serde(default)]
156 pub alt: bool,
158 #[serde(default)]
159 pub logo: bool,
161 #[serde(default)]
162 pub command: bool,
164}
165
166impl OutgoingEvent {
171 fn bare(family: impl Into<String>, id: impl Into<String>) -> Self {
173 Self {
174 message_type: "event",
175 session: String::new(),
176 family: family.into(),
177 id: id.into(),
178 value: None,
179 tag: None,
180 modifiers: None,
181 captured: None,
182 coalesce: None,
183 }
184 }
185
186 pub fn tagged(family: impl Into<String>, tag: impl Into<String>) -> Self {
188 Self {
189 message_type: "event",
190 session: String::new(),
191 family: family.into(),
192 id: String::new(),
193 value: None,
194 tag: Some(tag.into()),
195 modifiers: None,
196 captured: None,
197 coalesce: None,
198 }
199 }
200
201 pub fn generic(family: impl Into<String>, id: impl Into<String>, value: Option<Value>) -> Self {
204 Self {
205 value,
206 ..Self::bare(family, id)
207 }
208 }
209
210 pub fn widget_event(
216 family: impl Into<String>,
217 id: impl Into<String>,
218 value: Option<Value>,
219 ) -> Self {
220 let family = family.into();
221 crate::EventType::assert_custom_family(&family);
222 Self::generic(family, id, value)
223 }
224
225 pub fn click(id: impl Into<String>) -> Self {
227 Self::bare("click", id)
228 }
229
230 pub fn input(id: impl Into<String>, value: impl Into<String>) -> Self {
232 Self {
233 value: Some(Value::String(value.into())),
234 ..Self::bare("input", id)
235 }
236 }
237
238 pub fn submit(id: impl Into<String>, value: impl Into<String>) -> Self {
240 Self {
241 value: Some(Value::String(value.into())),
242 ..Self::bare("submit", id)
243 }
244 }
245
246 pub fn toggle(id: impl Into<String>, checked: bool) -> Self {
248 Self {
249 value: Some(Value::Bool(checked)),
250 ..Self::bare("toggle", id)
251 }
252 }
253
254 pub fn slide(id: impl Into<String>, value: f64) -> Self {
256 Self {
257 value: Some(serde_json::json!(sanitize_f64(value))),
258 coalesce: Some(CoalesceHint::Replace),
259 ..Self::bare("slide", id)
260 }
261 }
262
263 pub fn slide_release(id: impl Into<String>, value: f64) -> Self {
265 Self {
266 value: Some(serde_json::json!(sanitize_f64(value))),
267 ..Self::bare("slide_release", id)
268 }
269 }
270
271 pub fn select(id: impl Into<String>, value: impl Into<String>) -> Self {
273 Self {
274 value: Some(Value::String(value.into())),
275 ..Self::bare("select", id)
276 }
277 }
278
279 pub fn modifiers_changed(tag: impl Into<String>, modifiers: KeyModifiers) -> Self {
288 Self {
289 modifiers: Some(modifiers),
290 coalesce: Some(CoalesceHint::Replace),
291 ..Self::tagged("modifiers_changed", tag)
292 }
293 }
294
295 pub fn cursor_moved(tag: impl Into<String>, x: f32, y: f32) -> Self {
301 Self {
302 value: Some(serde_json::json!({"x": sanitize_f32(x), "y": sanitize_f32(y)})),
303 coalesce: Some(CoalesceHint::Replace),
304 ..Self::tagged("cursor_moved", tag)
305 }
306 }
307
308 pub fn cursor_entered(tag: impl Into<String>) -> Self {
310 Self::tagged("cursor_entered", tag)
311 }
312
313 pub fn cursor_left(tag: impl Into<String>) -> Self {
315 Self::tagged("cursor_left", tag)
316 }
317
318 pub fn button_pressed(tag: impl Into<String>, button: impl Into<String>) -> Self {
320 Self {
321 value: Some(Value::String(button.into())),
322 ..Self::tagged("button_pressed", tag)
323 }
324 }
325
326 pub fn button_released(tag: impl Into<String>, button: impl Into<String>) -> Self {
328 Self {
329 value: Some(Value::String(button.into())),
330 ..Self::tagged("button_released", tag)
331 }
332 }
333
334 pub fn wheel_scrolled(tag: impl Into<String>, delta_x: f32, delta_y: f32, unit: &str) -> Self {
336 Self {
337 value: Some(serde_json::json!({
338 "delta_x": sanitize_f32(delta_x),
339 "delta_y": sanitize_f32(delta_y),
340 "unit": unit,
341 })),
342 coalesce: Some(CoalesceHint::Accumulate(vec![
343 "delta_x".into(),
344 "delta_y".into(),
345 ])),
346 ..Self::tagged("wheel_scrolled", tag)
347 }
348 }
349
350 fn touch_event(family: &str, tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
355 Self {
356 value: Some(serde_json::json!({
357 "id": finger_id,
358 "x": sanitize_f32(x),
359 "y": sanitize_f32(y),
360 })),
361 ..Self::tagged(family, tag)
362 }
363 }
364
365 pub fn finger_pressed(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
367 Self::touch_event("finger_pressed", tag, finger_id, x, y)
368 }
369
370 pub fn finger_moved(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
372 Self {
373 coalesce: Some(CoalesceHint::Replace),
374 ..Self::touch_event("finger_moved", tag, finger_id, x, y)
375 }
376 }
377
378 pub fn finger_lifted(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
380 Self::touch_event("finger_lifted", tag, finger_id, x, y)
381 }
382
383 pub fn finger_lost(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
385 Self::touch_event("finger_lost", tag, finger_id, x, y)
386 }
387
388 pub fn ime_opened(tag: impl Into<String>) -> Self {
394 Self::tagged("ime_opened", tag)
395 }
396
397 pub fn ime_preedit(
399 tag: impl Into<String>,
400 text: impl Into<String>,
401 cursor: Option<std::ops::Range<usize>>,
402 ) -> Self {
403 let cursor_val = cursor
404 .map(|r| serde_json::json!({"start": r.start, "end": r.end}))
405 .unwrap_or(serde_json::Value::Null);
406 Self {
407 value: Some(serde_json::json!({"text": text.into(), "cursor": cursor_val})),
408 ..Self::tagged("ime_preedit", tag)
409 }
410 }
411
412 pub fn ime_commit(tag: impl Into<String>, text: impl Into<String>) -> Self {
414 Self {
415 value: Some(serde_json::json!({"text": text.into()})),
416 ..Self::tagged("ime_commit", tag)
417 }
418 }
419
420 pub fn ime_closed(tag: impl Into<String>) -> Self {
422 Self::tagged("ime_closed", tag)
423 }
424
425 pub fn window_opened(
431 tag: impl Into<String>,
432 window_id: impl Into<String>,
433 position: Option<(f32, f32)>,
434 width: f32,
435 height: f32,
436 scale_factor: f32,
437 ) -> Self {
438 let mut value = serde_json::json!({
441 "window_id": window_id.into(),
442 "width": sanitize_f32(width),
443 "height": sanitize_f32(height),
444 "scale_factor": sanitize_f32(scale_factor),
445 });
446 if let Some((x, y)) = position {
447 value["x"] = serde_json::json!(sanitize_f32(x));
448 value["y"] = serde_json::json!(sanitize_f32(y));
449 }
450 Self {
451 value: Some(value),
452 ..Self::tagged("window_opened", tag)
453 }
454 }
455
456 fn window_event(family: &str, tag: impl Into<String>, window_id: impl Into<String>) -> Self {
458 Self {
459 value: Some(serde_json::json!({"window_id": window_id.into()})),
460 ..Self::tagged(family, tag)
461 }
462 }
463
464 pub fn window_closed(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
466 Self::window_event("window_closed", tag, window_id)
467 }
468
469 pub fn window_close_requested(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
471 Self::window_event("window_close_requested", tag, window_id)
472 }
473
474 pub fn window_moved(
476 tag: impl Into<String>,
477 window_id: impl Into<String>,
478 x: f32,
479 y: f32,
480 ) -> Self {
481 Self {
482 value: Some(serde_json::json!({
483 "window_id": window_id.into(),
484 "x": sanitize_f32(x),
485 "y": sanitize_f32(y),
486 })),
487 ..Self::tagged("window_moved", tag)
488 }
489 }
490
491 pub fn window_resized(
493 tag: impl Into<String>,
494 window_id: impl Into<String>,
495 width: f32,
496 height: f32,
497 ) -> Self {
498 Self {
499 value: Some(serde_json::json!({
500 "window_id": window_id.into(),
501 "width": sanitize_f32(width),
502 "height": sanitize_f32(height),
503 })),
504 ..Self::tagged("window_resized", tag)
505 }
506 }
507
508 pub fn window_focused(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
510 Self::window_event("window_focused", tag, window_id)
511 }
512
513 pub fn window_unfocused(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
515 Self::window_event("window_unfocused", tag, window_id)
516 }
517
518 pub fn window_rescaled(
520 tag: impl Into<String>,
521 window_id: impl Into<String>,
522 scale_factor: f32,
523 ) -> Self {
524 Self {
525 value: Some(serde_json::json!({
526 "window_id": window_id.into(),
527 "scale_factor": sanitize_f32(scale_factor),
528 })),
529 ..Self::tagged("window_rescaled", tag)
530 }
531 }
532
533 pub fn file_hovered(
535 tag: impl Into<String>,
536 window_id: impl Into<String>,
537 path: impl Into<String>,
538 ) -> Self {
539 Self {
540 value: Some(serde_json::json!({
541 "window_id": window_id.into(),
542 "path": path.into(),
543 })),
544 ..Self::tagged("file_hovered", tag)
545 }
546 }
547
548 pub fn file_dropped(
550 tag: impl Into<String>,
551 window_id: impl Into<String>,
552 path: impl Into<String>,
553 ) -> Self {
554 Self {
555 value: Some(serde_json::json!({
556 "window_id": window_id.into(),
557 "path": path.into(),
558 })),
559 ..Self::tagged("file_dropped", tag)
560 }
561 }
562
563 pub fn files_hovered_left(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
565 Self::window_event("files_hovered_left", tag, window_id)
566 }
567
568 pub fn animation_frame(tag: impl Into<String>, timestamp_millis: u64) -> Self {
574 Self {
575 value: Some(serde_json::json!({"timestamp": timestamp_millis})),
576 coalesce: Some(CoalesceHint::Replace),
577 ..Self::tagged("animation_frame", tag)
578 }
579 }
580
581 pub fn theme_changed(tag: impl Into<String>, mode: impl Into<String>) -> Self {
583 Self {
584 value: Some(Value::String(mode.into())),
585 coalesce: Some(CoalesceHint::Replace),
586 ..Self::tagged("theme_changed", tag)
587 }
588 }
589
590 pub fn diagnostic(
596 canvas_id: impl Into<String>,
597 element_id: Option<String>,
598 level: &str,
599 code: &str,
600 message: &str,
601 ) -> Self {
602 Self {
603 value: Some(serde_json::json!({
604 "level": level,
605 "element_id": element_id,
606 "code": code,
607 "message": message,
608 })),
609 ..Self::bare("diagnostic", canvas_id)
610 }
611 }
612
613 pub fn pane_resized(id: impl Into<String>, split: impl Into<String>, ratio: f32) -> Self {
619 Self {
620 value: Some(serde_json::json!({"split": split.into(), "ratio": sanitize_f32(ratio)})),
621 coalesce: Some(CoalesceHint::Replace),
622 ..Self::bare("pane_resized", id)
623 }
624 }
625
626 pub fn pane_dragged(
628 id: impl Into<String>,
629 kind: &str,
630 pane: impl Into<String>,
631 target: Option<String>,
632 region: Option<&str>,
633 edge: Option<&str>,
634 ) -> Self {
635 let mut val = serde_json::json!({"action": kind, "pane": pane.into()});
636 if let Some(t) = target {
637 val["target"] = serde_json::json!(t);
638 }
639 if let Some(r) = region {
640 val["region"] = serde_json::json!(r);
641 }
642 if let Some(e) = edge {
643 val["edge"] = serde_json::json!(e);
644 }
645 Self {
646 value: Some(val),
647 ..Self::bare("pane_dragged", id)
648 }
649 }
650
651 pub fn pane_clicked(id: impl Into<String>, pane: impl Into<String>) -> Self {
653 Self {
654 value: Some(serde_json::json!({"pane": pane.into()})),
655 ..Self::bare("pane_clicked", id)
656 }
657 }
658
659 pub fn pane_focus_cycle(id: impl Into<String>, pane: impl Into<String>) -> Self {
661 Self {
662 value: Some(serde_json::json!({"pane": pane.into()})),
663 ..Self::bare("pane_focus_cycle", id)
664 }
665 }
666
667 pub fn paste(id: impl Into<String>, text: impl Into<String>) -> Self {
673 Self {
674 value: Some(Value::String(text.into())),
675 ..Self::bare("paste", id)
676 }
677 }
678
679 pub fn scripting_key_press(key: impl Into<String>, modifiers_json: Value) -> Self {
689 let mods: KeyModifiers =
690 serde_json::from_value(modifiers_json).unwrap_or(KeyModifiers::default());
691 Self {
692 modifiers: Some(mods),
693 value: Some(serde_json::json!({"key": key.into()})),
694 ..Self::bare("key_press", String::new())
695 }
696 }
697
698 pub fn scripting_key_release(key: impl Into<String>, modifiers_json: Value) -> Self {
704 let mods: KeyModifiers =
705 serde_json::from_value(modifiers_json).unwrap_or(KeyModifiers::default());
706 Self {
707 modifiers: Some(mods),
708 value: Some(serde_json::json!({"key": key.into()})),
709 ..Self::bare("key_release", String::new())
710 }
711 }
712
713 pub fn scripting_cursor_moved(x: f32, y: f32) -> Self {
719 Self {
720 value: Some(serde_json::json!({
721 "x": sanitize_f32(x),
722 "y": sanitize_f32(y),
723 })),
724 ..Self::bare("cursor_moved", String::new())
725 }
726 }
727
728 pub fn scripting_scroll(delta_x: f32, delta_y: f32) -> Self {
733 Self {
734 value: Some(serde_json::json!({
735 "delta_x": sanitize_f32(delta_x),
736 "delta_y": sanitize_f32(delta_y),
737 "unit": "pixel",
738 })),
739 ..Self::bare("wheel_scrolled", String::new())
740 }
741 }
742
743 pub fn option_hovered(id: impl Into<String>, value: impl Into<String>) -> Self {
749 Self {
750 value: Some(Value::String(value.into())),
751 ..Self::bare("option_hovered", id)
752 }
753 }
754
755 #[allow(clippy::too_many_arguments)]
760 pub fn scroll(
762 id: impl Into<String>,
763 abs_x: f32,
764 abs_y: f32,
765 rel_x: f32,
766 rel_y: f32,
767 bounds_w: f32,
768 bounds_h: f32,
769 content_w: f32,
770 content_h: f32,
771 ) -> Self {
772 Self {
773 value: Some(serde_json::json!({
774 "absolute_x": sanitize_f32(abs_x), "absolute_y": sanitize_f32(abs_y),
775 "relative_x": sanitize_f32(rel_x), "relative_y": sanitize_f32(rel_y),
776 "bounds_width": sanitize_f32(bounds_w), "bounds_height": sanitize_f32(bounds_h),
777 "content_width": sanitize_f32(content_w), "content_height": sanitize_f32(content_h),
778 })),
779 coalesce: Some(CoalesceHint::Replace),
780 ..Self::bare("scrolled", id)
781 }
782 }
783
784 fn modifiers_data(modifiers: &KeyModifiers) -> serde_json::Value {
795 serde_json::json!({
796 "shift": modifiers.shift,
797 "ctrl": modifiers.ctrl,
798 "alt": modifiers.alt,
799 "logo": modifiers.logo,
800 "command": modifiers.command,
801 })
802 }
803
804 pub fn pointer_press(
809 id: impl Into<String>,
810 x: f32,
811 y: f32,
812 button: &str,
813 pointer_type: &str,
814 finger: Option<u64>,
815 modifiers: KeyModifiers,
816 ) -> Self {
817 let mut val = serde_json::json!({
818 "x": sanitize_f32(x),
819 "y": sanitize_f32(y),
820 "button": button,
821 "pointer": pointer_type,
822 "modifiers": Self::modifiers_data(&modifiers),
823 });
824 if let Some(f) = finger {
825 val["finger"] = serde_json::json!(f);
826 }
827 Self {
828 value: Some(val),
829 ..Self::bare("press", id)
830 }
831 }
832
833 pub fn pointer_release(
835 id: impl Into<String>,
836 x: f32,
837 y: f32,
838 button: &str,
839 pointer_type: &str,
840 finger: Option<u64>,
841 modifiers: KeyModifiers,
842 ) -> Self {
843 let mut val = serde_json::json!({
844 "x": sanitize_f32(x),
845 "y": sanitize_f32(y),
846 "button": button,
847 "pointer": pointer_type,
848 "modifiers": Self::modifiers_data(&modifiers),
849 });
850 if let Some(f) = finger {
851 val["finger"] = serde_json::json!(f);
852 }
853 Self {
854 value: Some(val),
855 ..Self::bare("release", id)
856 }
857 }
858
859 pub fn pointer_move(
861 id: impl Into<String>,
862 x: f32,
863 y: f32,
864 pointer_type: &str,
865 finger: Option<u64>,
866 modifiers: KeyModifiers,
867 ) -> Self {
868 let mut val = serde_json::json!({
869 "x": sanitize_f32(x),
870 "y": sanitize_f32(y),
871 "pointer": pointer_type,
872 "modifiers": Self::modifiers_data(&modifiers),
873 });
874 if let Some(f) = finger {
875 val["finger"] = serde_json::json!(f);
876 }
877 Self {
878 value: Some(val),
879 coalesce: Some(CoalesceHint::Replace),
880 ..Self::bare("move", id)
881 }
882 }
883
884 pub fn pointer_scroll(
886 id: impl Into<String>,
887 x: f32,
888 y: f32,
889 delta_x: f32,
890 delta_y: f32,
891 pointer_type: &str,
892 modifiers: KeyModifiers,
893 ) -> Self {
894 Self {
895 value: Some(serde_json::json!({
896 "x": sanitize_f32(x),
897 "y": sanitize_f32(y),
898 "delta_x": sanitize_f32(delta_x),
899 "delta_y": sanitize_f32(delta_y),
900 "pointer": pointer_type,
901 "modifiers": Self::modifiers_data(&modifiers),
902 })),
903 coalesce: Some(CoalesceHint::Accumulate(vec![
904 "delta_x".into(),
905 "delta_y".into(),
906 ])),
907 ..Self::bare("scroll", id)
908 }
909 }
910
911 pub fn pointer_enter(id: impl Into<String>) -> Self {
913 Self::bare("enter", id)
914 }
915
916 pub fn pointer_exit(id: impl Into<String>) -> Self {
918 Self::bare("exit", id)
919 }
920
921 pub fn pointer_double_click(
923 id: impl Into<String>,
924 x: f32,
925 y: f32,
926 pointer_type: &str,
927 modifiers: KeyModifiers,
928 ) -> Self {
929 Self {
930 value: Some(serde_json::json!({
931 "x": sanitize_f32(x),
932 "y": sanitize_f32(y),
933 "pointer": pointer_type,
934 "modifiers": Self::modifiers_data(&modifiers),
935 })),
936 ..Self::bare("double_click", id)
937 }
938 }
939
940 pub fn resize(id: impl Into<String>, width: f32, height: f32) -> Self {
942 Self {
943 value: Some(serde_json::json!({
944 "width": sanitize_f32(width),
945 "height": sanitize_f32(height),
946 })),
947 coalesce: Some(CoalesceHint::Replace),
948 ..Self::bare("resize", id)
949 }
950 }
951}
952
953fn sanitize_f32(v: f32) -> Value {
963 if v.is_finite() {
964 serde_json::json!(v)
965 } else {
966 log::warn!("non-finite f32 ({v}) replaced with null in outgoing event");
967 Value::Null
968 }
969}
970
971fn sanitize_f64(v: f64) -> Value {
974 if v.is_finite() {
975 serde_json::json!(v)
976 } else {
977 log::warn!("non-finite f64 ({v}) replaced with null in outgoing event");
978 Value::Null
979 }
980}
981
982#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
994#[serde(rename_all = "snake_case")]
995pub enum DiagnosticLevel {
996 Info,
998 Warn,
1000 Error,
1003}
1004
1005impl std::fmt::Display for DiagnosticLevel {
1006 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1007 match self {
1008 Self::Info => f.write_str("info"),
1009 Self::Warn => f.write_str("warn"),
1010 Self::Error => f.write_str("error"),
1011 }
1012 }
1013}
1014
1015#[derive(Debug, Clone, Serialize)]
1033pub struct DiagnosticMessage {
1034 #[serde(rename = "type")]
1036 pub message_type: &'static str,
1037 pub session: String,
1039 pub level: DiagnosticLevel,
1041 pub diagnostic: crate::Diagnostic,
1043}
1044
1045impl DiagnosticMessage {
1046 pub fn new(level: DiagnosticLevel, diagnostic: crate::Diagnostic) -> Self {
1048 Self {
1049 message_type: "diagnostic",
1050 session: String::new(),
1051 level,
1052 diagnostic,
1053 }
1054 }
1055
1056 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1058 self.session = session.into();
1059 self
1060 }
1061}
1062
1063#[derive(Debug, Serialize)]
1065pub struct EffectResponse {
1066 #[serde(rename = "type")]
1067 pub message_type: &'static str,
1069 pub session: String,
1071 pub id: String,
1073 pub status: &'static str,
1075 #[serde(skip_serializing_if = "Option::is_none")]
1076 pub result: Option<Value>,
1078 #[serde(skip_serializing_if = "Option::is_none")]
1079 pub error: Option<String>,
1081}
1082
1083impl EffectResponse {
1084 pub fn ok(id: String, result: Value) -> Self {
1086 Self {
1087 message_type: "effect_response",
1088 session: String::new(),
1089 id,
1090 status: "ok",
1091 result: Some(result),
1092 error: None,
1093 }
1094 }
1095
1096 pub fn error(id: String, reason: String) -> Self {
1098 Self {
1099 message_type: "effect_response",
1100 session: String::new(),
1101 id,
1102 status: "error",
1103 result: None,
1104 error: Some(reason),
1105 }
1106 }
1107
1108 pub fn unsupported(id: String) -> Self {
1114 Self {
1115 message_type: "effect_response",
1116 session: String::new(),
1117 id,
1118 status: "unsupported",
1119 result: None,
1120 error: None,
1121 }
1122 }
1123
1124 pub fn cancelled(id: String) -> Self {
1128 Self {
1129 message_type: "effect_response",
1130 session: String::new(),
1131 id,
1132 status: "cancelled",
1133 result: None,
1134 error: None,
1135 }
1136 }
1137
1138 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1140 self.session = session.into();
1141 self
1142 }
1143}
1144
1145#[derive(Debug, Serialize)]
1149pub struct EffectStubAck {
1150 #[serde(rename = "type")]
1151 pub message_type: &'static str,
1153 pub session: String,
1155 pub kind: String,
1157 pub status: &'static str,
1159}
1160
1161impl EffectStubAck {
1162 pub fn registered(kind: String) -> Self {
1164 Self {
1165 message_type: "effect_stub_register_ack",
1166 session: String::new(),
1167 kind,
1168 status: "registered",
1169 }
1170 }
1171
1172 pub fn register_error(kind: String) -> Self {
1174 Self {
1175 message_type: "effect_stub_register_ack",
1176 session: String::new(),
1177 kind,
1178 status: "error",
1179 }
1180 }
1181
1182 pub fn unregistered(kind: String) -> Self {
1184 Self {
1185 message_type: "effect_stub_unregister_ack",
1186 session: String::new(),
1187 kind,
1188 status: "unregistered",
1189 }
1190 }
1191
1192 pub fn unregister_error(kind: String) -> Self {
1194 Self {
1195 message_type: "effect_stub_unregister_ack",
1196 session: String::new(),
1197 kind,
1198 status: "error",
1199 }
1200 }
1201
1202 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1204 self.session = session.into();
1205 self
1206 }
1207}
1208
1209#[derive(Debug, Serialize)]
1211pub struct QueryResponse {
1212 #[serde(rename = "type")]
1213 pub message_type: &'static str,
1215 pub session: String,
1217 pub id: String,
1219 pub target: String,
1221 pub data: Value,
1223}
1224
1225impl QueryResponse {
1226 pub fn new(id: String, target: String, data: Value) -> Self {
1228 Self {
1229 message_type: "query_response",
1230 session: String::new(),
1231 id,
1232 target,
1233 data,
1234 }
1235 }
1236
1237 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1239 self.session = session.into();
1240 self
1241 }
1242}
1243
1244#[derive(Debug, Serialize)]
1246pub struct InteractResponse {
1247 #[serde(rename = "type")]
1248 pub message_type: &'static str,
1250 pub session: String,
1252 pub id: String,
1254 pub events: Vec<OutgoingEvent>,
1256}
1257
1258impl InteractResponse {
1259 pub fn new(id: String, events: Vec<OutgoingEvent>) -> Self {
1261 Self {
1262 message_type: "interact_response",
1263 session: String::new(),
1264 id,
1265 events,
1266 }
1267 }
1268
1269 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1271 let session = session.into();
1272 for event in &mut self.events {
1273 event.session.clone_from(&session);
1274 }
1275 self.session = session;
1276 self
1277 }
1278}
1279
1280#[derive(Debug, Serialize)]
1285#[allow(dead_code)]
1286pub struct TreeHashResponse {
1287 #[serde(rename = "type")]
1288 pub message_type: &'static str,
1290 pub session: String,
1292 pub id: String,
1294 pub name: String,
1296 pub hash: String,
1298}
1299
1300#[allow(dead_code)]
1301impl TreeHashResponse {
1302 pub fn new(id: String, name: String, hash: String) -> Self {
1304 Self {
1305 message_type: "tree_hash_response",
1306 session: String::new(),
1307 id,
1308 name,
1309 hash,
1310 }
1311 }
1312
1313 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1315 self.session = session.into();
1316 self
1317 }
1318}
1319
1320#[derive(Debug, Serialize)]
1326pub struct ScreenshotResponse {
1327 #[serde(rename = "type")]
1328 pub message_type: &'static str,
1330 pub session: String,
1332 pub id: String,
1334 pub name: String,
1336 pub hash: String,
1338 pub width: u32,
1340 pub height: u32,
1342}
1343
1344impl ScreenshotResponse {
1345 pub fn new(id: String, name: String, hash: String, width: u32, height: u32) -> Self {
1347 Self {
1348 message_type: "screenshot_response",
1349 session: String::new(),
1350 id,
1351 name,
1352 hash,
1353 width,
1354 height,
1355 }
1356 }
1357
1358 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1360 self.session = session.into();
1361 self
1362 }
1363}
1364
1365#[derive(Debug, Serialize)]
1367pub struct ResetResponse {
1368 #[serde(rename = "type")]
1369 pub message_type: &'static str,
1371 pub session: String,
1373 pub id: String,
1375 pub status: &'static str,
1377}
1378
1379impl ResetResponse {
1380 pub fn ok(id: String) -> Self {
1382 Self {
1383 message_type: "reset_response",
1384 session: String::new(),
1385 id,
1386 status: "ok",
1387 }
1388 }
1389
1390 pub fn with_session(mut self, session: impl Into<String>) -> Self {
1392 self.session = session.into();
1393 self
1394 }
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399 use super::*;
1400 use serde_json::json;
1401
1402 #[test]
1403 fn effect_stub_register_ack_includes_status() {
1404 let ack = EffectStubAck::registered("file_open".to_string()).with_session("s1");
1405
1406 assert_eq!(
1407 serde_json::to_value(ack).unwrap(),
1408 json!({
1409 "type": "effect_stub_register_ack",
1410 "session": "s1",
1411 "kind": "file_open",
1412 "status": "registered",
1413 })
1414 );
1415 }
1416
1417 #[test]
1418 fn effect_stub_register_error_ack_includes_status() {
1419 let ack = EffectStubAck::register_error("not_real".to_string()).with_session("s1");
1420
1421 assert_eq!(
1422 serde_json::to_value(ack).unwrap(),
1423 json!({
1424 "type": "effect_stub_register_ack",
1425 "session": "s1",
1426 "kind": "not_real",
1427 "status": "error",
1428 })
1429 );
1430 }
1431
1432 #[test]
1433 fn effect_stub_unregister_ack_includes_status() {
1434 let ack = EffectStubAck::unregistered("file_open".to_string()).with_session("s1");
1435
1436 assert_eq!(
1437 serde_json::to_value(ack).unwrap(),
1438 json!({
1439 "type": "effect_stub_unregister_ack",
1440 "session": "s1",
1441 "kind": "file_open",
1442 "status": "unregistered",
1443 })
1444 );
1445 }
1446
1447 #[test]
1448 fn effect_stub_unregister_error_ack_includes_status() {
1449 let ack = EffectStubAck::unregister_error("not_real".to_string()).with_session("s1");
1450
1451 assert_eq!(
1452 serde_json::to_value(ack).unwrap(),
1453 json!({
1454 "type": "effect_stub_unregister_ack",
1455 "session": "s1",
1456 "kind": "not_real",
1457 "status": "error",
1458 })
1459 );
1460 }
1461
1462 #[test]
1463 fn screenshot_response_serializes_structured_fields() {
1464 let response = ScreenshotResponse::new(
1465 "sc1".to_string(),
1466 "homepage".to_string(),
1467 "d4e5f6".to_string(),
1468 1024,
1469 768,
1470 )
1471 .with_session("s1");
1472
1473 assert_eq!(
1474 serde_json::to_value(response).unwrap(),
1475 json!({
1476 "type": "screenshot_response",
1477 "session": "s1",
1478 "id": "sc1",
1479 "name": "homepage",
1480 "hash": "d4e5f6",
1481 "width": 1024,
1482 "height": 768,
1483 })
1484 );
1485 }
1486
1487 #[test]
1488 fn animation_frame_serializes_timestamp_object() {
1489 let event = OutgoingEvent::animation_frame("anim", 16_000).with_session("s1");
1490
1491 assert_eq!(
1492 serde_json::to_value(event).unwrap(),
1493 json!({
1494 "type": "event",
1495 "session": "s1",
1496 "family": "animation_frame",
1497 "id": "",
1498 "tag": "anim",
1499 "value": {
1500 "timestamp": 16_000,
1501 },
1502 })
1503 );
1504 }
1505
1506 #[test]
1507 fn widget_event_accepts_custom_family() {
1508 let event =
1509 OutgoingEvent::widget_event("star_rating:select", "rating", Some(json!({"value": 5})));
1510
1511 assert_eq!(event.family, "star_rating:select");
1512 assert_eq!(event.id, "rating");
1513 }
1514
1515 #[test]
1516 #[should_panic(
1517 expected = "custom event family \"click\" collides with a built-in event family"
1518 )]
1519 fn widget_event_rejects_builtin_family() {
1520 let _ = OutgoingEvent::widget_event("click", "button", None);
1521 }
1522
1523 #[test]
1524 fn generic_allows_builtin_renderer_events() {
1525 let event = OutgoingEvent::generic("click", "button", None);
1526
1527 assert_eq!(event.family, "click");
1528 assert_eq!(event.id, "button");
1529 }
1530
1531 #[test]
1540 fn cursor_moved_serializes_with_position_value_and_replace_hint() {
1541 let event = OutgoingEvent::cursor_moved("mouse", 10.0, 20.0).with_session("s1");
1542 assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
1543
1544 assert_eq!(
1545 serde_json::to_value(event).unwrap(),
1546 json!({
1547 "type": "event",
1548 "session": "s1",
1549 "family": "cursor_moved",
1550 "id": "",
1551 "tag": "mouse",
1552 "value": {"x": 10.0, "y": 20.0},
1553 })
1554 );
1555 }
1556
1557 #[test]
1558 fn cursor_entered_left_serialize_without_value() {
1559 let entered = OutgoingEvent::cursor_entered("mouse").with_session("s1");
1560 let left = OutgoingEvent::cursor_left("mouse").with_session("s1");
1561 for (event, family) in [(entered, "cursor_entered"), (left, "cursor_left")] {
1562 let val = serde_json::to_value(event).unwrap();
1563 assert_eq!(val["family"], family);
1564 assert_eq!(val["tag"], "mouse");
1565 assert!(val.get("value").is_none(), "{family} should omit value");
1567 }
1568 }
1569
1570 #[test]
1571 fn button_pressed_released_carry_button_string() {
1572 let pressed = OutgoingEvent::button_pressed("mouse", "left");
1573 assert_eq!(
1574 serde_json::to_value(pressed).unwrap()["value"],
1575 json!("left"),
1576 );
1577 let released = OutgoingEvent::button_released("mouse", "right");
1578 assert_eq!(
1579 serde_json::to_value(released).unwrap()["family"],
1580 "button_released",
1581 );
1582 }
1583
1584 #[test]
1585 fn wheel_scrolled_serializes_with_accumulate_hint() {
1586 let event = OutgoingEvent::wheel_scrolled("mouse", 1.0, 2.0, "pixel");
1587 match event.coalesce_hint() {
1588 Some(CoalesceHint::Accumulate(fields)) => {
1589 assert_eq!(fields, &vec!["delta_x".to_string(), "delta_y".to_string()]);
1590 }
1591 other => panic!("expected Accumulate hint, got {other:?}"),
1592 }
1593
1594 let val = serde_json::to_value(event).unwrap();
1595 assert_eq!(val["family"], "wheel_scrolled");
1596 assert_eq!(val["value"]["delta_x"], 1.0);
1597 assert_eq!(val["value"]["delta_y"], 2.0);
1598 assert_eq!(val["value"]["unit"], "pixel");
1599 }
1600
1601 #[test]
1602 fn modifiers_changed_carries_modifiers_with_replace_hint() {
1603 let event = OutgoingEvent::modifiers_changed(
1604 "kbd",
1605 KeyModifiers {
1606 shift: true,
1607 ctrl: true,
1608 alt: false,
1609 logo: false,
1610 command: false,
1611 },
1612 );
1613 assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
1614 let val = serde_json::to_value(event).unwrap();
1615 assert_eq!(val["family"], "modifiers_changed");
1616 assert_eq!(val["modifiers"]["shift"], true);
1617 assert_eq!(val["modifiers"]["ctrl"], true);
1618 assert_eq!(val["modifiers"]["alt"], false);
1619 }
1620
1621 #[test]
1622 fn theme_changed_serializes_with_string_mode_and_replace_hint() {
1623 let event = OutgoingEvent::theme_changed("theme", "dark");
1624 assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
1625 let val = serde_json::to_value(event).unwrap();
1626 assert_eq!(val["family"], "theme_changed");
1627 assert_eq!(val["value"], "dark");
1628 }
1629
1630 #[test]
1631 fn window_event_constructors_share_window_event_shape() {
1632 for (event, family) in [
1633 (OutgoingEvent::window_closed("win", "main"), "window_closed"),
1634 (
1635 OutgoingEvent::window_close_requested("win", "main"),
1636 "window_close_requested",
1637 ),
1638 (
1639 OutgoingEvent::window_focused("win", "main"),
1640 "window_focused",
1641 ),
1642 (
1643 OutgoingEvent::window_unfocused("win", "main"),
1644 "window_unfocused",
1645 ),
1646 (
1647 OutgoingEvent::files_hovered_left("win", "main"),
1648 "files_hovered_left",
1649 ),
1650 ] {
1651 let val = serde_json::to_value(event).unwrap();
1652 assert_eq!(val["family"], family);
1653 assert_eq!(val["tag"], "win");
1654 assert_eq!(val["value"]["window_id"], "main", "for {family}");
1655 }
1656 }
1657
1658 #[test]
1659 fn window_opened_includes_position_when_provided() {
1660 let with_pos =
1661 OutgoingEvent::window_opened("win", "main", Some((50.0, 75.0)), 800.0, 600.0, 2.0);
1662 let val = serde_json::to_value(with_pos).unwrap();
1663 assert_eq!(val["family"], "window_opened");
1664 assert_eq!(val["value"]["window_id"], "main");
1665 assert_eq!(val["value"]["width"], 800.0);
1666 assert_eq!(val["value"]["height"], 600.0);
1667 assert_eq!(val["value"]["scale_factor"], 2.0);
1668 assert_eq!(val["value"]["x"], 50.0);
1669 assert_eq!(val["value"]["y"], 75.0);
1670 }
1671
1672 #[test]
1673 fn window_opened_omits_position_when_absent() {
1674 let without_pos = OutgoingEvent::window_opened("win", "main", None, 800.0, 600.0, 1.0);
1677 let val = serde_json::to_value(without_pos).unwrap();
1678 assert!(val["value"].get("x").is_none());
1679 assert!(val["value"].get("y").is_none());
1680 }
1681
1682 #[test]
1687 fn finger_event_constructors_share_payload_shape() {
1688 for (event, family, expects_replace) in [
1689 (
1690 OutgoingEvent::finger_pressed("touch", 7, 1.0, 2.0),
1691 "finger_pressed",
1692 false,
1693 ),
1694 (
1695 OutgoingEvent::finger_moved("touch", 7, 1.0, 2.0),
1696 "finger_moved",
1697 true,
1698 ),
1699 (
1700 OutgoingEvent::finger_lifted("touch", 7, 1.0, 2.0),
1701 "finger_lifted",
1702 false,
1703 ),
1704 (
1705 OutgoingEvent::finger_lost("touch", 7, 1.0, 2.0),
1706 "finger_lost",
1707 false,
1708 ),
1709 ] {
1710 let has_replace = matches!(event.coalesce_hint(), Some(CoalesceHint::Replace));
1711 assert_eq!(
1712 has_replace, expects_replace,
1713 "{family} coalesce hint mismatch (only finger_moved should coalesce)"
1714 );
1715 let val = serde_json::to_value(event).unwrap();
1716 assert_eq!(val["family"], family);
1717 assert_eq!(val["tag"], "touch");
1718 assert_eq!(val["value"]["id"], 7);
1719 assert_eq!(val["value"]["x"], 1.0);
1720 assert_eq!(val["value"]["y"], 2.0);
1721 }
1722 }
1723
1724 #[test]
1736 fn pointer_scroll_carries_accumulate_hint_for_deltas() {
1737 let event = OutgoingEvent::pointer_scroll(
1738 "scroller",
1739 5.0,
1740 10.0,
1741 1.5,
1742 -2.5,
1743 "mouse",
1744 KeyModifiers::default(),
1745 );
1746 match event.coalesce_hint() {
1747 Some(CoalesceHint::Accumulate(fields)) => {
1748 let mut sorted: Vec<&str> = fields.iter().map(String::as_str).collect();
1749 sorted.sort();
1750 assert_eq!(sorted, vec!["delta_x", "delta_y"]);
1751 }
1752 other => panic!("expected Accumulate hint, got {other:?}"),
1753 }
1754 }
1755
1756 #[test]
1757 fn coalesce_hint_persists_after_with_session() {
1758 let event = OutgoingEvent::cursor_moved("mouse", 0.0, 0.0).with_session("s1");
1761 assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
1762 }
1763
1764 #[test]
1765 fn take_coalesce_consumes_hint_only_once() {
1766 let mut event = OutgoingEvent::cursor_moved("mouse", 0.0, 0.0);
1767 assert!(event.take_coalesce().is_some());
1768 assert!(event.take_coalesce().is_none());
1771 }
1772
1773 #[test]
1774 fn coalesce_hint_field_is_skipped_on_serialization() {
1775 let event = OutgoingEvent::cursor_moved("mouse", 0.0, 0.0);
1779 let val = serde_json::to_value(event).unwrap();
1780 assert!(val.get("coalesce").is_none());
1781 let object = val.as_object().unwrap();
1782 assert!(!object.contains_key("coalesce_hint"));
1783 }
1784}