-
Notifications
You must be signed in to change notification settings - Fork 0
Description
1. Introduction
This document outlines the architecture and implementation plan for the next evolution of the whip_ui framework. This development cycle is a strategic refactoring aimed at transforming whip_ui from a functional proof-of-concept into a production-ready, ergonomic, and high-performance UI solution.
Development will focus on four core pillars:
- Radically Improving Developer Experience (DX): A simplified, single-plugin API with convention-based asset organization.
- Establishing a Robust & Predictable Data-Driven Workflow: A hierarchical TOML format with a CSS-like style cascade and resilient, non-crashing validation.
- Creating a Fully Reactive & Asynchronous UI Model: Foundational hooks for state binding (including async data) and a type-safe, decoupled event system.
- Implementing a High-Performance Rendering Architecture: Decoupling the renderer from the ECS via an extract-and-batch pipeline.
2. Philosophy & Guiding Principles
- Developer Experience is Paramount: The framework's entry point will be a single, configurable
WhipUiPlugin. This abstracts away boilerplate and provides a clean, extensible builder pattern. - Data-Driven & Hierarchical: The UI definition format will use a nested
[[children]]structure in TOML that directly mirrors the visual hierarchy, enhancing readability and maintainability. - Convention over Configuration: The framework will expect a standardized asset directory structure (e.g.,
assets/ui/layouts/,assets/ui/widgets/,assets/ui/styles/) to simplify asset resolution and ensure project consistency. - Predictable & State-Driven Styling: A CSS-inspired cascade (Inline > State > Class > Base) will be used for styling, providing a powerful and familiar model. Styles will react to interaction states like
:hoverand:pressed. - Fail-Gracefully and Be Explicit: The framework will validate data definitions at load time. An invalid definition will not crash the application. Instead, it will log a clear, detailed error to the console and skip loading the invalid node, allowing the rest of the UI to function.
- Bevy-Idiomatic by Default: We will leverage Bevy's core features:
AssetServerfor loading,bevy_reflectfor state binding,Parent/Childrenfor the live UI hierarchy, and the Extract-to-Render-World pattern. This ensures performance, thread safety, and compatibility. - Type-Safety over Strings: Where possible, "stringly-typed" APIs will be avoided. Event types will be defined in a Rust
enum, turning potential runtime typos into immediate, clear deserialization errors.
3. Core Architectural Decisions
-
Plugin Architecture: A single
WhipUiPlugin::new("path/to/root.toml")will be the primary entry point, configurable via a builder pattern (.register_state::<T>(),.register_action::<T>()). It will handle its own asset loading to configure the window. -
TOML Format & Style Cascade: The format will support hierarchical definition, style sheet inclusion, and a clear, state-aware styling cascade.
- Example
assets/ui/styles/theme.toml:[styles.button] background_color = "0x3182CEFF" # Blue border_radius = [8, 8, 8, 8] [styles.button.states.hover] background_color = "0x2B6CB0FF" # Darker Blue [styles.button.states.pressed] background_color = "0x2C5282FF" # Even Darker Blue transform_scale = [0.98, 0.98]
- Example
assets/ui/layouts/main.toml:include_styles = "theme.toml" [[children]] widget_type = "Button" style_class = "button" [bind] text = "GameState.button_label" [on] Clicked = "IncrementCounter" # Type-safe event handler
- Example
-
Asset Pipeline (Resilient Loading): The
UiAssetLoaderwill parse the hierarchical TOML. During parsing, it will validate all bindings, actions, and style classes against aUiRegistry. On validation failure, the loader will log a detailed error and skip the invalid node and its descendants, ensuring the application remains running. -
Rendering Architecture (Extract & Batch): The final architecture will be fully decoupled.
- Definition: The main world contains data-only components:
VisualStyle(color, border radius) andShape(unit geometry). - Extraction: An
extract_ui_to_render_worldsystem queries these components and generates a list of transientUiRenderPrimitives. - Rendering: A new
UiBatchRenderPipelinein the renderer will consume this list of primitives, writing all vertex data into large, shared GPU buffers to minimize draw calls.
- Definition: The main world contains data-only components:
-
Binding & Event System:
- Asynchronous Data: For external data, the framework will support binding to a state machine
Resource(e.g.,Enum { Loading, Success(T), Error(String) }). This allows the UI to display placeholders while data is loading, preventing layout shifts. - Type-Safe Actions: The
[on]table will use a strongly-typedUiInteractionenum (Clicked,HoverStart, etc.) as keys, providing compile-time-like safety for data definitions.
- Asynchronous Data: For external data, the framework will support binding to a state machine
4. Actionable Implementation Plan
Phase 1: The Ergonomic Entry Point & Cleanup
- Goal: Establish the
WhipUiPluginas the single, self-contained entry point for the framework. - Code Targets:
whip_ui_example/src/main.rs(to be simplified)whip_ui/src/lib.rs(to define the new plugin)whip_ui/src/assets/plugin.rs(to be absorbed by the new plugin)
- Tasks (
whip_uicrate):- Create
WhipUiPlugin: Define a new publicWhipUiPluginstruct inwhip_ui/src/lib.rs. It will take the root layout path in its constructor (e.g.,WhipUiPlugin::new("ui/layouts/main.toml")). - Consolidate Plugins: The
WhipUiPlugin::buildmethod will internally addGuiFrameworkCorePlugin,GuiFrameworkInteractionPlugin,TaffyLayoutPlugin, and the newUiAssetPluginV2(from Phase 2). It will also register all necessary events and resources. - Implement Asynchronous Window Config:
- The plugin will send a
LoadUiRequestfor its root layout file. - Create a new system,
apply_window_config_from_asset, that listens forAssetEvent<UiDefinition>::Loaded. - When the root asset loads, this system will query the
WindowConfigfrom the asset and apply it to thePrimaryWindowentity, replacing the synchronous hack. It will also spawn the initial background quad.
- The plugin will send a
- Create
- Tasks (
whip_ui_examplecrate):- DELETE the
load_window_configandparse_window_config_directfunctions frommain.rs. - DELETE the manual
add_plugins(...)calls for all theGuiFramework*andUiAssetPluginplugins. - REPLACE all of it with a single line:
.add_plugins(WhipUiPlugin::new("ui/layouts/main.toml")). - DELETE the
setup_scene_ecssystem, as its responsibilities (background quad, initial UI load) will now be handled by the plugin.
- DELETE the
- Testing Strategy:
- Integration Test: Create a test that builds a minimal Bevy
AppwithWhipUiPlugin. Runapp.update()several times. Assert that theWindowresource has been modified to match the settings inmain.tomland that theBackgroundQuadentity has been spawned.
- Integration Test: Create a test that builds a minimal Bevy
- Success Criteria: The example app runs identically, but
whip_ui_example/src/main.rsis dramatically smaller and contains no framework-specific setup logic.
Phase 2: Hierarchical, Resilient, & Composable Data
- Goal: Replace the flat, brittle
UiTreeand manual parser with aserde-driven, hierarchical, and validated asset pipeline that builds a proper Bevy ECS hierarchy. - Code Targets:
assets/mod.rs(UiTree,UiAssetLoader,parse_custom_toml_structure)widgets/blueprint.rs(all structs)assets/ui/layouts/main.toml(to be converted)
- Tasks (
whip_uicrate):- Define New
serdeStructs: In a newwhip_ui/src/definition.rsmodule, create the new data structures for parsing the TOML file. These structs represent the source data, not the final scene graph.// The top-level struct that represents a loaded .toml UI file. // This is the actual Asset. #[derive(Asset, TypePath, Deserialize)] pub struct UiDefinition { pub window: Option<WindowConfig>, #[serde(default)] pub include_styles: Vec<String>, // The root node of the UI defined in this file. pub root: WidgetNode, } // A recursive node structure for parsing the TOML hierarchy. #[derive(Deserialize, Default)] pub struct WidgetNode { // "include" takes precedence over "widget_type" during spawning. // An "include" points to another .toml file to be used as a blueprint. pub include: Option<String>, // A "widget_type" is a named blueprint registered in the UiRegistry. pub widget_type: Option<String>, // Style-related fields pub style_class: Option<String>, #[serde(default)] pub style: StyleOverride, // For inline styles and state overrides // Hierarchy and Logic #[serde(default)] pub children: Vec<WidgetNode>, #[serde(default)] pub bind: HashMap<String, String>, #[serde(default)] pub on: HashMap<UiInteraction, String>, } // A container for inline style overrides, including states. #[derive(Deserialize, Default)] pub struct StyleOverride { #[serde(flatten)] pub base: StyleProperties, // The normal style properties #[serde(default)] pub states: HashMap<InteractionState, StyleProperties>, // e.g., hover, pressed } // The enum for interaction states, used as keys in the style map. #[derive(Deserialize, PartialEq, Eq, Hash)] pub enum InteractionState { Hover, Pressed, } // NOTE: The `StyleOverride` struct includes the `states` field for hover/pressed // effects. This is defined here to finalize the data structure, but the systems // that actually READ and APPLY these states will be implemented in Phase 4. // Phase 2 is only concerned with parsing this structure correctly.
- Create
UiRegistry: Define aResourceto be populated by the plugin's builder methods. This registry will map widget type names to asset paths (e.g.,"Button"->"ui/widgets/button.toml") and hold registered state types for binding validation. - Refactor
UiAssetLoader:- Rename to
UiDefinitionLoaderand change itsAssettype toUiDefinition. - DELETE all manual parsing functions.
- The
loadmethod will usetoml::from_strto parse the file into theUiDefinitionstruct. - Implement Resilience: After parsing, traverse the
UiDefinitiontree and use theUiRegistryto validatewidget_type,style_class,bindkeys, andonactions. On failure, log a detailed error.
- Rename to
- Refactor Spawning Logic: The
spawn_ui_treesystem will be rewritten. It will take the rootWidgetNodefrom the loadedUiDefinitionasset and recursively traverse it. For eachWidgetNode, it will spawn a BevyEntityand usecommands.entity(parent).add_child(child)to construct the live UI hierarchy using Bevy'sParent/Childrencomponents. - DELETE the old data structures:
UiTree,WidgetBlueprint,WidgetCollection, and the entirewidgets/blueprint.rsfile.
- Define New
- Testing Strategy:
- Integration Test: Load a TOML file. Traverse the resulting entity hierarchy using
Query<&Children>and assert that the structure in the ECS matches the nested structure in the TOML file.
- Integration Test: Load a TOML file. Traverse the resulting entity hierarchy using
- Success Criteria: The UI is spawned as a proper Bevy hierarchy. Invalid nodes in the TOML are skipped and logged without crashing the app.
Phase 3: The Reactive Action & Binding System
- Goal: Implement the full reactive loop, driven by the
UiRegistryfor validation. - Code Targets:
gui_framework/plugins/interaction.rs(interaction_system)gui_framework/events/interaction_events.rs- New systems for binding and action translation.
- Tasks (
whip_uicrate):- Define Semantic Events & Components: Create
UiInteractionenum (derivingDeserialize,Eq,Hash),UiAction(String)event,EventHandlerscomponent, andDataBindingscomponent. - Refactor
interaction_system: It will query theEventHandlerscomponent on a clicked entity and send the correspondingUiActionevent. - Implement
binding_system:- This system will use
bevy_reflectto perform data binding. - It will query for entities with a
DataBindingscomponent. - For each binding (e.g.,
"Text.value" -> "GameState.button_label"), it will first query theUiRegistryto find the registeredGameStateresource. - It will then use reflection on that resource to get the value and apply it to the target component property on the entity.
- This system will use
- Define Semantic Events & Components: Create
- Testing Strategy:
- Binding Test: Attempt to bind to a property on a resource that was not registered with
.register_state(). Assert that the asset loader produces a clear error.
- Binding Test: Attempt to bind to a property on a resource that was not registered with
- Success Criteria: Clicking a button modifies a registered resource, and the UI automatically updates. Bindings to unregistered states fail at load time.
Phase 4: State-Driven & Cascading Styles
- Goal: Replace the inflexible
ShapeData.colorwith a full, CSS-like style cascade that reacts to interaction states. - Code Targets:
gui_framework/components/shape_data.rs(to be refactored)gui_framework/plugins/interaction.rs(to add state markers)
- Tasks (
whip_uicrate):- Add State Markers: Enhance
interaction_systemtocommands.entity(e).insert(Hovered)andremove<Hovered>as the mouse enters/leaves an interactive entity. Do the same for aPressedmarker component. - Refactor
ShapeData:- Create a new
VisualStylecomponent to hold all style properties (background_color,border_color,border_radius, etc.). - Create a new
Shapecomponent to hold only unit geometry (e.g.,Shape::Quad). - DELETE the
colorandscalingfields fromShapeData. RenameShapeDatatoShapeVerticesor similar, as it will only hold the final, computed vertex data for the renderer.
- Create a new
- Implement
style_resolver_system:- Data Access Prerequisite: This system relies on
Handles being stored as components. The spawning logic (from Phase 2) must be updated to add the appropriateHandle<WidgetBlueprint>andHandle<StyleSheetAsset>to each entity it creates. - System Triggers: The system will run on
Added<Styleable>entities, and also re-run whenever an entity's state markers change (i.e., onChanged<Hovered>orChanged<Pressed>). - Cascade Implementation: The system will perform the following steps to compute the final style for an entity:
a. Query the entity for itsHandle<WidgetBlueprint>andHandle<StyleSheetAsset>.
b. Access the globalAssets<WidgetBlueprint>andAssets<StyleSheetAsset>resources.
c. Apply Base Style: Use the blueprint handle to look up the asset and establish the base styles.
d. Apply Class Style: Use the stylesheet handle and the entity'sstyle_classstring to look up and merge the class styles over the base.
e. Apply Inline Style: Merge styles from the entity's inlineStyleOverridecomponent.
f. Apply State Style: If aHoveredorPressedmarker component exists on the entity, merge the corresponding state styles, withPressedtaking priority overHovered. - Finally, the system inserts the final, computed
VisualStylecomponent onto the entity.
- Data Access Prerequisite: This system relies on
- Create Temporary Adapter: Create a
visual_style_to_shape_data_adaptersystem. It reads the finalVisualStyleand writes thebackground_colorto the oldShapeData.colorfield. This keeps the current renderer working until Phase 5.
- Add State Markers: Enhance
- Testing Strategy:
- Visual Regression Testing: Create a test that spawns a button, programmatically adds the
Hoveredcomponent, renders a frame, and compares it to a "golden" image of the hovered state.
- Visual Regression Testing: Create a test that spawns a button, programmatically adds the
- Success Criteria: A button in the example app changes color on hover and press. The styling is defined in a separate
theme.tomlfile and applied via astyle_class.
Phase 5: High-Performance Decoupled Rendering
- Goal: Replace the per-entity rendering architecture with a modern, high-performance extract-and-batch pipeline.
- Code Targets:
- The entire
gui_framework/renderingmodule. gui_framework/components/text_layout.rs(TextRenderData).
- The entire
- Tasks (
whip_uicrate):- Define
UiRenderPrimitive: A transient struct passed to the render world, containingMat4transform, color, shape info (like border radius), etc. - Implement
extract_ui_to_render_worldsystem:- This system runs in the
Extractschedule. - It queries for entities with
VisualStyle,Shape,GlobalTransform, andUiNode(for Taffy size) in the main world. - It generates a
Vec<UiRenderPrimitive>and inserts it as a resource into the render world.
- This system runs in the
- Gut the Renderer:
- DELETE the
HashMap<Entity, ...>caches fromBufferManagerandTextRenderer. - The
rendersystem inrender_engine.rsno longer takesQuerys. It takesRes<Vec<UiRenderPrimitive>>. - Refactor the
BufferManagerto manage a few large, dynamicvk::Buffers for per-frame batching.
- DELETE the
- Final Cleanup:
- DELETE the
TextRenderDatacomponent. - DELETE the
visual_style_to_shape_data_adaptersystem from Phase 4. - DELETE the old
ShapeDatacomponent, replacing it fully with theVisualStyle/Shapemodel.
- DELETE the
- Define
- Testing Strategy:
- Performance Benchmark: Create a test that spawns 10,000 simple shapes. Measure the frame time before and after this phase.
- Success Criteria: The application renders correctly with significantly improved performance. The codebase is cleaner, with all per-entity GPU resources and coupled rendering logic removed.
5. Future Considerations & Long-Term Vision
This proposal lays the architectural foundation for a powerful and ergonomic UI framework. Upon its successful completion, the following high-impact features will be prioritized to build upon this new base, with a special focus on enabling AI-driven UI development.
-
Live Visual UI Editor:
- Concept: A built-in "edit mode," toggled by a developer hotkey, that allows for direct manipulation of the running UI.
- Implementation: This will specifically leverage the
toml_editcrate. Unlike the standardtomlcrate used for initial loading,toml_editis designed to parse and re-serialize TOML files while preserving comments, whitespace, and formatting. This makes it the ideal choice for a non-destructive editor that respects the original file's structure.
-
AI-Centric Control Protocol (MCP) Server:
- Concept: Expose the framework's core functionality through a machine-readable API, likely a JSON-RPC or WebSocket server. This is the primary interface for an AI agent to interact with the UI framework.
- Implementation: The server would provide endpoints for querying, manipulation, and receiving event streams, allowing an AI to fully drive the UI.
-
Advanced Built-in Widget Library:
- Concept: Expand the library of pre-built widget blueprints to include more complex, high-level components.
- Implementation Suggestions:
Node(for node graph editors),DataTable,ScrollableContainer.
-
Animation & Transitions System:
- Concept: A system for defining and running animations on UI properties for smooth visual transitions.
-
Enhanced Accessibility (a11y):
- Concept: Deepen the integration with
bevy_a11yto ensure the UI is fully navigable and usable by assistive technologies and AI agents.
- Concept: Deepen the integration with
6. Plan Enhancements: Current Code vs. Post-Proposal
This table summarizes the key transformations this development proposal will bring to the whip_ui framework, comparing the current state of the codebase to the state after successful implementation.
| Aspect | Current Codebase State | State After Implementing This Proposal | Why it's Better |
|---|---|---|---|
| Framework Setup | Manual & Verbose: whip_ui_example manually adds ~5 plugins and performs synchronous file I/O in main.rs for window setup. |
Automated & Self-Contained: A single WhipUiPlugin handles all setup, including asynchronous window configuration from the root layout file. |
Massive DX Improvement. Reduces boilerplate to a single line, removes hacks, and makes the framework trivial to integrate. |
| Data Definition | Flat & Brittle: UiAssetLoader manually parses a flat [widgets] table. The structure is rigid and not intuitive. |
Hierarchical & Composable: A serde-driven parser loads an intuitive, nested [[children]] structure. Supports include for blueprints and include_styles for themes. |
Readability & Scalability. The data format now mirrors the visual hierarchy, making it vastly easier to write and maintain complex UIs. Composition enables a reusable component ecosystem. |
| Data Validation | None: A typo in a widget ID or a missing property in the TOML leads to a runtime panic or silent failure. | Resilient & Validated: Invalid bindings, actions, or style classes are caught at load time. An error is logged, and the invalid widget is skipped without crashing the app. | Robustness & DX. Critical for hot-reloading. Provides immediate, non-fatal feedback, drastically improving the development loop for humans and AI. |
| Styling System | Inflexible & Mixed: Style properties like color are mixed directly into data components (ShapeData). No concept of reuse or state changes. |
State-Driven Style Cascade: A full cascade (Inline > State > Class > Base) resolves styles from multiple sources. Styles can react to Hovered and Pressed states. |
Power & Predictability. Provides an industry-standard, powerful, and reusable styling system. Enables visually responsive and interactive UI without custom logic. |
| Rendering Architecture | Coupled & Per-Entity: GPU handles (vk::Buffer) are stored in ECS components (TextRenderData). BufferManager creates unique buffers for every single shape. |
Decoupled & Batched: An extract system gathers data into transient UiRenderPrimitives. The renderer consumes this data to draw into large, shared buffers. |
Performance & Correctness. The standard modern engine architecture. It's vastly more performant, thread-safe, and decouples game logic from rendering, enabling easier maintenance and future platform support. |
| State Management | Non-existent: No mechanism for UI to react to application state changes. | Reflect-Based & Asynchronous-Aware: A binding_system uses bevy_reflect to sync Resource data to UI components. Supports a state machine pattern for handling data that isn't immediately available. |
Powerful & Idiomatic. Creates a fully reactive UI. Solves the difficult problem of handling asynchronous data (e.g., network requests) without layout shifts or blocking the app. |
| Event Handling | Low-Level: Systems listen for raw EntityClicked events and must know the implementation details of the UI. |
Decoupled & Semantic: Raw input events are translated into high-level, semantic UiAction events (e.g., IncrementCounter). The [on] table is type-safe. |
Clean Application Logic. Decouples the UI from the business logic. The application only needs to care about meaningful actions, not raw input, making the code cleaner and more maintainable. |