Skip to main content

tessera_ui/
plugin.rs

1//! Plugin lifecycle hooks for Tessera platform integrations.
2
3use std::{
4    any::{Any, TypeId},
5    collections::HashMap,
6    error::Error,
7    sync::Arc,
8};
9
10use parking_lot::RwLock;
11use tracing::{error, warn};
12use winit::window::Window;
13
14#[cfg(target_os = "android")]
15use winit::platform::android::activity::AndroidApp;
16
17/// The result type used by plugin lifecycle hooks.
18pub type PluginResult = Result<(), Box<dyn Error + Send + Sync>>;
19
20type DesktopWakeHandler = Arc<dyn Fn() + Send + Sync>;
21
22/// Host-managed desktop window actions exposed to UI and platform plugins.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum DesktopWindowAction {
25    /// Minimizes the active window.
26    Minimize,
27    /// Maximizes the active window.
28    Maximize,
29    /// Toggles the active window maximized state.
30    ToggleMaximize,
31    /// Requests application shutdown through the renderer host.
32    Close,
33}
34
35impl DesktopWindowAction {
36    pub(crate) fn merge_pending(current: Option<Self>, new: Self) -> Self {
37        match (current, new) {
38            (Some(Self::Close), _) | (_, Self::Close) => Self::Close,
39            (_, new) => new,
40        }
41    }
42}
43
44/// Desktop platform services exposed to plugins.
45#[derive(Clone)]
46pub struct DesktopPlatformContext {
47    window: Arc<Window>,
48    pending_action: Arc<RwLock<Option<DesktopWindowAction>>>,
49    wake_handler: DesktopWakeHandler,
50}
51
52impl DesktopPlatformContext {
53    /// Returns the active window associated with the renderer.
54    pub fn window(&self) -> &Window {
55        &self.window
56    }
57
58    /// Clones the underlying window handle for long-lived usage.
59    pub fn window_handle(&self) -> Arc<Window> {
60        self.window.clone()
61    }
62
63    /// Minimizes the current window.
64    pub fn minimize(&self) {
65        self.request_action(DesktopWindowAction::Minimize);
66    }
67
68    /// Maximizes the current window.
69    pub fn maximize(&self) {
70        self.request_action(DesktopWindowAction::Maximize);
71    }
72
73    /// Toggles the maximized state of the current window.
74    pub fn toggle_maximize(&self) {
75        self.request_action(DesktopWindowAction::ToggleMaximize);
76    }
77
78    /// Requests host-managed application shutdown.
79    pub fn request_close(&self) {
80        self.request_action(DesktopWindowAction::Close);
81    }
82
83    fn request_action(&self, action: DesktopWindowAction) {
84        let mut pending_action = self.pending_action.write();
85        let next_action = DesktopWindowAction::merge_pending(*pending_action, action);
86        let changed = *pending_action != Some(next_action);
87        *pending_action = Some(next_action);
88        drop(pending_action);
89
90        if changed {
91            (self.wake_handler)();
92        }
93    }
94
95    pub(crate) fn new(
96        window: Arc<Window>,
97        pending_action: Arc<RwLock<Option<DesktopWindowAction>>>,
98        wake_handler: DesktopWakeHandler,
99    ) -> Self {
100        Self {
101            window,
102            pending_action,
103            wake_handler,
104        }
105    }
106}
107
108/// Lifecycle hooks for platform plugins.
109pub trait Plugin: Send + Sync + 'static {
110    /// Returns the plugin name for logging and diagnostics.
111    fn name(&self) -> &'static str {
112        std::any::type_name::<Self>()
113    }
114
115    /// Called when the renderer creates or resumes its platform resources.
116    fn on_resumed(&mut self, _context: &PluginContext) -> PluginResult {
117        Ok(())
118    }
119
120    /// Called when the renderer suspends and releases platform resources.
121    fn on_suspended(&mut self, _context: &PluginContext) -> PluginResult {
122        Ok(())
123    }
124
125    /// Called when the renderer is shutting down.
126    fn on_shutdown(&mut self, _context: &PluginContext) -> PluginResult {
127        Ok(())
128    }
129}
130
131trait PluginEntry: Send + Sync {
132    fn name(&self) -> &'static str;
133    fn resumed(&self, context: &PluginContext) -> PluginResult;
134    fn suspended(&self, context: &PluginContext) -> PluginResult;
135    fn shutdown(&self, context: &PluginContext) -> PluginResult;
136}
137
138struct PluginSlot<P: Plugin> {
139    inner: Arc<RwLock<P>>,
140}
141
142impl<P: Plugin> PluginSlot<P> {
143    fn new(inner: Arc<RwLock<P>>) -> Self {
144        Self { inner }
145    }
146}
147
148impl<P: Plugin> PluginEntry for PluginSlot<P> {
149    fn name(&self) -> &'static str {
150        self.inner.read().name()
151    }
152
153    fn resumed(&self, context: &PluginContext) -> PluginResult {
154        self.inner.write().on_resumed(context)
155    }
156
157    fn suspended(&self, context: &PluginContext) -> PluginResult {
158        self.inner.write().on_suspended(context)
159    }
160
161    fn shutdown(&self, context: &PluginContext) -> PluginResult {
162        self.inner.write().on_shutdown(context)
163    }
164}
165
166/// Platform context shared with plugins during lifecycle events.
167#[derive(Clone)]
168pub struct PluginContext {
169    desktop: DesktopPlatformContext,
170    #[cfg(target_os = "android")]
171    android_app: AndroidApp,
172}
173
174impl PluginContext {
175    /// Returns desktop platform services associated with the renderer.
176    pub fn desktop(&self) -> &DesktopPlatformContext {
177        &self.desktop
178    }
179
180    /// Returns the active window associated with the renderer.
181    pub fn window(&self) -> &Window {
182        self.desktop.window()
183    }
184
185    /// Clones the underlying window handle for long-lived usage.
186    pub fn window_handle(&self) -> Arc<Window> {
187        self.desktop.window_handle()
188    }
189
190    /// Returns the active UIKit view when called on the main thread.
191    #[cfg(target_os = "ios")]
192    pub fn ui_view(&self) -> Option<objc2::rc::Retained<objc2_ui_kit::UIView>> {
193        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
194
195        objc2::MainThreadMarker::new()?;
196        let RawWindowHandle::UiKit(handle) = self.window().window_handle().ok()?.as_raw() else {
197            return None;
198        };
199        // SAFETY: Winit owns this live view and the main-thread check precedes access.
200        unsafe { objc2::rc::Retained::retain(handle.ui_view.as_ptr().cast()) }
201    }
202
203    /// Returns the Android application handle when running on Android.
204    #[cfg(target_os = "android")]
205    pub fn android_app(&self) -> &AndroidApp {
206        &self.android_app
207    }
208
209    #[cfg(target_os = "android")]
210    pub(crate) fn new(desktop: DesktopPlatformContext, android_app: AndroidApp) -> Self {
211        Self {
212            desktop,
213            android_app,
214        }
215    }
216
217    #[cfg(not(target_os = "android"))]
218    pub(crate) fn new(desktop: DesktopPlatformContext) -> Self {
219        Self { desktop }
220    }
221}
222
223/// Registers a plugin instance for the active UI session.
224pub fn register_plugin<P: Plugin>(plugin: P) {
225    register_plugin_arc(Arc::new(RwLock::new(plugin)));
226}
227
228/// Registers a plugin instance wrapped in an `Arc<RwLock<_>>`.
229pub fn register_plugin_boxed<P: Plugin>(plugin: Arc<RwLock<P>>) {
230    register_plugin_arc(plugin);
231}
232
233/// Provides access to the registered plugin instance.
234///
235/// # Panics
236///
237/// Panics if the plugin type was not registered.
238pub fn with_plugin<T, R>(f: impl FnOnce(&T) -> R) -> R
239where
240    T: Plugin + 'static,
241{
242    let plugin = plugin_instance::<T>();
243    let guard = plugin.read();
244    f(&*guard)
245}
246
247/// Provides mutable access to the registered plugin instance.
248///
249/// # Panics
250///
251/// Panics if the plugin type was not registered.
252pub fn with_plugin_mut<T, R>(f: impl FnOnce(&mut T) -> R) -> R
253where
254    T: Plugin + 'static,
255{
256    let plugin = plugin_instance::<T>();
257    let mut guard = plugin.write();
258    f(&mut *guard)
259}
260
261pub(crate) struct PluginHost {
262    plugins: Vec<Arc<dyn PluginEntry>>,
263    shutdown_called: bool,
264}
265
266impl PluginHost {
267    pub(crate) fn new() -> Self {
268        Self {
269            plugins: registered_plugins(),
270            shutdown_called: false,
271        }
272    }
273
274    pub(crate) fn resumed(&self, context: &PluginContext) {
275        self.dispatch("resumed", context, |plugin, ctx| plugin.resumed(ctx));
276    }
277
278    pub(crate) fn suspended(&self, context: &PluginContext) {
279        self.dispatch("suspended", context, |plugin, ctx| plugin.suspended(ctx));
280    }
281
282    pub(crate) fn shutdown(&mut self, context: &PluginContext) {
283        if self.shutdown_called {
284            return;
285        }
286        self.shutdown_called = true;
287        self.dispatch("shutdown", context, |plugin, ctx| plugin.shutdown(ctx));
288    }
289
290    fn dispatch<F>(&self, stage: &'static str, context: &PluginContext, mut handler: F)
291    where
292        F: FnMut(&dyn PluginEntry, &PluginContext) -> PluginResult,
293    {
294        for plugin in &self.plugins {
295            if let Err(err) = handler(plugin.as_ref(), context) {
296                error!("Plugin '{}' {} hook failed: {}", plugin.name(), stage, err);
297            }
298        }
299    }
300}
301
302fn plugin_registry() -> Arc<RwLock<Vec<Arc<dyn PluginEntry>>>> {
303    crate::runtime_resource::<RwLock<Vec<Arc<dyn PluginEntry>>>>()
304}
305
306fn registered_plugins() -> Vec<Arc<dyn PluginEntry>> {
307    plugin_registry().read().clone()
308}
309
310fn register_plugin_arc<P: Plugin>(plugin: Arc<RwLock<P>>) {
311    assert!(
312        crate::RuntimeSession::current().is_some(),
313        "register plugins through an EntryPoint or RuntimeSession"
314    );
315    let plugin_entry = Arc::new(PluginSlot::new(plugin.clone())) as Arc<dyn PluginEntry>;
316    let instance_registry = plugin_instance_registry();
317    let mut instances = instance_registry.write();
318    let type_id = TypeId::of::<P>();
319    if instances.contains_key(&type_id) {
320        warn!(
321            "Plugin '{}' was registered more than once; keeping the first instance",
322            std::any::type_name::<P>()
323        );
324        return;
325    }
326    instances.insert(type_id, plugin as Arc<dyn Any + Send + Sync>);
327    drop(instances);
328
329    let plugins = plugin_registry();
330    let mut registry = plugins.write();
331    registry.push(plugin_entry);
332}
333
334fn plugin_instance_registry() -> Arc<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>> {
335    crate::runtime_resource::<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>()
336}
337
338/// Reads a plugin when it is available in the active session.
339pub fn try_with_plugin<T: Plugin, R>(read: impl FnOnce(&T) -> R) -> Option<R> {
340    let _session = crate::RuntimeSession::current()?;
341    let registry = plugin_instance_registry();
342    let plugin = registry.read().get(&TypeId::of::<T>()).cloned()?;
343    let plugin = Arc::downcast::<RwLock<T>>(plugin).ok()?;
344    Some(read(&plugin.read()))
345}
346
347/// Updates a plugin when it is available in the active session.
348pub fn try_with_plugin_mut<T: Plugin, R>(update: impl FnOnce(&mut T) -> R) -> Option<R> {
349    let _session = crate::RuntimeSession::current()?;
350    let registry = plugin_instance_registry();
351    let plugin = registry.read().get(&TypeId::of::<T>()).cloned()?;
352    let plugin = Arc::downcast::<RwLock<T>>(plugin).ok()?;
353    Some(update(&mut plugin.write()))
354}
355
356fn plugin_instance<T: Plugin>() -> Arc<RwLock<T>> {
357    let instances = plugin_instance_registry();
358    let registry = instances.read();
359    let type_id = TypeId::of::<T>();
360    let Some(plugin) = registry.get(&type_id) else {
361        panic!("Plugin '{}' is not registered", std::any::type_name::<T>());
362    };
363    let plugin = plugin.clone();
364    drop(registry);
365    match Arc::downcast::<RwLock<T>>(plugin) {
366        Ok(plugin) => plugin,
367        Err(_) => panic!(
368            "Plugin '{}' has a mismatched type",
369            std::any::type_name::<T>()
370        ),
371    }
372}