astrelis_ui/plugin/
mod.rs1pub 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
41pub trait UiPlugin: Any + 'static {
49 fn name(&self) -> &str;
51
52 fn register_widgets(&self, registry: &mut WidgetTypeRegistry);
56
57 fn handle_event(&mut self, _event: &UiInputEvent, _ctx: &mut PluginEventContext<'_>) -> bool {
62 false
63 }
64
65 fn post_layout(&mut self, _tree: &mut UiTree) {}
67
68 fn update(&mut self, _dt: f32, _tree: &mut UiTree) {}
70
71 fn as_any(&self) -> &dyn Any;
73
74 fn as_any_mut(&mut self) -> &mut dyn Any;
76}
77
78pub 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
111pub struct PluginManager {
117 plugins: Vec<Box<dyn UiPlugin>>,
118 widget_registry: WidgetTypeRegistry,
119}
120
121impl PluginManager {
122 pub fn new() -> Self {
124 Self {
125 plugins: Vec::new(),
126 widget_registry: WidgetTypeRegistry::new(),
127 }
128 }
129
130 pub fn add_plugin<P: UiPlugin>(&mut self, plugin: P) -> PluginHandle<P> {
136 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 plugin.register_widgets(&mut self.widget_registry);
149
150 self.plugins.push(Box::new(plugin));
152
153 PluginHandle {
154 _private: (),
155 _marker: PhantomData,
156 }
157 }
158
159 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 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 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 pub fn widget_registry(&self) -> &WidgetTypeRegistry {
199 &self.widget_registry
200 }
201
202 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 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 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 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
238pub 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 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 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 }
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 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()); }
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; let _clone = handle; }
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 }
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}