Skip to main content

astrelis_ui/plugin/
mod.rs

1//! Plugin system for modular UI widget registration and event handling.
2//!
3//! The plugin architecture replaces hardcoded downcast chains with a registry-based
4//! dispatch system. Each plugin registers its widget types and their handler functions,
5//! enabling O(1) dispatch instead of O(N) downcast chains.
6//!
7//! # Architecture
8//!
9//! - [`UiPlugin`]: Trait for implementing widget plugins (core, docking, scrolling, etc.)
10//! - [`PluginHandle`]: Zero-cost proof token that a plugin is registered
11//! - [`PluginManager`]: Owns plugins and the widget type registry
12//! - [`registry::WidgetTypeRegistry`]: Maps `TypeId` → handler functions for O(1) dispatch
13//!
14//! # Example
15//!
16//! ```ignore
17//! let mut ui = UiSystem::new(graphics);
18//! // CorePlugin is auto-added in UiCore::new()
19//!
20//! // Add optional plugins:
21//! let docking = ui.add_plugin(DockingPlugin::new());
22//!
23//! // Use plugin handle for typed access:
24//! let plugin: &DockingPlugin = ui.plugin(&docking);
25//! ```
26
27pub mod core_widgets;
28pub mod event_types;
29pub mod registry;
30
31pub use event_types::{KeyEventData, MouseButtonKind, PluginEventContext, UiInputEvent};
32pub use registry::{
33    EventResponse, TraversalBehavior, WidgetOverflow, WidgetRenderContext, WidgetTypeDescriptor,
34    WidgetTypeRegistry,
35};
36
37use crate::tree::UiTree;
38use std::any::Any;
39use std::marker::PhantomData;
40
41/// Trait for UI plugins that register widget types and handle events.
42///
43/// Plugins provide:
44/// - Widget type registration (render, measure, traversal handlers)
45/// - Cross-widget stateful event handling (drags, gestures, etc.)
46/// - Post-layout processing
47/// - Per-frame updates (animations, state cleanup)
48pub trait UiPlugin: Any + 'static {
49    /// Plugin name for debugging and logging.
50    fn name(&self) -> &str;
51
52    /// Register widget type descriptors this plugin provides.
53    ///
54    /// Called once when the plugin is added to the manager.
55    fn register_widgets(&self, registry: &mut WidgetTypeRegistry);
56
57    /// Handle a UI input event. Return `true` if consumed.
58    ///
59    /// Called before per-widget-type dispatch, in plugin registration order.
60    /// Use this for cross-widget stateful interactions (drags, etc.)
61    fn handle_event(&mut self, _event: &UiInputEvent, _ctx: &mut PluginEventContext<'_>) -> bool {
62        false
63    }
64
65    /// Called after Taffy layout for custom post-processing.
66    fn post_layout(&mut self, _tree: &mut UiTree) {}
67
68    /// Per-frame update (animations, state cleanup, etc.)
69    fn update(&mut self, _dt: f32, _tree: &mut UiTree) {}
70
71    /// Downcast support — return `self` as `&dyn Any`.
72    fn as_any(&self) -> &dyn Any;
73
74    /// Downcast support — return `self` as `&mut dyn Any`.
75    fn as_any_mut(&mut self) -> &mut dyn Any;
76}
77
78/// Zero-cost proof token that a plugin of type `P` has been registered.
79///
80/// Only [`PluginManager::add_plugin`] can construct a handle. The handle gates
81/// builder methods and provides typed access to plugin state.
82///
83/// ```ignore
84/// let handle = ui.add_plugin(MyPlugin::new());
85/// // handle proves MyPlugin is registered — gates builder methods
86/// root.my_widget(&handle).build();
87/// // typed access to plugin state
88/// let plugin: &MyPlugin = ui.plugin(&handle);
89/// ```
90pub struct PluginHandle<P: UiPlugin> {
91    _private: (),
92    _marker: PhantomData<P>,
93}
94
95impl<P: UiPlugin> Clone for PluginHandle<P> {
96    fn clone(&self) -> Self {
97        *self
98    }
99}
100
101impl<P: UiPlugin> Copy for PluginHandle<P> {}
102
103impl<P: UiPlugin> std::fmt::Debug for PluginHandle<P> {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_struct("PluginHandle")
106            .field("type", &std::any::type_name::<P>())
107            .finish()
108    }
109}
110
111/// Manages UI plugins and the widget type registry.
112///
113/// Plugins are stored in registration order. The widget type registry
114/// is populated during [`add_plugin`](PluginManager::add_plugin) and
115/// provides O(1) dispatch for render/measure/event operations.
116pub struct PluginManager {
117    plugins: Vec<Box<dyn UiPlugin>>,
118    widget_registry: WidgetTypeRegistry,
119}
120
121impl PluginManager {
122    /// Create a new empty plugin manager.
123    pub fn new() -> Self {
124        Self {
125            plugins: Vec::new(),
126            widget_registry: WidgetTypeRegistry::new(),
127        }
128    }
129
130    /// Add a plugin, register its widgets, and return a proof handle.
131    ///
132    /// # Panics
133    ///
134    /// Panics if a plugin of the same concrete type is already registered.
135    pub fn add_plugin<P: UiPlugin>(&mut self, plugin: P) -> PluginHandle<P> {
136        // Check for duplicate registration
137        let type_id = std::any::TypeId::of::<P>();
138        for existing in &self.plugins {
139            if existing.as_any().type_id() == type_id {
140                panic!(
141                    "Plugin '{}' is already registered",
142                    std::any::type_name::<P>()
143                );
144            }
145        }
146
147        // Register widget types
148        plugin.register_widgets(&mut self.widget_registry);
149
150        // Store the plugin
151        self.plugins.push(Box::new(plugin));
152
153        PluginHandle {
154            _private: (),
155            _marker: PhantomData,
156        }
157    }
158
159    /// Get a handle for an already-registered plugin by type.
160    ///
161    /// Returns `Some(PluginHandle)` if the plugin is registered, `None` otherwise.
162    /// This is useful for obtaining handles to auto-registered plugins
163    /// (e.g., `DockingPlugin`, `ScrollPlugin`).
164    pub fn handle<P: UiPlugin>(&self) -> Option<PluginHandle<P>> {
165        if self.get::<P>().is_some() {
166            Some(PluginHandle {
167                _private: (),
168                _marker: PhantomData,
169            })
170        } else {
171            None
172        }
173    }
174
175    /// Get a reference to a registered plugin by type.
176    pub fn get<P: UiPlugin>(&self) -> Option<&P> {
177        let type_id = std::any::TypeId::of::<P>();
178        for plugin in &self.plugins {
179            if plugin.as_any().type_id() == type_id {
180                return plugin.as_any().downcast_ref::<P>();
181            }
182        }
183        None
184    }
185
186    /// Get a mutable reference to a registered plugin by type.
187    pub fn get_mut<P: UiPlugin>(&mut self) -> Option<&mut P> {
188        let type_id = std::any::TypeId::of::<P>();
189        for plugin in &mut self.plugins {
190            if plugin.as_any().type_id() == type_id {
191                return plugin.as_any_mut().downcast_mut::<P>();
192            }
193        }
194        None
195    }
196
197    /// Get a reference to the widget type registry.
198    pub fn widget_registry(&self) -> &WidgetTypeRegistry {
199        &self.widget_registry
200    }
201
202    /// Dispatch `post_layout` to all plugins in registration order.
203    pub fn post_layout(&mut self, tree: &mut UiTree) {
204        for plugin in &mut self.plugins {
205            plugin.post_layout(tree);
206        }
207    }
208
209    /// Dispatch `update` to all plugins in registration order.
210    pub fn update(&mut self, dt: f32, tree: &mut UiTree) {
211        for plugin in &mut self.plugins {
212            plugin.update(dt, tree);
213        }
214    }
215
216    /// Dispatch an event to all plugins in order. Returns `true` if consumed.
217    pub fn handle_event(&mut self, event: &UiInputEvent, ctx: &mut PluginEventContext<'_>) -> bool {
218        for plugin in &mut self.plugins {
219            if plugin.handle_event(event, ctx) {
220                return true;
221            }
222        }
223        false
224    }
225
226    /// Number of registered plugins.
227    pub fn plugin_count(&self) -> usize {
228        self.plugins.len()
229    }
230}
231
232impl Default for PluginManager {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238/// Core plugin providing built-in widget types.
239///
240/// This plugin is automatically added in `UiCore::new()` — users never
241/// need to add it manually. It registers descriptors for:
242/// Container, Text, Button, TextInput, Image, Row, Column, Tooltip,
243/// HScrollbar, VScrollbar.
244pub struct CorePlugin;
245
246impl UiPlugin for CorePlugin {
247    fn name(&self) -> &str {
248        "core"
249    }
250
251    fn register_widgets(&self, registry: &mut WidgetTypeRegistry) {
252        use crate::widgets::*;
253        use core_widgets::*;
254
255        registry.register::<Container>(
256            WidgetTypeDescriptor::new("Container")
257                .with_render(render_container)
258                .with_overflow(container_overflow),
259        );
260        registry.register::<Text>(
261            WidgetTypeDescriptor::new("Text")
262                .with_render(render_text)
263                .with_caches_measurement(),
264        );
265        registry.register::<Button>(
266            WidgetTypeDescriptor::new("Button")
267                .with_render(render_button)
268                .with_on_hover(button_hover)
269                .with_on_press(button_press)
270                .with_on_click(button_click),
271        );
272        // TextInput has no custom render — uses default style-based rendering
273        registry.register::<TextInput>(
274            WidgetTypeDescriptor::new("TextInput")
275                .with_on_click(text_input_click)
276                .with_on_key_input(text_input_key)
277                .with_on_char_input(text_input_char),
278        );
279        registry.register::<Image>(WidgetTypeDescriptor::new("Image").with_render(render_image));
280        // Row and Column are layout-only — no visual rendering in current code
281        registry.register::<Row>(WidgetTypeDescriptor::new("Row"));
282        registry.register::<Column>(WidgetTypeDescriptor::new("Column"));
283        registry
284            .register::<Tooltip>(WidgetTypeDescriptor::new("Tooltip").with_render(render_tooltip));
285        registry.register::<HScrollbar>(
286            WidgetTypeDescriptor::new("HScrollbar").with_render(render_hscrollbar),
287        );
288        registry.register::<VScrollbar>(
289            WidgetTypeDescriptor::new("VScrollbar").with_render(render_vscrollbar),
290        );
291
292        // ScrollContainer is registered by ScrollPlugin (see scroll_plugin.rs)
293    }
294
295    fn as_any(&self) -> &dyn std::any::Any {
296        self
297    }
298
299    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
300        self
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    struct TestPlugin {
309        initialized: bool,
310    }
311
312    impl TestPlugin {
313        fn new() -> Self {
314            Self { initialized: true }
315        }
316    }
317
318    impl UiPlugin for TestPlugin {
319        fn name(&self) -> &str {
320            "test"
321        }
322
323        fn register_widgets(&self, registry: &mut WidgetTypeRegistry) {
324            // Register a dummy type for testing
325            registry.register::<TestPlugin>(WidgetTypeDescriptor::new("TestWidget"));
326        }
327
328        fn as_any(&self) -> &dyn Any {
329            self
330        }
331
332        fn as_any_mut(&mut self) -> &mut dyn Any {
333            self
334        }
335    }
336
337    #[test]
338    fn test_plugin_manager_add_and_get() {
339        let mut manager = PluginManager::new();
340        let _handle = manager.add_plugin(TestPlugin::new());
341
342        let plugin = manager.get::<TestPlugin>().unwrap();
343        assert!(plugin.initialized);
344        assert_eq!(plugin.name(), "test");
345    }
346
347    #[test]
348    fn test_plugin_manager_get_mut() {
349        let mut manager = PluginManager::new();
350        let _handle = manager.add_plugin(TestPlugin::new());
351
352        let plugin = manager.get_mut::<TestPlugin>().unwrap();
353        plugin.initialized = false;
354
355        let plugin = manager.get::<TestPlugin>().unwrap();
356        assert!(!plugin.initialized);
357    }
358
359    #[test]
360    #[should_panic(expected = "already registered")]
361    fn test_plugin_manager_duplicate_panics() {
362        let mut manager = PluginManager::new();
363        manager.add_plugin(TestPlugin::new());
364        manager.add_plugin(TestPlugin::new()); // should panic
365    }
366
367    #[test]
368    fn test_plugin_handle_is_copy() {
369        let mut manager = PluginManager::new();
370        let handle = manager.add_plugin(TestPlugin::new());
371        let _copy = handle; // Copy
372        let _clone = handle; // still valid after copy
373    }
374
375    #[test]
376    fn test_core_plugin_registers_widgets() {
377        let mut manager = PluginManager::new();
378        manager.add_plugin(CorePlugin);
379
380        let registry = manager.widget_registry();
381        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::Container>()));
382        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::Text>()));
383        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::Button>()));
384        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::TextInput>()));
385        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::Image>()));
386        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::Row>()));
387        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::Column>()));
388        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::Tooltip>()));
389        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::HScrollbar>()));
390        assert!(registry.contains(std::any::TypeId::of::<crate::widgets::VScrollbar>()));
391        // ScrollContainer is registered by ScrollPlugin, not CorePlugin
392    }
393
394    #[test]
395    fn test_scroll_plugin_registers_scroll_container() {
396        let mut manager = PluginManager::new();
397        manager.add_plugin(crate::scroll_plugin::ScrollPlugin::new());
398
399        let registry = manager.widget_registry();
400        assert!(registry.contains(std::any::TypeId::of::<
401            crate::widgets::scroll_container::ScrollContainer,
402        >()));
403    }
404
405    #[test]
406    fn test_widget_type_descriptor_builder() {
407        let desc = WidgetTypeDescriptor::new("Test").with_clips_children(|_| true);
408
409        assert_eq!(desc.name, "Test");
410        assert!(desc.clips_children.is_some());
411        assert!(desc.measure.is_none());
412        assert!(desc.traversal.is_none());
413        assert!(desc.scroll_offset.is_none());
414    }
415
416    #[test]
417    fn test_registry_len() {
418        let mut registry = WidgetTypeRegistry::new();
419        assert!(registry.is_empty());
420        assert_eq!(registry.len(), 0);
421
422        registry.register::<TestPlugin>(WidgetTypeDescriptor::new("Test"));
423        assert!(!registry.is_empty());
424        assert_eq!(registry.len(), 1);
425    }
426}