| name | ribir-style-and-cleanliness |
| description | Specialized for code style and cleanliness within the Ribir UI framework. Use when working with Ribir DSL (@, rdl!, pipe!), state management ($read, $write, part_writer), or performance optimizations. |
Ribir Style & Cleanliness Guide
This guide ensures that all Ribir code generated by agents meets expert-level standards for readability, performance, and maintainability.
1. DSL Elegance (The $ & @ Syntax)
1.1 Prefer the @ Symbol
Within rdl! and related macros (e.g., fn_widget!, widget!), prioritize using @ to declare widgets instead of the raw rdl! syntax.
- ✅ Recommended:
@Flex { ... }
- ❌ Avoid:
rdl! { Flex { ... } }
1.2 $ / @ Syntax Boundary
$read(s), $write(s), and @Widget { ... } are only valid inside Ribir macro context. Standard Rust code cannot use them directly, even inside closures or helper functions unless those expressions are written inside fn_widget!, rdl!, widget!, or another Ribir DSL macro.
Only the $ family and @ syntax have this boundary. Ordinary macros, function-style helpers such as fn_widget!, button!, text!, flex!, container!, and APIs such as part_writer can be used wherever normal Rust code allows them.
For simple one-line child calls or macro invocations, prefer removing unnecessary braces:
-
@ { my_widget() } -> @my_widget()
-
@ { some_macro!(...) } -> @some_macro!(...)
-
Do not apply this rule to a bare name or single variable such as @ { child }.
-
Keep @ { ... } when the expression is multi-line, contains local bindings or control flow, is a bare name, or is clearer with an explicit block.
-
Avoid Borrow Conflicts: Never mix $read and $write on the same state within a single expression (this causes a Rust RefCell runtime panic).
-
Do Not Assume Closure Context Is Enough: move |_| ... is still plain Rust unless the closure appears inside a Ribir macro body.
1.3 Mandatory move Capture
Always explicitly use the move keyword in all event closures (e.g., on_tap, on_pointer_move).
- ✅ Recommended:
on_tap: move |_| ...
- ❌ Avoid:
on_tap: |_| ... (leads to lifetime conflicts)
1.4 Automatic State Capture (Prefer Automatic Capture)
Within Ribir macros, prioritize the automatic capture mechanisms provided by the framework. Avoid manual calls to .clone_writer(), .clone_reader(), or manually defining clone variables outside the macro.
The macros automatically handle capture and lifecycle for:
-
Read/Write: $read(s), $write(s), $writer(s)
-
Subscription/Observation: $reader(s), $watcher(s)
-
Reference Cloning: $clone(s)
-
✅ Recommended (Idiomatic: Concise & Declarative):
button! {
on_tap: move |_| *$write(cnt) += 1,
@ { pipe!($read(cnt).to_string()) }
}
-
❌ Avoid (Non-idiomatic: Verbose boilerplate):
let c_cnt = cnt.clone_writer();
button! {
on_tap: move |_| *c_cnt.write() += 1,
@ { pipe!(...) }
}
1.5 Use Widget Function Macros For Root Widgets
Prefer the most direct widget function macro when a helper function returns a single root widget tree. Use button!, text!, flex!, container!, and similar helpers when they make the root flatter. Most common widgets already have a corresponding function macro; the list in this guide is illustrative, not exhaustive.
Inside an existing DSL tree, prefer normal child declarations such as @FilledButton { ... } instead of switching to @some_macro! just because a function macro exists.
Use fn_widget! only when you need a small DSL scope for local bindings, helper values, or $-based reads around the root tree.
When a function macro name conflicts with another macro in scope, qualify it explicitly.
- ✅ Recommended (Root helper stays flat):
fn title() -> Widget<'static> {
flex! {
direction: Direction::Horizontal,
align_items: Align::Center,
@Text { text: "Hello" }
@pipe!($read(cnt).to_string())
}
.into_widget()
}
- ✅ Recommended (Nested child keeps declarer form):
@FilledButton {
on_tap: move |_| submit(),
@ { "Submit" }
}
- ❌ Avoid (Unnecessary nesting at root):
fn title() -> Widget<'static> {
fn_widget! {
@Flex {
direction: Direction::Horizontal,
align_items: Align::Center,
@Text { text: "Hello" }
}
}
.into_widget()
}
- ❌ Avoid (Switching child style without a reason):
@filled_button! {
on_tap: move |_| submit(),
@ { "Submit" }
}
1.6 Prefer Direct Layout Macros For Root Widgets
flex!, stack!, and similar layout macros are the preferred form when a widget can be expressed as a single root layout tree.
Use fn_widget! only when you need Rust logic before the tree description, such as local bindings, control flow, computed values, or state setup.
- ✅ Recommended: direct layout macro for a simple widget tree
fn title() -> Widget<'static> {
flex! {
direction: Direction::Horizontal,
@Text { text: "Title" }
}
.into_widget()
}
- ❌ Avoid: wrapping a single root layout tree in
fn_widget! without a reason
fn title() -> Widget<'static> {
fn_widget! {
@Flex {
direction: Direction::Horizontal,
@Text { text: "Title" }
}
}
.into_widget()
}
1.7 DSL Minimization & Logic Separation
DSL should only describe UI tree structure and declarative property bindings. If logic does not involve specific DSL transformations (like $read, pipe!, @) or is complex, extract it outside the macro as standard Rust code.
When you do need state reads inside DSL, place the read as close as possible to the expression that consumes it instead of hoisting it upward.
If the only local binding exists for one child list or subtree, prefer placing it inside that specific @ { ... } block rather than wrapping the whole widget in fn_widget!.
- ✅ Recommended (Decoupled Logic & View):
let display_text = if user.is_logged_in() {
format!("Welcome, {}!", user.name())
} else {
"Please log in.".to_string()
};
text! {
text: display_text,
foreground: Color::BLACK,
}
- ❌ Avoid (Complex logic inside DSL):
text! {
text: {
if user.is_logged_in() {
format!("Welcome, {}!", user.name())
} else {
"Please log in.".to_string()
}
},
foreground: Color::BLACK,
}
- ✅ Recommended (Read close to use):
flex! {
@ {
let rows = $read(this).max_rounds();
(0..rows).map(|row| row_widget(row))
}
}
2. State & Performance
2.1 State Slicing & Performance
When dealing with long lists or deep UI trees, consider using part_writer for state slicing. This ensures sub-widgets only depend on the specific data they need, avoiding unnecessary re-renders.
2.2 Reactive Streams
- Prefer
distinct_pipe! over pipe! unless you specifically need to process duplicate values (helps reduce redundant UI updates).
2.3 Lightweight Smart Pointers
- When a weak count is not needed, prioritize using the repository's internal
Rc and Arc smart pointers (re-exported from rclite in ribir_algo) instead of std::rc::Rc or std::sync::Arc. They are more lightweight and have better performance as they do not support weak counts.
3. Debuggability
3.1 Recommendation for debug_name
For key interactive widgets or those requiring frequent debugging, adding a debug_name is recommended. This significantly improves efficiency when using MCP debugging tools, allowing direct interaction via stable names (e.g., name:counter_button).
4. Automation
Before committing any code changes, the following command sequence must be executed:
cargo +nightly ci check (Compile verification)
cargo +nightly ci lint (Static analysis, includes formatting)
cargo +nightly ci test (Logic verification)