Skip to main content

atomr_view_core/
bridge.rs

1use 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                        // Conflate: only keep the latest scene for this window
96                        self.pending_scenes.insert(window_id, scene);
97                        // In a real impl, we might use a timer to flush,
98                        // or flush on next frame tick.
99                        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                            // Forward to window actor
121                            // route.tell(...)
122                        }
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                            // route.tell(...)
129                        }
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}