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, OnceLock},
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 Android application handle when running on Android.
191    #[cfg(target_os = "android")]
192    pub fn android_app(&self) -> &AndroidApp {
193        &self.android_app
194    }
195
196    #[cfg(target_os = "android")]
197    pub(crate) fn new(desktop: DesktopPlatformContext, android_app: AndroidApp) -> Self {
198        Self {
199            desktop,
200            android_app,
201        }
202    }
203
204    #[cfg(not(target_os = "android"))]
205    pub(crate) fn new(desktop: DesktopPlatformContext) -> Self {
206        Self { desktop }
207    }
208}
209
210/// Registers a plugin instance for the current process.
211pub fn register_plugin<P: Plugin>(plugin: P) {
212    register_plugin_arc(Arc::new(RwLock::new(plugin)));
213}
214
215/// Registers a plugin instance wrapped in an `Arc<RwLock<_>>`.
216pub fn register_plugin_boxed<P: Plugin>(plugin: Arc<RwLock<P>>) {
217    register_plugin_arc(plugin);
218}
219
220/// Provides access to the registered plugin instance.
221///
222/// # Panics
223///
224/// Panics if the plugin type was not registered.
225pub fn with_plugin<T, R>(f: impl FnOnce(&T) -> R) -> R
226where
227    T: Plugin + 'static,
228{
229    let plugin = plugin_instance::<T>();
230    let guard = plugin.read();
231    f(&*guard)
232}
233
234/// Provides mutable access to the registered plugin instance.
235///
236/// # Panics
237///
238/// Panics if the plugin type was not registered.
239pub fn with_plugin_mut<T, R>(f: impl FnOnce(&mut T) -> R) -> R
240where
241    T: Plugin + 'static,
242{
243    let plugin = plugin_instance::<T>();
244    let mut guard = plugin.write();
245    f(&mut *guard)
246}
247
248pub(crate) struct PluginHost {
249    plugins: Vec<Arc<dyn PluginEntry>>,
250    shutdown_called: bool,
251}
252
253impl PluginHost {
254    pub(crate) fn new() -> Self {
255        Self {
256            plugins: registered_plugins(),
257            shutdown_called: false,
258        }
259    }
260
261    pub(crate) fn resumed(&self, context: &PluginContext) {
262        self.dispatch("resumed", context, |plugin, ctx| plugin.resumed(ctx));
263    }
264
265    pub(crate) fn suspended(&self, context: &PluginContext) {
266        self.dispatch("suspended", context, |plugin, ctx| plugin.suspended(ctx));
267    }
268
269    pub(crate) fn shutdown(&mut self, context: &PluginContext) {
270        if self.shutdown_called {
271            return;
272        }
273        self.shutdown_called = true;
274        self.dispatch("shutdown", context, |plugin, ctx| plugin.shutdown(ctx));
275    }
276
277    fn dispatch<F>(&self, stage: &'static str, context: &PluginContext, mut handler: F)
278    where
279        F: FnMut(&dyn PluginEntry, &PluginContext) -> PluginResult,
280    {
281        for plugin in &self.plugins {
282            if let Err(err) = handler(plugin.as_ref(), context) {
283                error!("Plugin '{}' {} hook failed: {}", plugin.name(), stage, err);
284            }
285        }
286    }
287}
288
289fn plugin_registry() -> &'static RwLock<Vec<Arc<dyn PluginEntry>>> {
290    static REGISTRY: OnceLock<RwLock<Vec<Arc<dyn PluginEntry>>>> = OnceLock::new();
291    REGISTRY.get_or_init(|| RwLock::new(Vec::new()))
292}
293
294fn registered_plugins() -> Vec<Arc<dyn PluginEntry>> {
295    plugin_registry().read().clone()
296}
297
298fn register_plugin_arc<P: Plugin>(plugin: Arc<RwLock<P>>) {
299    let plugin_entry = Arc::new(PluginSlot::new(plugin.clone())) as Arc<dyn PluginEntry>;
300    let mut instances = plugin_instance_registry().write();
301    let type_id = TypeId::of::<P>();
302    if instances.contains_key(&type_id) {
303        warn!(
304            "Plugin '{}' was registered more than once; keeping the first instance",
305            std::any::type_name::<P>()
306        );
307        return;
308    }
309    instances.insert(type_id, plugin as Arc<dyn Any + Send + Sync>);
310    drop(instances);
311
312    let mut registry = plugin_registry().write();
313    registry.push(plugin_entry);
314}
315
316fn plugin_instance_registry() -> &'static RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>> {
317    static REGISTRY: OnceLock<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>> =
318        OnceLock::new();
319    REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
320}
321
322fn plugin_instance<T: Plugin>() -> Arc<RwLock<T>> {
323    let registry = plugin_instance_registry().read();
324    let type_id = TypeId::of::<T>();
325    let Some(plugin) = registry.get(&type_id) else {
326        panic!("Plugin '{}' is not registered", std::any::type_name::<T>());
327    };
328    let plugin = plugin.clone();
329    drop(registry);
330    match Arc::downcast::<RwLock<T>>(plugin) {
331        Ok(plugin) => plugin,
332        Err(_) => panic!(
333            "Plugin '{}' has a mismatched type",
334            std::any::type_name::<T>()
335        ),
336    }
337}