- name
- ribir-widget-creator
- description
- Specialized guide for creating new Ribir UI widgets. Use this skill when asked to create, implement, or refactor a widget in the Ribir framework, especially when the work should include a gallery showcase or a hands-on UI walkthrough.
# Ribir Widget Creation Guide
This guide ensures that all Ribir widgets generated by agents are structurally sound, well-designed, and properly integrate with the framework's reactive UI paradigm. It draws upon the best practices established in components like `NavigationRail` and `InteractiveLayers`, including the live gallery workflow used by `examples/gallery/src/sections/navigation_rail.rs`.
## 0. Core Principles
Ribir widgets follow the **Single Source of Truth** and **Unidirectional Data Flow** principles.
- **Basic Implementation**: See [Custom Widgets Tutorial](../../../docs/en/advanced/custom_widgets.md) for how to structure widgets using `Compose`, `ComposeChild`, and `Template`.
- **Interactive Standards**: See [Interactive Widget Design Standard](../../../docs/en/concepts/interactive-widget-design.md) for advanced data flow, controlled patterns, and optimistic UI.
### Key Principles:
- **Pipe is Authority**: UI state is a projection of data.
- **Strict Control**: Interaction events (Path B) represent user intent and should not directly update state unless via confirmed Path A or optimistic Path C.
### Scope Triage
Start by classifying scope and auditing existing surfaces:
1. **New public widget**: widget + tests + gallery showcase + route + walkthrough.
2. **Existing public widget, user-facing change**: update the current showcase and rerun the walkthrough. Add a new route/page only if none exists.
3. **Internal/private widget**: gallery work only when explicitly requested.
Before adding files or routes, check the widget implementation, theme classes, current showcase, route tables, and tests. Extend the current showcase when possible.
### Delivery Definition of Done
For a new public widget, or a substantial user-facing expansion of one, done means:
- widget implementation + supporting tests
- gallery showcase added or updated
- route added if the widget was not already exposed in gallery
- walkthrough run for user-facing layout, interaction, or navigation changes
Do not create duplicate gallery pages or routes for small fixes when an existing showcase already covers the widget.
## 1. Defining the Widget
Ribir widgets are primarily struct definitions accompanied by the `#[declare]` macro, which automatically implements the builder pattern for properties and events.
### 1.1 The `#[declare]` Macro
Use `#[declare]` on your widget's struct. It generates the infrastructure needed for the DSL to construct the widget.
- **Defaults**: Use `#[declare(default)]` when a field has a sensible default value.
- **Setters & Events**: You can define custom setters and bind events directly in the macro. This enables uncontrolled/TwoWay binding behavior.
- **Smart Pointers**: For public text properties and event payloads, prefer `CowArc<str>` over `String` for performance and memory sharing. Use `String` for internal mutable scratch data only when it genuinely makes the logic clearer.
```rust
#[derive(Clone)]
#[declare]
pub struct MyWidget {
#[declare(default)]
pub title: CowArc<str>,
/// A business selection state
#[declare(default, setter = set_selected, event = MySelectEvent::to.clone())]
pub selected: CowArc<str>,
}
```
### 1.2 Defining Class Names
Use the `class_names!` macro to declare stable stylistic identifiers for your widget and its sub-components. Base classes and mode/state classes should be cleanly separated.
```rust
class_names! {
/// Root container class
MY_WIDGET,
/// Class for selected items
MY_WIDGET_SELECTED,
/// Class for the icon
MY_WIDGET_ICON,
}
```
## 2. Managing Children with Templates
For widgets that contain varying child components, use `#[derive(Template)]`. This allows the DSL to destructure and partition incoming widgets smoothly, supporting optional nodes and varying quantities.
### 2.1 Struct Templates
When a widget expects specific distinct parts (e.g., an icon and an optional label).
```rust
#[derive(Template)]
pub struct MyWidgetChildren<'c> {
pub icon: Widget<'c>,
pub label: Option<TextValue>,
}
```
### 2.2 Enum Templates
When a widget can accept a varying list of heterogenous child types (like a menu, an action, and many navigation items).
```rust
#[derive(Template)]
pub enum MyWidgetChild<'c> {
Header(HeaderWidget),
Item(PairOf<'c, MyItem>),
Footer(FooterWidget),
}
```
You can process these by iterating over `children: Vec<MyWidgetChild<'c>>` and partitioning them as needed before composing the main visual tree.
## 3. Composing the UI
The heart of a widget is its `ComposeChild` implementation, which bridges the declarative struct and the actual widget tree.
### 3.1 Implementing `ComposeChild`
Always implement `ComposeChild<'c>` to define the structure, unless the widget acts completely transparently without children (where `Compose` might be used).
```rust
impl<'c> ComposeChild<'c> for MyWidget {
type Child = MyWidgetChildren<'c>;
fn compose_child(this: impl StateWriter<Value = Self>, child: Self::Child) -> Widget<'c> {
let MyWidgetChildren { icon, label } = child;
fn_widget! {
let mut classes = ClassList::from([MY_WIDGET]);
@Flex {
class: classes,
@ { icon }
@ { label.map(|l| text! { text: l }.into_widget()) }
}
}
.into_widget()
}
}
```
### 3.2 Extracting Context State (Providers)
Leverage `BuildCtx` and `Provider::of` to conditionally render elements based on external context (such as disabling interactive layers or knowing if a rail is expanded).
```rust
let ctx = BuildCtx::get();
if Provider::of::<DisableInteractiveLayer>(ctx).is_some() {
return child; // Escape early if interactivity isn't needed
}
let expanded = Variant::<RailExpanded>::new_or_default(ctx);
```
If your widget needs to distribute state to its descendants, use `@Providers { ... }` or the `providers!` macro.
```rust
providers! {
providers: [
Provider::new(MyContextState::default()),
Provider::writer(this.clone_writer(), None)
],
@Flex { ... }
}
```
## 4. State & Styling Patterns
### 4.1 Stable Base Classes
To avoid unnecessary re-renders or layout jitter (like breaking width smoothing transitions), keep the base class stable. Switch only the active/mode class dynamically when state changes using `class_list!`.
- ✅ **Recommended**:
```rust
let sel_item_cls = item.selected_cls().map(Some);
let item_classes = class_list![RAIL_ITEM, sel_item_cls];
```
### 4.2 Using `FatObj`
Use `@FatObj { ... }` to wrap generic widget elements when attaching events, setting dynamic properties (`visible`), or applying interactive visual layers via `StateLayer` or `FocusIndicator`. It safely merges interactivity to existing child trees.
```rust
@FatObj {
on_action: move |e: &mut Event| { ... },
@Providers {
providers: [...],
@ { content }
}
}
```
## 5. Event Handling & Bubbling
If your widget serves as a container that tracks nested interactions, handle bubbling logic internally. Keep business routing external by emitting custom events.
- Define a custom event type alias on top of `CustomEvent`.
- Catch events from children via `on_action` or `on_tap`.
- Bubble events upwards to expose them cleanly without deeply coupling business state logic inside the view hierarchy.
```rust
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MySelect { pub to: CowArc<str> }
pub type MySelectEvent = CustomEvent<MySelect>;
// Inside ComposeChild:
@FatObj {
on_action: move |e: &mut Event| {
let to = item.key.clone();
e.window().bubble_custom_event(e.target(), MySelect { to });
},
@ { child }
}
```
## 6. Testing Widgets
Ribir tests use a virtual window (`TestWindow`) and a dedicated test environment. Follow these patterns to verify your widget's behavior.
### 6.1 Basic Environment Setup
Always use `reset_test_env!()` at the start of each test to ensure a clean state.
```rust
#[test]
fn my_widget_initial_state() {
reset_test_env!();
let wnd = TestWindow::from_widget(fn_widget! {
@MyWidget { title: "Test" }
});
wnd.draw_frame();
// Assertions...
}
```
### 6.2 Testing Interactions
Use `TestWindow` methods to simulate user input. For events involving position (like clicks), use `map_to_global` to get the correct coordinates for a specific `WidgetId`.
```rust
#[test]
fn widget_click_updates_state() {
reset_test_env!();
let (selected, w_selected) = split_value(false);
let wnd = TestWindow::from_widget(fn_widget! {
@MyWidget {
on_tap: move |_| *$write(w_selected) = true,
}
});
wnd.draw_frame();
// Simulate a click at the center of the window
let pos = Point::new(10., 10.);
wnd.process_cursor_move(pos);
wnd.process_mouse_press(Box::new(DummyDeviceId), MouseButtons::PRIMARY);
wnd.process_mouse_release(Box::new(DummyDeviceId), MouseButtons::PRIMARY);
wnd.draw_frame();
assert!(*selected.read());
}
```
### 6.3 Advanced Layout Assertions
For precision layout testing, use epsilon-based comparisons for floating-point values and verify the full semantic hierarchy.
```rust
fn assert_close(actual: f32, expected: f32, label: &str) {
let eps = 0.5;
assert!((actual - expected).abs() <= eps, "{label} expected {expected}, got {actual}");
}
#[test]
fn layout_precision() {
// ... setup wnd ...
let rect = wnd.layout_info_by_id(id).unwrap().rect;
assert_close(rect.width(), 80.0, "collapsed width");
}
```
### 6.4 Image-Based Testing
Use the `widget_image_tests!` macro from `ribir_dev_helper` to perform visual regression testing. This macro automatically runs the test across different themes and backends.
```rust
use ribir_core::test_helper::*;
use ribir_dev_helper::*;
widget_image_tests!(
my_widget_test_name,
WidgetTester::new(fn_widget! {
@MyWidget { ... }
})
.with_wnd_size(Size::new(200., 200.))
);
```
- **Golden Images**: Images are stored in the `test_cases/` folder at the workspace root, relative to the test source path.
- **Updating Images**: Run tests with `RIBIR_IMG_TEST=overwrite cargo test` to update the golden images when intentional visual changes are made.
### 6.5 Gallery Showcase Integration
When scope triage says the widget should have a public gallery surface, use the `NavigationRail` delivery as the reference pattern.
Typical file touch-list for a new widget showcase:
- `widgets/src/<widget>.rs` (or the widget's main implementation file)
- `examples/gallery/src/sections/<widget>.rs`
- `examples/gallery/src/sections.rs`
- `examples/gallery/src/sections/widgets.rs`
- `examples/gallery/src/app.rs` when route metadata, breadcrumbs, redirects, or shell selection logic need updates
Before creating a new gallery page, first check whether the widget already has a showcase that should be extended instead.
1. **Create a dedicated gallery section** in `examples/gallery/src/sections/<widget>.rs`.
- Expose it as `pub fn page_<widget>() -> Widget<'static>`.
- Wrap the page with `section_page(...)` from `examples/gallery/src/sections/common.rs`.
- Register the new section module in `examples/gallery/src/sections.rs` so the page is actually reachable by the gallery.
2. **Add a Widgets landing entry** in `examples/gallery/src/sections/widgets.rs`.
- The widget should have its own card/tile/entry that explains the demo and navigates to the showcase route.
- Use the gallery's shared visual helpers and styles instead of inventing one-off chrome.
3. **Wire the route under `/widgets/*`**.
- Add the route in `examples/gallery/src/sections/widgets.rs`.
- Update `examples/gallery/src/app.rs` when breadcrumbs, redirects, or top-level rail selection need to know about the new nested widget route.
- Prefer explicit path constants and breadcrumb tables so nested widget routes remain discoverable and testable.
4. **Make the showcase stateful and realistic**.
- Expose the widget's major states, modes, and slots instead of showing only one static pose.
- If the widget has structural toggles (for example optional slots or child topology changes), it is acceptable to rebuild the widget for those combinations.
- Keep ordinary control state and data state reactively bound so the demo behaves like a real app rather than a static pose with disconnected controls.
5. **Prefer representative usage over synthetic micro-demos**.
- Show the widget in a believable application context when possible.
- Reuse shared page scaffolding from `examples/gallery/src/sections/common.rs` and gallery classes from `examples/gallery/src/styles.rs`.
6. **Name important targets for debugging**.
- Add `debug_name` to the showcase root and important interactive controls when it helps the walkthrough target them reliably.
### 6.6 Gallery Route & Showcase Regression Tests
When a widget has a gallery showcase, test that wiring too — not just the widget internals.
1. **Route metadata tests**:
- If `examples/gallery/src/app.rs` contains logic like top-level section mapping, redirects, or breadcrumb tables, add/update tests for the new widget route.
- The `NavigationRail` route tests are the reference pattern here.
2. **Showcase smoke tests**:
- If the new gallery page contains structural toggles, control panels, or multiple major layout branches, add smoke tests that mount the showcase and exercise those branches.
- Non-panicking branch coverage is valuable for demos because the showcase itself is user-facing product surface, not disposable sample code.
3. **Test the real composition path**:
- Prefer mounting the same gallery page composition that users open, rather than only testing tiny helper fragments in isolation.
## 7. Manual Walkthrough & Walkthrough with MCP (Confirming User Experience)
When the widget has a public showcase, automated tests are not enough. Run an MCP walkthrough to confirm the **real user experience (UX)**.
When you create or substantially change a public widget surface, run the walkthrough before declaring the task done. If MCP or gallery launch is unavailable, report that blocker explicitly.
### 7.1 Using the MCP Inspector for UX Verification
1. **Launch the Gallery Showcase**:
- Prefer using the `examples/gallery` demo as the walkthrough surface for public widgets.
- Open the widget through the same gallery route that end users will use, not through an isolated scratch snippet unless the widget is explicitly internal-only.
2. **Verify Navigation Into the Demo**:
- Confirm the Widgets landing entry opens the correct showcase route.
- Confirm gallery rail selection and breadcrumbs remain correct for the nested route.
3. **Visual Consistency Check**:
- Use the inspector to verify the `WidgetTree`, layout bounds, hierarchy, margins, and alignments.
- Compare the default state and the main alternate states (expanded/collapsed, selected/unselected, empty/populated, etc.).
4. **Confirm Interactive "Feel"**:
- Trigger the important user interactions yourself (tap, hover, keyboard focus, toggles, scrolling, mode switches, and slot combinations as applicable).
- Verify that ripples, focus rings, state layers, transitions, and visibility changes feel responsive and intentional.
5. **Contextual Awareness**:
- Verify the widget reacts correctly to providers, parent layout constraints, and realistic content sizes.
- If the gallery demo exposes control-panel toggles like `NavigationRail`, exercise each major branch at least once.
6. **Fix Findings Before Exit**:
- If the walkthrough reveals spacing glitches, clipping, incorrect transitions, broken selection logic, or confusing states, fix them before ending the task whenever practical.
### 7.2 UX Walkthrough Checklist
- [ ] **Gallery Entry Exists**: The widget has a discoverable entry in the gallery Widgets page.
View on GitHub