Skip to main content

cranpose_native/
session.rs

1use std::sync::{
2    Arc, Mutex,
3    atomic::{AtomicBool, Ordering},
4    mpsc,
5};
6
7use tokio::sync::oneshot;
8
9use crate::{NativeContent, worker};
10
11/// A native hosting failure.
12#[derive(Debug, thiserror::Error, uniffi::Error)]
13pub enum NativeError {
14    /// The component has shut down.
15    #[error("component is closed")]
16    Closed,
17    /// Rendering or platform integration failed.
18    #[error("{detail}")]
19    Runtime {
20        /// Description of the failure.
21        detail: String,
22    },
23}
24
25/// One application event exchanged with the native host.
26#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
27pub struct NativeEvent {
28    /// Application-defined event name.
29    pub name: String,
30    /// Application-defined event value.
31    pub value: String,
32}
33
34/// Touch input in logical host coordinates.
35#[derive(Clone, Copy, uniffi::Enum)]
36pub enum TouchPhase {
37    /// Begins a gesture.
38    Down,
39    /// Moves the active pointer.
40    Move,
41    /// Ends the gesture.
42    Up,
43    /// Cancels the gesture.
44    Cancel,
45}
46
47/// One mounted native child.
48#[derive(uniffi::Record)]
49pub struct NativeSlot {
50    /// Stable identity within the session.
51    pub id: u64,
52    /// Registered platform factory name.
53    pub kind: String,
54    /// Factory configuration.
55    pub value: String,
56    /// Logical horizontal offset.
57    pub x: f32,
58    /// Logical vertical offset.
59    pub y: f32,
60    /// Logical width.
61    pub width: f32,
62    /// Logical height.
63    pub height: f32,
64}
65
66/// A presentation update. Empty pixels preserve the currently presented image and slots.
67#[derive(uniffi::Record)]
68pub struct NativeFrame {
69    /// Physical image width.
70    pub width: u32,
71    /// Physical image height.
72    pub height: u32,
73    /// Premultiplied RGBA pixels, empty when unchanged.
74    pub pixels: Vec<u8>,
75    /// Complete native-child snapshot accompanying a changed image.
76    pub slots: Vec<NativeSlot>,
77    /// Application events emitted since the preceding frame.
78    pub events: Vec<NativeEvent>,
79    /// Delay until the next scheduled update, absent when idle.
80    pub next_frame_ms: Option<u64>,
81}
82
83/// Wakes a platform view when its composition needs a frame.
84#[uniffi::export(callback_interface)]
85pub trait FrameListener: Send + Sync {
86    /// Schedule a frame on the platform UI thread.
87    fn request_frame(&self);
88}
89
90pub(crate) enum Command {
91    Frame(
92        u32,
93        u32,
94        f32,
95        oneshot::Sender<Result<NativeFrame, NativeError>>,
96    ),
97    Touch(TouchPhase, f32, f32),
98    Event(NativeEvent),
99    NativeEvent(u64, String),
100    Visible(bool),
101    Close,
102}
103
104#[derive(Default)]
105pub(crate) struct Wake(Mutex<Option<Arc<dyn FrameListener>>>);
106
107impl Wake {
108    pub(crate) fn request(&self) {
109        let listener = self
110            .0
111            .lock()
112            .ok()
113            .and_then(|guard| guard.as_ref().map(Arc::clone));
114        if let Some(listener) = listener {
115            listener.request_frame();
116        }
117    }
118}
119
120/// Owns an embedded composition and its worker thread.
121///
122/// Platform `CranposeView` adapters drive this session automatically. An application
123/// creates one with `NativeSession::new` and supplies only its content and events.
124#[derive(uniffi::Object)]
125pub struct NativeSession {
126    sender: mpsc::Sender<Command>,
127    closed: AtomicBool,
128    wake: Arc<Wake>,
129}
130
131impl NativeSession {
132    /// Constructs content on the owning worker. Captures crossing into this factory
133    /// must be Send; the composition and its state stay on that worker afterwards.
134    pub fn new(factory: impl FnOnce() -> NativeContent + Send + 'static) -> Arc<Self> {
135        let (sender, receiver) = mpsc::channel();
136        let wake = Arc::new(Wake::default());
137        let worker_wake = Arc::clone(&wake);
138        std::thread::spawn(move || worker::run(receiver, factory, worker_wake));
139        Arc::new(Self {
140            sender,
141            closed: AtomicBool::new(false),
142            wake,
143        })
144    }
145
146    fn send(&self, command: Command) -> Result<(), NativeError> {
147        if self.closed.load(Ordering::Acquire) {
148            return Err(NativeError::Closed);
149        }
150        self.sender.send(command).map_err(|_| NativeError::Closed)
151    }
152}
153
154#[uniffi::export]
155impl NativeSession {
156    /// Attaches the platform frame callback and requests an initial frame.
157    pub fn set_listener(&self, listener: Box<dyn FrameListener>) -> Result<(), NativeError> {
158        if self.closed.load(Ordering::Acquire) {
159            return Err(NativeError::Closed);
160        }
161        *self.wake.0.lock().map_err(|_| NativeError::Closed)? = Some(Arc::from(listener));
162        self.wake.request();
163        Ok(())
164    }
165
166    /// Produces the next presentation update. Platform adapters serialize these calls.
167    pub async fn frame(
168        &self,
169        width: u32,
170        height: u32,
171        density: f32,
172    ) -> Result<NativeFrame, NativeError> {
173        let (sender, receiver) = oneshot::channel();
174        self.send(Command::Frame(width, height, density, sender))?;
175        receiver.await.map_err(|_| NativeError::Closed)?
176    }
177
178    /// Delivers application input without exposing rendering internals.
179    pub fn send_event(&self, name: String, value: String) -> Result<(), NativeError> {
180        self.send(Command::Event(NativeEvent { name, value }))?;
181        self.wake.request();
182        Ok(())
183    }
184
185    /// Routes a pointer event in logical points.
186    pub fn touch(&self, phase: TouchPhase, x: f32, y: f32) -> Result<(), NativeError> {
187        self.send(Command::Touch(phase, x, y))?;
188        self.wake.request();
189        Ok(())
190    }
191
192    /// Routes a factory event from a mounted native child; stale IDs are ignored.
193    pub fn native_event(&self, id: u64, event: String) -> Result<(), NativeError> {
194        self.send(Command::NativeEvent(id, event))?;
195        self.wake.request();
196        Ok(())
197    }
198
199    /// Suspends hidden or detached content and resumes it when visible.
200    pub fn set_visible(&self, visible: bool) -> Result<(), NativeError> {
201        self.send(Command::Visible(visible))?;
202        if visible {
203            self.wake.request();
204        }
205        Ok(())
206    }
207
208    /// Releases the worker and its composition. Safe to call repeatedly.
209    pub fn shutdown(&self) {
210        if !self.closed.swap(true, Ordering::AcqRel) {
211            if let Ok(mut listener) = self.wake.0.lock() {
212                *listener = None;
213            }
214            let _ = self.sender.send(Command::Close);
215        }
216    }
217}
218
219impl Drop for NativeSession {
220    fn drop(&mut self) {
221        self.shutdown();
222    }
223}