Skip to content

UI Layout Enhancments #15

@MrScripty

Description

@MrScripty

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:

  1. Radically Improving Developer Experience (DX): A simplified, single-plugin API with convention-based asset organization.
  2. Establishing a Robust & Predictable Data-Driven Workflow: A hierarchical TOML format with a CSS-like style cascade and resilient, non-crashing validation.
  3. Creating a Fully Reactive & Asynchronous UI Model: Foundational hooks for state binding (including async data) and a type-safe, decoupled event system.
  4. 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 :hover and :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: AssetServer for loading, bevy_reflect for state binding, Parent/Children for 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
  • Asset Pipeline (Resilient Loading): The UiAssetLoader will parse the hierarchical TOML. During parsing, it will validate all bindings, actions, and style classes against a UiRegistry. 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.

    1. Definition: The main world contains data-only components: VisualStyle (color, border radius) and Shape (unit geometry).
    2. Extraction: An extract_ui_to_render_world system queries these components and generates a list of transient UiRenderPrimitives.
    3. Rendering: A new UiBatchRenderPipeline in the renderer will consume this list of primitives, writing all vertex data into large, shared GPU buffers to minimize draw calls.
  • 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-typed UiInteraction enum (Clicked, HoverStart, etc.) as keys, providing compile-time-like safety for data definitions.

4. Actionable Implementation Plan


Phase 1: The Ergonomic Entry Point & Cleanup

  • Goal: Establish the WhipUiPlugin as 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_ui crate):
    1. Create WhipUiPlugin: Define a new public WhipUiPlugin struct in whip_ui/src/lib.rs. It will take the root layout path in its constructor (e.g., WhipUiPlugin::new("ui/layouts/main.toml")).
    2. Consolidate Plugins: The WhipUiPlugin::build method will internally add GuiFrameworkCorePlugin, GuiFrameworkInteractionPlugin, TaffyLayoutPlugin, and the new UiAssetPluginV2 (from Phase 2). It will also register all necessary events and resources.
    3. Implement Asynchronous Window Config:
      • The plugin will send a LoadUiRequest for its root layout file.
      • Create a new system, apply_window_config_from_asset, that listens for AssetEvent<UiDefinition>::Loaded.
      • When the root asset loads, this system will query the WindowConfig from the asset and apply it to the PrimaryWindow entity, replacing the synchronous hack. It will also spawn the initial background quad.
  • Tasks (whip_ui_example crate):
    1. DELETE the load_window_config and parse_window_config_direct functions from main.rs.
    2. DELETE the manual add_plugins(...) calls for all the GuiFramework* and UiAssetPlugin plugins.
    3. REPLACE all of it with a single line: .add_plugins(WhipUiPlugin::new("ui/layouts/main.toml")).
    4. DELETE the setup_scene_ecs system, as its responsibilities (background quad, initial UI load) will now be handled by the plugin.
  • Testing Strategy:
    • Integration Test: Create a test that builds a minimal Bevy App with WhipUiPlugin. Run app.update() several times. Assert that the Window resource has been modified to match the settings in main.toml and that the BackgroundQuad entity has been spawned.
  • Success Criteria: The example app runs identically, but whip_ui_example/src/main.rs is dramatically smaller and contains no framework-specific setup logic.

Phase 2: Hierarchical, Resilient, & Composable Data

  • Goal: Replace the flat, brittle UiTree and manual parser with a serde-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_ui crate):
    1. Define New serde Structs: In a new whip_ui/src/definition.rs module, 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.
    2. Create UiRegistry: Define a Resource to 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.
    3. Refactor UiAssetLoader:
      • Rename to UiDefinitionLoader and change its Asset type to UiDefinition.
      • DELETE all manual parsing functions.
      • The load method will use toml::from_str to parse the file into the UiDefinition struct.
      • Implement Resilience: After parsing, traverse the UiDefinition tree and use the UiRegistry to validate widget_type, style_class, bind keys, and on actions. On failure, log a detailed error.
    4. Refactor Spawning Logic: The spawn_ui_tree system will be rewritten. It will take the root WidgetNode from the loaded UiDefinition asset and recursively traverse it. For each WidgetNode, it will spawn a Bevy Entity and use commands.entity(parent).add_child(child) to construct the live UI hierarchy using Bevy's Parent/Children components.
    5. DELETE the old data structures: UiTree, WidgetBlueprint, WidgetCollection, and the entire widgets/blueprint.rs file.
  • 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.
  • 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 UiRegistry for 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_ui crate):
    1. Define Semantic Events & Components: Create UiInteraction enum (deriving Deserialize, Eq, Hash), UiAction(String) event, EventHandlers component, and DataBindings component.
    2. Refactor interaction_system: It will query the EventHandlers component on a clicked entity and send the corresponding UiAction event.
    3. Implement binding_system:
      • This system will use bevy_reflect to perform data binding.
      • It will query for entities with a DataBindings component.
      • For each binding (e.g., "Text.value" -> "GameState.button_label"), it will first query the UiRegistry to find the registered GameState resource.
      • It will then use reflection on that resource to get the value and apply it to the target component property on the entity.
  • 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.
  • 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.color with 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_ui crate):
    1. Add State Markers: Enhance interaction_system to commands.entity(e).insert(Hovered) and remove<Hovered> as the mouse enters/leaves an interactive entity. Do the same for a Pressed marker component.
    2. Refactor ShapeData:
      • Create a new VisualStyle component to hold all style properties (background_color, border_color, border_radius, etc.).
      • Create a new Shape component to hold only unit geometry (e.g., Shape::Quad).
      • DELETE the color and scaling fields from ShapeData. Rename ShapeData to ShapeVertices or similar, as it will only hold the final, computed vertex data for the renderer.
    3. 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 appropriate Handle<WidgetBlueprint> and Handle<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., on Changed<Hovered> or Changed<Pressed>).
      • Cascade Implementation: The system will perform the following steps to compute the final style for an entity:
        a. Query the entity for its Handle<WidgetBlueprint> and Handle<StyleSheetAsset>.
        b. Access the global Assets<WidgetBlueprint> and Assets<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's style_class string to look up and merge the class styles over the base.
        e. Apply Inline Style: Merge styles from the entity's inline StyleOverride component.
        f. Apply State Style: If a Hovered or Pressed marker component exists on the entity, merge the corresponding state styles, with Pressed taking priority over Hovered.
      • Finally, the system inserts the final, computed VisualStyle component onto the entity.
    4. Create Temporary Adapter: Create a visual_style_to_shape_data_adapter system. It reads the final VisualStyle and writes the background_color to the old ShapeData.color field. This keeps the current renderer working until Phase 5.
  • Testing Strategy:
    • Visual Regression Testing: Create a test that spawns a button, programmatically adds the Hovered component, renders a frame, and compares it to a "golden" image of the hovered state.
  • Success Criteria: A button in the example app changes color on hover and press. The styling is defined in a separate theme.toml file and applied via a style_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/rendering module.
    • gui_framework/components/text_layout.rs (TextRenderData).
  • Tasks (whip_ui crate):
    1. Define UiRenderPrimitive: A transient struct passed to the render world, containing Mat4 transform, color, shape info (like border radius), etc.
    2. Implement extract_ui_to_render_world system:
      • This system runs in the Extract schedule.
      • It queries for entities with VisualStyle, Shape, GlobalTransform, and UiNode (for Taffy size) in the main world.
      • It generates a Vec<UiRenderPrimitive> and inserts it as a resource into the render world.
    3. Gut the Renderer:
      • DELETE the HashMap<Entity, ...> caches from BufferManager and TextRenderer.
      • The render system in render_engine.rs no longer takes Querys. It takes Res<Vec<UiRenderPrimitive>>.
      • Refactor the BufferManager to manage a few large, dynamic vk::Buffers for per-frame batching.
    4. Final Cleanup:
      • DELETE the TextRenderData component.
      • DELETE the visual_style_to_shape_data_adapter system from Phase 4.
      • DELETE the old ShapeData component, replacing it fully with the VisualStyle/Shape model.
  • 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_edit crate. Unlike the standard toml crate used for initial loading, toml_edit is 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_a11y to ensure the UI is fully navigable and usable by assistive technologies and AI agents.

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.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions