atomr_view_core/
bridge.rs1use crate::scene::{SceneDescription, SceneKey, ScenePatch};
2use atomr_core::prelude::*;
3use pyo3::prelude::*;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tokio::sync::mpsc;
7use uuid::Uuid;
8
9#[pyclass]
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, uniffi::Record)]
11pub struct CorrelationId {
12 pub id: String,
13}
14
15#[pymethods]
16impl CorrelationId {
17 #[new]
18 pub fn new() -> Self {
19 Self { id: Uuid::new_v4().to_string() }
20 }
21
22 pub fn __repr__(&self) -> String {
23 self.id.clone()
24 }
25}
26
27impl Default for CorrelationId {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
34pub enum BackendCommand {
35 CreateWindow { id: String, title: String },
36 DestroyWindow { id: String },
37 SetScene { window_id: String, scene: SceneDescription },
38 ApplyPatches { window_id: String, patches: Vec<ScenePatch> },
39 RequestRedraw { window_id: String },
40 OpenFilePicker { correlation_id: CorrelationId, title: String },
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
44pub enum BackendEvent {
45 WindowCreated { id: String },
46 WindowClosed { id: String },
47 Input { window_id: String, event: InputEvent },
48 FrameTick { now_ms: u64 },
49 FilePickerResult { correlation_id: CorrelationId, path: Option<String> },
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Enum)]
53pub enum InputEvent {
54 Click { key: SceneKey, x: f32, y: f32 },
55 KeyDown { key_code: String },
56 TextInput { text: String },
57}
58
59pub struct UiBridge {
60 pub cmd_rx: mpsc::Receiver<BackendCommand>,
61 pub evt_tx: mpsc::Sender<BackendEvent>,
62}
63
64pub struct UiBridgeActor {
65 cmd_tx: mpsc::Sender<BackendCommand>,
66 evt_rx: Option<mpsc::Receiver<BackendEvent>>,
67 window_routes: HashMap<String, UntypedActorRef>,
68 pending_scenes: HashMap<String, SceneDescription>,
69}
70
71impl UiBridgeActor {
72 pub fn new(cmd_tx: mpsc::Sender<BackendCommand>, evt_rx: mpsc::Receiver<BackendEvent>) -> Self {
73 Self { cmd_tx, evt_rx: Some(evt_rx), window_routes: HashMap::new(), pending_scenes: HashMap::new() }
74 }
75
76 async fn flush_pending_scenes(&mut self) {
77 for (window_id, scene) in self.pending_scenes.drain() {
78 let _ = self.cmd_tx.send(BackendCommand::SetScene { window_id, scene }).await;
79 }
80 }
81}
82
83#[async_trait]
84impl Actor for UiBridgeActor {
85 type Msg = UiBridgeMessage;
86
87 async fn handle(&mut self, ctx: &mut Context<Self>, msg: UiBridgeMessage) {
88 match msg {
89 UiBridgeMessage::RegisterWindow { id, actor } => {
90 self.window_routes.insert(id, actor);
91 }
92 UiBridgeMessage::Command(cmd) => {
93 match cmd {
94 BackendCommand::SetScene { window_id, scene } => {
95 self.pending_scenes.insert(window_id, scene);
97 self.flush_pending_scenes().await;
100 }
101 _ => {
102 let _ = self.cmd_tx.send(cmd).await;
103 }
104 }
105 }
106 UiBridgeMessage::StartEventLoop => {
107 if let Some(mut evt_rx) = self.evt_rx.take() {
108 let myself = ctx.self_ref().clone();
109 tokio::spawn(async move {
110 while let Some(evt) = evt_rx.recv().await {
111 myself.tell(UiBridgeMessage::InternalEvent(evt));
112 }
113 });
114 }
115 }
116 UiBridgeMessage::InternalEvent(evt) => {
117 match evt {
118 BackendEvent::WindowClosed { id } => {
119 if let Some(_route) = self.window_routes.get(&id) {
120 }
123 self.window_routes.remove(&id);
124 self.pending_scenes.remove(&id);
125 }
126 BackendEvent::Input { window_id, event: _ } => {
127 if let Some(_route) = self.window_routes.get(&window_id) {
128 }
130 }
131 _ => {}
132 }
133 }
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
139pub enum UiBridgeMessage {
140 RegisterWindow { id: String, actor: UntypedActorRef },
141 Command(BackendCommand),
142 StartEventLoop,
143 InternalEvent(BackendEvent),
144}