Skip to main content

astrelis_ui/
lib.rs

1//! Astrelis UI - Taffy-based UI system with WGPU rendering
2//!
3//! This crate provides a flexible UI system built on Taffy layout engine:
4//! - Declarative widget API
5//! - Flexbox and Grid layouts via Taffy
6//! - GPU-accelerated rendering
7//! - Event handling system
8//! - Composable widget tree
9//!
10//! ## Quick Start
11//!
12//! ```rust,no_run
13//! # use astrelis_ui::UiSystem;
14//! # use astrelis_render::{Color, GraphicsContext};
15//! # let graphics_context = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
16//! let mut ui = UiSystem::new(graphics_context);
17//!
18//! ui.build(|root| {
19//!     root.container()
20//!         .width(800.0)
21//!         .height(600.0)
22//!         .padding(20.0)
23//!         .child(|container| {
24//!             container.text("Hello, World!")
25//!                 .size(24.0)
26//!                 .color(Color::WHITE)
27//!                 .build();
28//!             container.button("Click Me").build();
29//!             container.container().build()
30//!         })
31//!         .build();
32//! });
33//!
34//! // In render loop:
35//! // ui.update(delta_time);
36//! // ui.handle_events(&mut event_batch);
37//! // ui.render(&mut render_pass, viewport_size);
38//! ```
39//!
40//! ## API Conventions
41//!
42//! This crate follows consistent method naming conventions:
43//!
44//! ### Mutation Methods
45//! - **`set_*()`** - Full replacement that may trigger complete rebuild/re-layout
46//!   - Example: `set_text()` replaces text and triggers text shaping
47//! - **`update_*()`** - Incremental update optimized with dirty flags
48//!   - Example: `update_text()` only marks TEXT_SHAPING dirty, skipping layout
49//! - **`add_*()`** - Append to a collection
50//!   - Example: `add_widget()` appends to widget tree
51//!
52//! ### Accessor Methods
53//! - **`get_*()`** - Returns `Option<&T>` for possibly-missing values
54//!   - Example: `get_widget(id)` returns `Option<&Widget>`
55//! - **`*()` (no prefix)** - Returns `&T`, panics if unavailable (use when required)
56//!   - Example: `widget(id)` returns `&Widget` or panics
57//! - **`try_*()`** - Fallible operation returning `Result`
58//!   - Example: `try_layout()` returns `Result<(), LayoutError>`
59//! - **`has_*()`** - Boolean check for existence
60//!   - Example: `has_widget(id)` returns `bool`
61//!
62//! ### Computation Methods
63//! - **`compute_*()`** - Expensive computation (results often cached)
64//!   - Example: `compute_layout()` runs Taffy layout solver
65//! - **`calculate_*()`** - Mathematical calculation
66//!   - Example: `calculate_bounds()` computes widget bounds
67//!
68//! ### Builder Methods
69//! - **`with_*(value)`** - Builder method returning `Self` for chaining
70//!   - Example: `with_padding(20.0)` sets padding and returns builder
71//! - **`build()`** - Finalizes builder and consumes it
72//!   - Example: `widget.build()` adds widget to tree
73
74pub mod animation;
75pub mod builder;
76pub mod clip;
77pub mod constraint;
78pub mod constraint_builder;
79pub mod constraint_resolver;
80pub mod culling;
81pub mod debug;
82pub mod dirty;
83pub mod draw_list;
84pub mod event;
85pub mod focus;
86pub mod glyph_atlas;
87pub mod gpu_types;
88pub mod inspector;
89pub mod instance_buffer;
90pub mod layout;
91pub mod layout_engine;
92pub mod length;
93pub mod menu;
94pub mod metrics;
95pub mod metrics_collector;
96pub mod middleware;
97pub mod overlay;
98pub mod plugin;
99pub mod renderer;
100pub mod scroll_plugin;
101pub mod style;
102pub use style::{Overflow, PointerEvents};
103pub mod theme;
104pub mod tooltip;
105pub mod tree;
106pub mod viewport_context;
107pub mod virtual_scroll;
108pub mod widget_id;
109pub mod widgets;
110
111pub use animation::{
112    AnimatableProperty, Animation, AnimationState, AnimationSystem, EasingFunction,
113    WidgetAnimations, bounce, fade_in, fade_out, scale, slide_in_left, slide_in_top, translate_x,
114    translate_y,
115};
116use astrelis_core::geometry::Size;
117pub use clip::{ClipRect, PhysicalClipRect};
118pub use debug::DebugOverlay;
119pub use dirty::{DirtyFlags, DirtyRanges, Versioned};
120pub use draw_list::{DrawCommand, DrawList, ImageCommand, QuadCommand, RenderLayer, TextCommand};
121pub use glyph_atlas::{
122    GlyphBatch, atlas_entry_uv_coords, create_glyph_batches, glyph_to_instance, glyphs_to_instances,
123};
124pub use gpu_types::{ImageInstance, QuadInstance, QuadVertex, TextInstance};
125pub use instance_buffer::InstanceBuffer;
126pub use length::{Length, LengthAuto, LengthPercentage, auto, length, percent, vh, vmax, vmin, vw};
127use std::sync::Arc;
128// Re-export constraint system for advanced responsive layouts
129pub use astrelis_render::ImageSampling;
130pub use astrelis_text::{SyncTextShaper, TextPipeline, TextShapeRequest, TextShaper};
131pub use constraint::{CalcExpr, Constraint};
132pub use constraint_builder::{calc, clamp, max_of, max2, min_of, min2, px};
133pub use constraint_resolver::{ConstraintResolver, ResolveContext};
134pub use metrics::UiMetrics;
135pub use viewport_context::ViewportContext;
136pub use widget_id::{WidgetId, WidgetIdRegistry};
137pub use widgets::{HScrollbar, ScrollbarOrientation, ScrollbarTheme, VScrollbar};
138pub use widgets::{Image, ImageFit, ImageTexture, ImageUV};
139pub use widgets::{ScrollAxis, ScrollContainer, ScrollbarVisibility};
140
141// Re-export main types
142pub use builder::{
143    ContainerNodeBuilder,
144    // Legacy aliases
145    ImageBuilder,
146    IntoNodeBuilder,
147    LeafNodeBuilder,
148    UiBuilder,
149    WidgetBuilder,
150};
151#[cfg(feature = "docking")]
152pub use builder::{DockSplitterNodeBuilder, DockTabsNodeBuilder};
153pub use event::{UiEvent, UiEventSystem};
154pub use focus::{FocusDirection, FocusEvent, FocusManager, FocusPolicy, FocusScopeId};
155pub use layout::LayoutCache;
156pub use renderer::UiRenderer;
157pub use style::Style;
158pub use theme::{ColorPalette, ColorRole, Shapes, Spacing, Theme, ThemeBuilder, Typography};
159pub use tree::{NodeId, UiTree};
160pub use widgets::Widget;
161
162// Re-export new architecture modules
163pub use culling::{AABB, CullingStats, CullingTree};
164pub use inspector::{
165    EditableProperty, InspectorConfig, InspectorGraphs, PropertyEditor, SearchState, TreeViewState,
166    UiInspector, WidgetIdRegistryExt, WidgetKind,
167};
168pub use layout_engine::{LayoutEngine, LayoutMode, LayoutRequest};
169pub use menu::{ContextMenu, MenuBar, MenuItem, MenuStyle};
170pub use metrics_collector::{
171    FrameTimingMetrics, MemoryMetrics, MetricsCollector, MetricsConfig, PerformanceWarning,
172    WidgetMetrics,
173};
174pub use middleware::{
175    InspectorMiddleware, Keybind, KeybindRegistry, MiddlewareContext, MiddlewareManager, Modifiers,
176    OverlayContext, OverlayDrawList, OverlayRenderer, UiMiddleware,
177};
178pub use overlay::{
179    AnchorAlignment, Overlay, OverlayConfig, OverlayId, OverlayManager, OverlayPosition, ZLayer,
180};
181pub use tooltip::{TooltipConfig, TooltipContent, TooltipManager, TooltipPosition};
182pub use virtual_scroll::{
183    ItemHeight, MountedItem, VirtualScrollConfig, VirtualScrollState, VirtualScrollStats,
184    VirtualScrollUpdate, VirtualScrollView,
185};
186
187// Docking system re-exports
188#[cfg(feature = "docking")]
189pub use widgets::docking::{
190    DRAG_THRESHOLD, DockSplitter, DockTabs, DockZone, DockingStyle, DragManager, DragState,
191    DragType, PanelConstraints, SplitDirection, TabScrollIndicator, TabScrollbarPosition,
192};
193
194// Plugin system re-exports
195pub use plugin::{
196    CorePlugin, PluginHandle, PluginManager, TraversalBehavior, UiPlugin, WidgetOverflow,
197    WidgetRenderContext, WidgetTypeDescriptor, WidgetTypeRegistry,
198};
199
200// Re-export common types from dependencies
201pub use astrelis_core::math::{Vec2, Vec4};
202pub use astrelis_render::Color;
203pub use taffy::{
204    AlignContent, AlignItems, Display, FlexDirection, FlexWrap, JustifyContent, Position,
205};
206
207use astrelis_core::profiling::profile_function;
208use astrelis_render::{GraphicsContext, RenderWindow, Viewport};
209use astrelis_winit::event::EventBatch;
210
211// Re-export renderer descriptor for advanced configuration
212pub use renderer::{UiRendererBuilder, UiRendererDescriptor};
213
214/// Render-agnostic UI core managing tree, layout, and logic.
215///
216/// This is the inner layer that doesn't depend on graphics context.
217/// Use this for benchmarks, tests, and headless UI processing.
218pub struct UiCore {
219    tree: UiTree,
220    event_system: UiEventSystem,
221    plugin_manager: PluginManager,
222    viewport_size: Size<f32>,
223    widget_registry: WidgetIdRegistry,
224    viewport: Viewport,
225    theme: Theme,
226}
227
228impl UiCore {
229    /// Create a new render-agnostic UI core.
230    ///
231    /// Automatically adds [`CorePlugin`] and [`ScrollPlugin`](scroll_plugin::ScrollPlugin)
232    /// to register all built-in widget types.
233    /// When the `docking` feature is enabled, also adds [`DockingPlugin`](widgets::docking::plugin::DockingPlugin).
234    pub fn new() -> Self {
235        let mut plugin_manager = PluginManager::new();
236        plugin_manager.add_plugin(CorePlugin);
237        plugin_manager.add_plugin(scroll_plugin::ScrollPlugin::new());
238        #[cfg(feature = "docking")]
239        plugin_manager.add_plugin(widgets::docking::plugin::DockingPlugin::new());
240
241        Self {
242            tree: UiTree::new(),
243            event_system: UiEventSystem::new(),
244            plugin_manager,
245            viewport_size: Size::new(800.0, 600.0),
246            widget_registry: WidgetIdRegistry::new(),
247            viewport: Viewport::default(),
248            theme: Theme::dark(),
249        }
250    }
251
252    /// Build the UI tree using a declarative builder API.
253    pub fn build<F>(&mut self, build_fn: F)
254    where
255        F: FnOnce(&mut UiBuilder),
256    {
257        self.widget_registry.clear();
258        let mut builder = UiBuilder::new(&mut self.tree, &mut self.widget_registry);
259        build_fn(&mut builder);
260        builder.finish();
261    }
262
263    /// Set the viewport size for layout calculations.
264    ///
265    /// When the viewport size changes, any constraints using viewport units
266    /// (vw, vh, vmin, vmax) or complex expressions will be automatically
267    /// re-resolved during the next layout computation.
268    pub fn set_viewport(&mut self, viewport: Viewport) {
269        let new_size = viewport.to_logical().into();
270        let size_changed = self.viewport_size != new_size;
271
272        self.viewport_size = new_size;
273        self.viewport = viewport;
274
275        // If size changed, mark viewport-constrained nodes as dirty
276        if size_changed {
277            self.tree.mark_viewport_dirty();
278        }
279    }
280
281    /// Get the current viewport size.
282    pub fn viewport_size(&self) -> Size<f32> {
283        self.viewport_size
284    }
285
286    /// Compute layout without font rendering (uses approximate text sizing).
287    pub fn compute_layout(&mut self) {
288        #[cfg(feature = "docking")]
289        {
290            let padding = self
291                .plugin_manager
292                .get::<widgets::docking::plugin::DockingPlugin>()
293                .map(|p| p.docking_context.style().content_padding)
294                .unwrap_or(0.0);
295            self.tree.set_docking_content_padding(padding);
296        }
297        let widget_registry = self.plugin_manager.widget_registry();
298        self.tree
299            .compute_layout(self.viewport_size, None, widget_registry);
300        // Invalidate docking cache so stale layout coordinates are refreshed
301        #[cfg(feature = "docking")]
302        if let Some(dp) = self
303            .plugin_manager
304            .get_mut::<widgets::docking::plugin::DockingPlugin>()
305        {
306            dp.invalidate_cache();
307        }
308    }
309
310    /// Run plugin post-layout hooks.
311    ///
312    /// This dispatches `post_layout` to all registered plugins, allowing them to
313    /// perform custom post-processing (e.g., ScrollPlugin updating content/viewport sizes).
314    pub fn run_post_layout_plugins(&mut self) {
315        self.plugin_manager.post_layout(&mut self.tree);
316    }
317
318    /// Compute layout with instrumentation for performance metrics.
319    pub fn compute_layout_instrumented(&mut self) -> UiMetrics {
320        let widget_registry = self.plugin_manager.widget_registry();
321        let metrics =
322            self.tree
323                .compute_layout_instrumented(self.viewport_size, None, widget_registry);
324        #[cfg(feature = "docking")]
325        if let Some(dp) = self
326            .plugin_manager
327            .get_mut::<widgets::docking::plugin::DockingPlugin>()
328        {
329            dp.invalidate_cache();
330        }
331        metrics
332    }
333
334    /// Get the node ID for a widget ID.
335    pub fn get_node_id(&self, widget_id: WidgetId) -> Option<NodeId> {
336        self.widget_registry.get_node(widget_id)
337    }
338
339    /// Register a widget ID to node ID mapping.
340    pub fn register_widget(&mut self, widget_id: WidgetId, node_id: NodeId) {
341        self.widget_registry.register(widget_id, node_id);
342    }
343
344    /// Update text content of a Text widget by ID with automatic dirty marking.
345    ///
346    /// Returns true if the content changed.
347    pub fn update_text(&mut self, widget_id: WidgetId, new_content: impl Into<String>) -> bool {
348        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
349            self.tree.update_text_content(node_id, new_content)
350        } else {
351            false
352        }
353    }
354
355    /// Update button label by ID with automatic dirty marking.
356    ///
357    /// Returns true if the label changed.
358    pub fn update_button_label(
359        &mut self,
360        widget_id: WidgetId,
361        new_label: impl Into<String>,
362    ) -> bool {
363        if let Some(node_id) = self.widget_registry.get_node(widget_id)
364            && let Some(node) = self.tree.get_node_mut(node_id)
365            && let Some(button) = node.widget.as_any_mut().downcast_mut::<widgets::Button>()
366        {
367            let changed = button.set_label(new_label);
368            if changed {
369                self.tree
370                    .mark_dirty_flags(node_id, DirtyFlags::TEXT_SHAPING);
371            }
372            return changed;
373        }
374        false
375    }
376
377    /// Update text input value by ID with automatic dirty marking.
378    ///
379    /// Returns true if the value changed.
380    pub fn update_text_input(&mut self, widget_id: WidgetId, new_value: impl Into<String>) -> bool {
381        if let Some(node_id) = self.widget_registry.get_node(widget_id)
382            && let Some(node) = self.tree.get_node_mut(node_id)
383            && let Some(input) = node
384                .widget
385                .as_any_mut()
386                .downcast_mut::<widgets::TextInput>()
387        {
388            let changed = input.set_value(new_value);
389            if changed {
390                self.tree
391                    .mark_dirty_flags(node_id, DirtyFlags::TEXT_SHAPING);
392            }
393            return changed;
394        }
395        false
396    }
397
398    /// Update widget color by ID with automatic dirty marking.
399    ///
400    /// Returns true if the color changed.
401    pub fn update_color(&mut self, widget_id: WidgetId, color: astrelis_render::Color) -> bool {
402        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
403            self.tree.update_color(node_id, color)
404        } else {
405            false
406        }
407    }
408
409    /// Update widget opacity by ID with automatic dirty marking.
410    ///
411    /// Opacity is inherited: `computed_opacity = parent_opacity * node_opacity`.
412    /// Returns true if the value changed.
413    pub fn update_opacity(&mut self, widget_id: WidgetId, opacity: f32) -> bool {
414        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
415            self.tree.update_opacity(node_id, opacity)
416        } else {
417            false
418        }
419    }
420
421    /// Update widget visual translation by ID with automatic dirty marking.
422    ///
423    /// Translation is visual-only (does not affect layout).
424    /// Returns true if the value changed.
425    pub fn update_translate(&mut self, widget_id: WidgetId, translate: Vec2) -> bool {
426        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
427            self.tree.update_translate(node_id, translate)
428        } else {
429            false
430        }
431    }
432
433    /// Update widget visual X translation by ID.
434    pub fn update_translate_x(&mut self, widget_id: WidgetId, x: f32) -> bool {
435        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
436            self.tree.update_translate_x(node_id, x)
437        } else {
438            false
439        }
440    }
441
442    /// Update widget visual Y translation by ID.
443    pub fn update_translate_y(&mut self, widget_id: WidgetId, y: f32) -> bool {
444        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
445            self.tree.update_translate_y(node_id, y)
446        } else {
447            false
448        }
449    }
450
451    /// Update widget visual scale by ID.
452    pub fn update_scale(&mut self, widget_id: WidgetId, scale: Vec2) -> bool {
453        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
454            self.tree.update_scale(node_id, scale)
455        } else {
456            false
457        }
458    }
459
460    /// Update widget visual X scale by ID.
461    pub fn update_scale_x(&mut self, widget_id: WidgetId, x: f32) -> bool {
462        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
463            self.tree.update_scale_x(node_id, x)
464        } else {
465            false
466        }
467    }
468
469    /// Update widget visual Y scale by ID.
470    pub fn update_scale_y(&mut self, widget_id: WidgetId, y: f32) -> bool {
471        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
472            self.tree.update_scale_y(node_id, y)
473        } else {
474            false
475        }
476    }
477
478    /// Set widget visibility by ID.
479    ///
480    /// When `false`, collapses from layout (like CSS `display: none`).
481    /// Returns true if the value changed.
482    pub fn set_visible(&mut self, widget_id: WidgetId, visible: bool) -> bool {
483        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
484            self.tree.set_visible(node_id, visible)
485        } else {
486            false
487        }
488    }
489
490    /// Toggle widget visibility by ID.
491    ///
492    /// Returns true if the value changed.
493    pub fn toggle_visible(&mut self, widget_id: WidgetId) -> bool {
494        if let Some(node_id) = self.widget_registry.get_node(widget_id) {
495            self.tree.toggle_visible(node_id)
496        } else {
497            false
498        }
499    }
500
501    /// Get mutable access to the tree.
502    pub fn tree_mut(&mut self) -> &mut UiTree {
503        &mut self.tree
504    }
505
506    /// Get reference to the tree.
507    pub fn tree(&self) -> &UiTree {
508        &self.tree
509    }
510
511    /// Get reference to the event system.
512    pub fn events(&self) -> &UiEventSystem {
513        &self.event_system
514    }
515
516    /// Get mutable access to the event system.
517    pub fn event_system_mut(&mut self) -> &mut UiEventSystem {
518        &mut self.event_system
519    }
520
521    /// Get reference to the widget registry.
522    pub fn widget_registry(&self) -> &WidgetIdRegistry {
523        &self.widget_registry
524    }
525
526    /// Set the theme, marking all widget colors dirty.
527    pub fn set_theme(&mut self, theme: Theme) {
528        self.theme = theme;
529        self.tree.mark_all_dirty(DirtyFlags::COLOR);
530    }
531
532    /// Get a reference to the current theme.
533    pub fn theme(&self) -> &Theme {
534        &self.theme
535    }
536
537    /// Get a reference to the docking style.
538    #[cfg(feature = "docking")]
539    pub fn docking_style(&self) -> &widgets::docking::DockingStyle {
540        self.plugin_manager
541            .get::<widgets::docking::plugin::DockingPlugin>()
542            .expect("DockingPlugin is auto-added when docking feature is enabled")
543            .docking_context
544            .style()
545    }
546
547    /// Replace the docking style.
548    #[cfg(feature = "docking")]
549    pub fn set_docking_style(&mut self, style: widgets::docking::DockingStyle) {
550        self.plugin_manager
551            .get_mut::<widgets::docking::plugin::DockingPlugin>()
552            .expect("DockingPlugin is auto-added when docking feature is enabled")
553            .docking_context
554            .set_style(style);
555    }
556
557    /// Add a plugin to the UI core and return a handle for typed access.
558    ///
559    /// The plugin's widget types are registered immediately.
560    ///
561    /// # Panics
562    ///
563    /// Panics if a plugin of the same concrete type is already registered.
564    pub fn add_plugin<P: UiPlugin>(&mut self, plugin: P) -> PluginHandle<P> {
565        self.plugin_manager.add_plugin(plugin)
566    }
567
568    /// Get a handle for an already-registered plugin.
569    ///
570    /// Returns `Some(PluginHandle)` if the plugin is registered, `None` otherwise.
571    /// Useful for obtaining handles to auto-registered plugins.
572    pub fn plugin_handle<P: UiPlugin>(&self) -> Option<PluginHandle<P>> {
573        self.plugin_manager.handle::<P>()
574    }
575
576    /// Get a reference to a registered plugin by type, using a handle as proof.
577    pub fn plugin<P: UiPlugin>(&self, _handle: &PluginHandle<P>) -> &P {
578        self.plugin_manager
579            .get::<P>()
580            .expect("plugin handle guarantees registration")
581    }
582
583    /// Get a mutable reference to a registered plugin by type, using a handle as proof.
584    pub fn plugin_mut<P: UiPlugin>(&mut self, _handle: &PluginHandle<P>) -> &mut P {
585        self.plugin_manager
586            .get_mut::<P>()
587            .expect("plugin handle guarantees registration")
588    }
589
590    /// Get a reference to the plugin manager.
591    pub fn plugin_manager(&self) -> &PluginManager {
592        &self.plugin_manager
593    }
594
595    /// Get a mutable reference to the plugin manager.
596    pub fn plugin_manager_mut(&mut self) -> &mut PluginManager {
597        &mut self.plugin_manager
598    }
599
600    /// Handle events from the event batch.
601    pub fn handle_events(&mut self, events: &mut EventBatch) {
602        self.event_system.handle_events_with_plugins(
603            events,
604            &mut self.tree,
605            &mut self.plugin_manager,
606        );
607    }
608}
609
610impl Default for UiCore {
611    fn default() -> Self {
612        Self::new()
613    }
614}
615
616/// Main UI system managing tree, layout, rendering, and events.
617///
618/// This wraps UiCore and adds rendering capabilities.
619pub struct UiSystem {
620    core: UiCore,
621    renderer: UiRenderer,
622}
623
624impl UiSystem {
625    /// Create a new UI system with default configuration (no depth testing).
626    ///
627    /// **Warning:** This creates a renderer without depth testing. If your render pass
628    /// has a depth attachment, use [`from_window`](Self::from_window) instead to ensure
629    /// pipeline-renderpass compatibility.
630    ///
631    /// # Example
632    ///
633    /// ```rust,no_run
634    /// # use astrelis_ui::UiSystem;
635    /// # use astrelis_render::GraphicsContext;
636    /// // For simple use without depth testing
637    /// let graphics = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
638    /// let ui = UiSystem::new(graphics);
639    /// ```
640    pub fn new(context: Arc<GraphicsContext>) -> Self {
641        profile_function!();
642        Self {
643            core: UiCore::new(),
644            renderer: UiRenderer::new(context),
645        }
646    }
647
648    /// Create a new UI system configured for a specific window.
649    ///
650    /// This is the **recommended** constructor as it ensures the renderer's pipelines
651    /// are compatible with the window's render pass configuration (surface format
652    /// and depth format).
653    ///
654    /// # Example
655    ///
656    /// ```rust,no_run
657    /// # use astrelis_ui::UiSystem;
658    /// # use astrelis_render::{GraphicsContext, RenderWindow, RenderWindowBuilder};
659    /// # use astrelis_winit::window::Window;
660    /// # fn example(winit_window: Window) {
661    /// let graphics = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
662    ///
663    /// // Create window with depth buffer
664    /// let window = RenderWindowBuilder::new()
665    ///     .with_depth_default()
666    ///     .build(winit_window, graphics.clone())
667    ///     .expect("Failed to create window");
668    ///
669    /// // UI automatically uses matching formats
670    /// let ui = UiSystem::from_window(graphics, &window);
671    /// # }
672    /// ```
673    pub fn from_window(context: Arc<GraphicsContext>, window: &RenderWindow) -> Self {
674        profile_function!();
675        Self {
676            core: UiCore::new(),
677            renderer: UiRenderer::from_window(context, window),
678        }
679    }
680
681    /// Create a new UI system with explicit configuration.
682    ///
683    /// Use this when you need full control over the renderer configuration,
684    /// or when the target is not a `RenderWindow`.
685    ///
686    /// # Example
687    ///
688    /// ```rust,no_run
689    /// # use astrelis_ui::{UiSystem, UiRendererDescriptor};
690    /// # use astrelis_render::{GraphicsContext, wgpu};
691    /// let graphics = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
692    ///
693    /// let desc = UiRendererDescriptor {
694    ///     name: "Game HUD".to_string(),
695    ///     surface_format: wgpu::TextureFormat::Bgra8UnormSrgb,
696    ///     depth_format: Some(wgpu::TextureFormat::Depth32Float),
697    /// };
698    ///
699    /// let ui = UiSystem::with_descriptor(graphics, desc);
700    /// ```
701    pub fn with_descriptor(
702        context: Arc<GraphicsContext>,
703        descriptor: UiRendererDescriptor,
704    ) -> Self {
705        profile_function!();
706        Self {
707            core: UiCore::new(),
708            renderer: UiRenderer::with_descriptor(context, descriptor),
709        }
710    }
711
712    /// Build the UI tree using a declarative builder API.
713    ///
714    /// Note: This does a full rebuild. For incremental updates, use update methods.
715    pub fn build<F>(&mut self, build_fn: F)
716    where
717        F: FnOnce(&mut UiBuilder),
718    {
719        // Clear the draw list since we're rebuilding the tree
720        // This prevents stale draw commands from accumulating
721        self.renderer.clear_draw_list();
722        self.core.build(build_fn);
723    }
724
725    /// Update UI state (animations, hover, etc.).
726    ///
727    /// Note: This no longer marks the entire tree dirty - only changed widgets are marked.
728    pub fn update(&mut self, delta_time: f32) {
729        // Update plugin animations (docking ghost tabs, panel transitions, etc.)
730        #[cfg(feature = "docking")]
731        if let Some(dp) = self
732            .core
733            .plugin_manager
734            .get_mut::<widgets::docking::plugin::DockingPlugin>()
735        {
736            dp.update_animations(delta_time);
737        }
738
739        let _ = delta_time;
740    }
741
742    /// Set the viewport size for layout calculations.
743    pub fn set_viewport(&mut self, viewport: Viewport) {
744        self.renderer.set_viewport(viewport);
745        self.core.set_viewport(viewport);
746    }
747
748    /// Handle events from the event batch.
749    pub fn handle_events(&mut self, events: &mut EventBatch) {
750        self.core.handle_events(events);
751    }
752
753    /// Compute layout for all widgets.
754    pub fn compute_layout(&mut self) {
755        let viewport_size = self.core.viewport_size();
756        let font_renderer = self.renderer.font_renderer();
757        #[cfg(feature = "docking")]
758        {
759            let padding = self
760                .core
761                .plugin_manager
762                .get::<widgets::docking::plugin::DockingPlugin>()
763                .map(|p| p.docking_context.style().content_padding)
764                .unwrap_or(0.0);
765            self.core.tree.set_docking_content_padding(padding);
766        }
767        let widget_registry = self.core.plugin_manager.widget_registry();
768        self.core
769            .tree
770            .compute_layout(viewport_size, Some(font_renderer), widget_registry);
771        // Invalidate docking cache so stale layout coordinates are refreshed
772        #[cfg(feature = "docking")]
773        if let Some(dp) = self
774            .core
775            .plugin_manager
776            .get_mut::<widgets::docking::plugin::DockingPlugin>()
777        {
778            dp.invalidate_cache();
779        }
780    }
781
782    /// Get the node ID for a widget ID.
783    pub fn get_node_id(&self, widget_id: WidgetId) -> Option<tree::NodeId> {
784        self.core.get_node_id(widget_id)
785    }
786
787    /// Register a widget ID to node ID mapping.
788    pub fn register_widget(&mut self, widget_id: WidgetId, node_id: tree::NodeId) {
789        self.core.register_widget(widget_id, node_id);
790    }
791
792    /// Update text content of a Text widget by ID with automatic dirty marking.
793    ///
794    /// This is much faster than rebuilding the entire UI tree.
795    /// Returns true if the content changed.
796    ///
797    /// # Example
798    /// ```no_run
799    /// # use astrelis_ui::{UiSystem, WidgetId};
800    /// # use astrelis_render::GraphicsContext;
801    /// # let context = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
802    /// # let mut ui = UiSystem::new(context);
803    /// let counter_id = WidgetId::new("counter");
804    /// ui.update_text(counter_id, "Count: 42");
805    /// ```
806    pub fn update_text(&mut self, widget_id: WidgetId, new_content: impl Into<String>) -> bool {
807        self.core.update_text(widget_id, new_content)
808    }
809
810    /// Get text cache statistics from the renderer.
811    pub fn text_cache_stats(&self) -> String {
812        self.renderer.text_cache_stats()
813    }
814
815    /// Get text cache hit rate.
816    pub fn text_cache_hit_rate(&self) -> f32 {
817        self.renderer.text_cache_hit_rate()
818    }
819
820    /// Log text cache statistics.
821    pub fn log_text_cache_stats(&self) {
822        self.renderer.log_text_cache_stats();
823    }
824
825    /// Update button label by ID with automatic dirty marking.
826    ///
827    /// Returns true if the label changed.
828    pub fn update_button_label(
829        &mut self,
830        widget_id: WidgetId,
831        new_label: impl Into<String>,
832    ) -> bool {
833        self.core.update_button_label(widget_id, new_label)
834    }
835
836    /// Update text input value by ID with automatic dirty marking.
837    ///
838    /// Returns true if the value changed.
839    pub fn update_text_input(&mut self, widget_id: WidgetId, new_value: impl Into<String>) -> bool {
840        self.core.update_text_input(widget_id, new_value)
841    }
842
843    /// Update widget color by ID with automatic dirty marking.
844    ///
845    /// Returns true if the color changed.
846    pub fn update_color(&mut self, widget_id: WidgetId, color: astrelis_render::Color) -> bool {
847        self.core.update_color(widget_id, color)
848    }
849
850    /// Update widget opacity by ID with automatic dirty marking.
851    pub fn update_opacity(&mut self, widget_id: WidgetId, opacity: f32) -> bool {
852        self.core.update_opacity(widget_id, opacity)
853    }
854
855    /// Update widget visual translation by ID.
856    pub fn update_translate(&mut self, widget_id: WidgetId, translate: Vec2) -> bool {
857        self.core.update_translate(widget_id, translate)
858    }
859
860    /// Update widget visual X translation by ID.
861    pub fn update_translate_x(&mut self, widget_id: WidgetId, x: f32) -> bool {
862        self.core.update_translate_x(widget_id, x)
863    }
864
865    /// Update widget visual Y translation by ID.
866    pub fn update_translate_y(&mut self, widget_id: WidgetId, y: f32) -> bool {
867        self.core.update_translate_y(widget_id, y)
868    }
869
870    /// Update widget visual scale by ID.
871    pub fn update_scale(&mut self, widget_id: WidgetId, scale: Vec2) -> bool {
872        self.core.update_scale(widget_id, scale)
873    }
874
875    /// Update widget visual X scale by ID.
876    pub fn update_scale_x(&mut self, widget_id: WidgetId, x: f32) -> bool {
877        self.core.update_scale_x(widget_id, x)
878    }
879
880    /// Update widget visual Y scale by ID.
881    pub fn update_scale_y(&mut self, widget_id: WidgetId, y: f32) -> bool {
882        self.core.update_scale_y(widget_id, y)
883    }
884
885    /// Set widget visibility by ID.
886    pub fn set_visible(&mut self, widget_id: WidgetId, visible: bool) -> bool {
887        self.core.set_visible(widget_id, visible)
888    }
889
890    /// Toggle widget visibility by ID.
891    pub fn toggle_visible(&mut self, widget_id: WidgetId) -> bool {
892        self.core.toggle_visible(widget_id)
893    }
894
895    /// Render the UI using retained mode instanced rendering.
896    ///
897    /// This is the high-performance path that only updates dirty nodes
898    /// and uses GPU instancing for efficient rendering.
899    ///
900    /// Note: This automatically computes layout. If you need to control
901    /// layout computation separately (e.g., for middleware freeze functionality),
902    /// use `compute_layout()` + `render_without_layout()` instead.
903    pub fn render(&mut self, render_pass: &mut astrelis_render::wgpu::RenderPass) {
904        profile_function!();
905        let logical_size = self.core.viewport_size();
906        // Sync docking content padding before layout
907        #[cfg(feature = "docking")]
908        {
909            let padding = self
910                .core
911                .plugin_manager
912                .get::<widgets::docking::plugin::DockingPlugin>()
913                .map(|p| p.docking_context.style().content_padding)
914                .unwrap_or(0.0);
915            self.core.tree.set_docking_content_padding(padding);
916        }
917        // Compute layout if dirty (clears layout-related dirty flags)
918        let font_renderer = self.renderer.font_renderer();
919        let widget_registry = self.core.plugin_manager.widget_registry();
920        self.core
921            .tree
922            .compute_layout(logical_size, Some(font_renderer), widget_registry);
923
924        // Invalidate docking cache so stale layout coordinates are refreshed
925        #[cfg(feature = "docking")]
926        if let Some(dp) = self
927            .core
928            .plugin_manager
929            .get_mut::<widgets::docking::plugin::DockingPlugin>()
930        {
931            dp.invalidate_cache();
932        }
933
934        // Compute accurate tab widths using text shaping (docking only)
935        #[cfg(feature = "docking")]
936        {
937            let font_renderer = self.renderer.font_renderer();
938            crate::widgets::docking::compute_all_tab_widths(self.core.tree_mut(), font_renderer);
939        }
940
941        // Run plugin post-layout hooks (ScrollPlugin updates content/viewport sizes)
942        self.core.run_post_layout_plugins();
943
944        // Clean up draw commands for nodes removed since last frame
945        let removed = self.core.tree_mut().drain_removed_nodes();
946        if !removed.is_empty() {
947            self.renderer.remove_stale_nodes(&removed);
948        }
949
950        // Render using retained mode (processes paint-only dirty flags)
951        #[cfg(feature = "docking")]
952        {
953            // Get cross-container preview and animations from DockingPlugin
954            let (preview, animations) = self
955                .core
956                .plugin_manager
957                .get::<widgets::docking::plugin::DockingPlugin>()
958                .map(|dp| (dp.cross_container_preview, &dp.dock_animations))
959                .unzip();
960            let widget_registry = self.core.plugin_manager.widget_registry();
961
962            self.renderer.render_instanced_with_preview(
963                self.core.tree(),
964                render_pass,
965                self.core.viewport,
966                preview.flatten().as_ref(),
967                animations,
968                widget_registry,
969            );
970        }
971
972        #[cfg(not(feature = "docking"))]
973        {
974            let widget_registry = self.core.plugin_manager.widget_registry();
975            self.renderer.render_instanced(
976                self.core.tree(),
977                render_pass,
978                self.core.viewport,
979                widget_registry,
980            );
981        }
982
983        // Clear all dirty flags after rendering
984        // (layout computation no longer clears flags - renderer owns this)
985        self.core.tree_mut().clear_dirty_flags();
986    }
987
988    /// Render the UI without computing layout.
989    ///
990    /// Use this when you want to manually control layout computation,
991    /// for example when implementing layout freeze functionality with middleware.
992    ///
993    /// # Parameters
994    /// - `render_pass`: The WGPU render pass to render into
995    /// - `clear_dirty_flags`: Whether to clear dirty flags after rendering.
996    ///   Set to `false` when layout is frozen to preserve dirty state for inspection.
997    ///
998    /// Typical usage:
999    /// ```ignore
1000    /// // Check if middleware wants to freeze layout
1001    /// let skip_layout = middlewares.pre_layout(&ctx);
1002    /// if !skip_layout {
1003    ///     ui.compute_layout();
1004    /// }
1005    /// // Don't clear flags when frozen so inspector can keep showing them
1006    /// ui.render_without_layout(render_pass, !skip_layout);
1007    /// ```
1008    pub fn render_without_layout(
1009        &mut self,
1010        render_pass: &mut astrelis_render::wgpu::RenderPass,
1011        clear_dirty_flags: bool,
1012    ) {
1013        profile_function!();
1014        // Run plugin post-layout hooks (ScrollPlugin updates content/viewport sizes)
1015        self.core.run_post_layout_plugins();
1016
1017        // Clean up draw commands for nodes removed since last frame
1018        let removed = self.core.tree_mut().drain_removed_nodes();
1019        if !removed.is_empty() {
1020            self.renderer.remove_stale_nodes(&removed);
1021        }
1022
1023        // Render using retained mode (processes paint-only dirty flags)
1024        let widget_registry = self.core.plugin_manager.widget_registry();
1025        self.renderer.render_instanced(
1026            self.core.tree(),
1027            render_pass,
1028            self.core.viewport,
1029            widget_registry,
1030        );
1031
1032        // Clear dirty flags unless we're in a frozen state
1033        if clear_dirty_flags {
1034            self.core.tree_mut().clear_dirty_flags();
1035        }
1036    }
1037
1038    /// Get mutable access to the core for advanced usage.
1039    pub fn core_mut(&mut self) -> &mut UiCore {
1040        &mut self.core
1041    }
1042
1043    /// Get reference to the core.
1044    pub fn core(&self) -> &UiCore {
1045        &self.core
1046    }
1047
1048    /// Get mutable access to the tree for advanced usage.
1049    pub fn tree_mut(&mut self) -> &mut UiTree {
1050        self.core.tree_mut()
1051    }
1052
1053    /// Get reference to the tree.
1054    pub fn tree(&self) -> &UiTree {
1055        self.core.tree()
1056    }
1057
1058    /// Get mutable access to the event system.
1059    pub fn event_system_mut(&mut self) -> &mut UiEventSystem {
1060        self.core.event_system_mut()
1061    }
1062
1063    /// Get reference to the font renderer.
1064    pub fn font_renderer(&self) -> &astrelis_text::FontRenderer {
1065        self.renderer.font_renderer()
1066    }
1067
1068    /// Set the theme, marking all widget colors dirty.
1069    pub fn set_theme(&mut self, theme: Theme) {
1070        self.renderer.set_theme_colors(theme.colors.clone());
1071        self.core.set_theme(theme);
1072    }
1073
1074    /// Get a reference to the current theme.
1075    pub fn theme(&self) -> &Theme {
1076        self.core.theme()
1077    }
1078
1079    /// Get a reference to the docking style.
1080    #[cfg(feature = "docking")]
1081    pub fn docking_style(&self) -> &widgets::docking::DockingStyle {
1082        self.core.docking_style()
1083    }
1084
1085    /// Replace the docking style.
1086    #[cfg(feature = "docking")]
1087    pub fn set_docking_style(&mut self, style: widgets::docking::DockingStyle) {
1088        self.core.set_docking_style(style);
1089    }
1090
1091    /// Add a plugin to the UI system and return a handle for typed access.
1092    ///
1093    /// The plugin's widget types are registered immediately.
1094    ///
1095    /// # Panics
1096    ///
1097    /// Panics if a plugin of the same concrete type is already registered.
1098    pub fn add_plugin<P: UiPlugin>(&mut self, plugin: P) -> PluginHandle<P> {
1099        self.core.add_plugin(plugin)
1100    }
1101
1102    /// Get a handle for an already-registered plugin.
1103    ///
1104    /// Returns `Some(PluginHandle)` if the plugin is registered, `None` otherwise.
1105    /// Useful for obtaining handles to auto-registered plugins.
1106    pub fn plugin_handle<P: UiPlugin>(&self) -> Option<PluginHandle<P>> {
1107        self.core.plugin_handle::<P>()
1108    }
1109
1110    /// Get a reference to a registered plugin by type, using a handle as proof.
1111    pub fn plugin<P: UiPlugin>(&self, handle: &PluginHandle<P>) -> &P {
1112        self.core.plugin(handle)
1113    }
1114
1115    /// Get a mutable reference to a registered plugin by type, using a handle as proof.
1116    pub fn plugin_mut<P: UiPlugin>(&mut self, handle: &PluginHandle<P>) -> &mut P {
1117        self.core.plugin_mut(handle)
1118    }
1119
1120    /// Get a reference to the plugin manager.
1121    pub fn plugin_manager(&self) -> &PluginManager {
1122        self.core.plugin_manager()
1123    }
1124
1125    /// Get the current renderer configuration.
1126    ///
1127    /// Returns the descriptor used to create the renderer, including surface format
1128    /// and depth format settings.
1129    ///
1130    /// # Example
1131    ///
1132    /// ```rust,no_run
1133    /// # use astrelis_ui::UiSystem;
1134    /// # use astrelis_render::GraphicsContext;
1135    /// # fn example(ui: &UiSystem) {
1136    /// let desc = ui.renderer_descriptor();
1137    /// println!("Surface format: {:?}", desc.surface_format);
1138    /// # }
1139    /// ```
1140    pub fn renderer_descriptor(&self) -> &UiRendererDescriptor {
1141        self.renderer.descriptor()
1142    }
1143
1144    /// Reconfigure the renderer with new format settings.
1145    ///
1146    /// Call this when the target surface format changes (e.g., window moved
1147    /// to a different monitor).
1148    ///
1149    /// # Example
1150    ///
1151    /// ```rust,no_run
1152    /// # use astrelis_ui::{UiSystem, UiRendererDescriptor};
1153    /// # use astrelis_render::{GraphicsContext, RenderWindow};
1154    /// # fn example(ui: &mut UiSystem, window: &RenderWindow) {
1155    /// // Window moved to different monitor - surface format may have changed
1156    /// ui.reconfigure(UiRendererDescriptor::from_window(window));
1157    /// # }
1158    /// ```
1159    pub fn reconfigure(&mut self, descriptor: UiRendererDescriptor) {
1160        self.renderer.reconfigure(descriptor);
1161    }
1162
1163    /// Reconfigure from a window, inheriting its format configuration.
1164    ///
1165    /// Convenience method for updating the renderer when a window's surface
1166    /// format changes (e.g., when moved to a different monitor).
1167    ///
1168    /// # Example
1169    ///
1170    /// ```rust,no_run
1171    /// # use astrelis_ui::UiSystem;
1172    /// # use astrelis_render::{GraphicsContext, RenderWindow};
1173    /// # fn example(ui: &mut UiSystem, window: &RenderWindow) {
1174    /// // Handle surface format change after window moved
1175    /// ui.reconfigure_from_window(window);
1176    /// # }
1177    /// ```
1178    pub fn reconfigure_from_window(&mut self, window: &RenderWindow) {
1179        self.renderer.reconfigure_from_window(window);
1180    }
1181}