Skip to main content

ribir-widget-creator

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.

Source facts

Repository
RibirX/Ribir
Last source activity
April 3, 2026 at 09:14
Detected SKILL.md language
English
Stars
1,731
Forks
39

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub