Declarative UI and the rsx!
Macro
OSUI embraces a declarative approach to UI development, heavily inspired by modern web frameworks. This is primarily facilitated by the rsx!
macro, which allows you to describe your UI's structure and behavior in a clear, nested, and expressive way.
Why Declarative UI?
Traditional imperative UI development involves manually creating, positioning, and updating UI elements based on state changes. This can lead to complex, hard-to-maintain code, especially for dynamic UIs.
Declarative UI, in contrast:
- Focuses on "What": You describe the desired UI state for a given data state, rather than the steps to get there.
- Simplicity: The code is often more readable and easier to reason about, as it mirrors the visual structure of the UI.
- Reactivity: When the underlying data changes, the framework (OSUI, in this case) efficiently updates the UI to reflect the new state, minimizing manual DOM manipulation.
- Composition: Encourages breaking down complex UIs into smaller, reusable components.
The rsx!
Macro
The rsx!
macro is the cornerstone of OSUI's declarative syntax. It transforms a nested, JSX-like structure into a tree of RsxElement
s, which are then used to build Widget
s on the Screen
.
Basic Syntax
use osui::prelude::*;
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension); // Needed for basic interaction
rsx! {
// Simple text string as an Element
"Hello, World!"
// Static element: No dynamic dependencies or state
static Div {
// Nested content
"This is a static div."
}
// Dynamic element: Reacts to state changes
%my_state // This widget depends on `my_state`
Div {
// Content can be interpolated from state
"Current value: {my_state}"
}
}.draw(&screen);
screen.run()?;
Ok(())
}
Key Features of rsx!
Syntax
-
Direct Text: Any string literal within
rsx!
is treated as aString
element.rsx! {
"Just some text."
format!("Dynamic text: {}", 123) // You can also use format!
} -
Element Declaration: Elements are typically declared by their struct name (e.g.,
Div
,FlexRow
,Input
).rsx! {
Div {} // A simple Div element
} -
Properties (Fields): You can set public fields on the element struct directly using comma-separated key-value pairs, similar to struct instantiation.
rsx! {
Heading, smooth: true, { "Important Title" } // Sets the `smooth` field on Heading
}This expands to:
let mut elem = Heading::new();
elem.smooth = true;
// ... then `elem` is wrapped in a Widget -
Children: Content nested within curly braces
{}
after an element forms its children. These children are then drawn by the parent element'safter_render
method.rsx! {
Div {
"First child"
"Second child"
FlexRow { "Nested FlexRow" }
}
} -
Static vs. Dynamic Widgets:
static
keyword: Prefix an element withstatic
to explicitly declare it as aStaticWidget
. This means its content and components won't change unless manually modified elsewhere. It incurs no dependency tracking overhead.rsx! {
static Div { "Always the same" }
}- Dynamic (Reactive): By default, if an element has dependencies (see below) or is just a string literal without
static
, it becomes aDynWidget
. This allows it to refresh when its dependencies change.
-
Dependencies (
%dep_name
): Prefixing an element with%variable_name
registersvariable_name
(which must implementDependencyHandler
, likeState<T>
) as a dependency for that widget. Ifvariable_name
signals a change, the widget will automatically rebuild and re-render.let counter = use_state(0);
rsx! {
%counter // This Div depends on `counter`
Div {
// `counter` can be used directly in its content
"Count: {counter}"
}
}Multiple dependencies can be listed:
%dep1 %dep2 Element {}
. -
Components (
@ComponentType
): Attach components to an element using the@
symbol followed by the component's type and its constructor or a value.use osui::prelude::*;
rsx! {
// Attach a Transform component with specific dimensions
@Transform::new().dimensions(10, 5);
// Attach a Style component with a solid red background
@Style { background: Background::Solid(0xFF0000) };
Div {
"A red box"
}
// Attach a custom Handler component for events
@Handler::new(|_, e: &MyEvent| { /* ... */ });
Div { "Click me!" }
}You can attach multiple components to the same element, each on its own
@
line. -
Macro Expansion (
$expand => ($args)
): You can include the output of anotherRsx
-generating function or macro usingmacro_name => (args)
. This is useful for creating reusable UI fragments.fn my_button(text: &str) -> Rsx {
rsx! {
Div { format!("Button: {}", text) }
}
}
rsx! {
my_button => ("Click Me")
my_button => ("Another Button")
}
How rsx!
Works Internally
The rsx!
macro recursively expands into a series of Rsx::create_element
or Rsx::create_element_static
calls. Each RsxElement
stores either a StaticWidget
directly or a closure that produces a WidgetLoad
(for dynamic widgets), along with its dependencies and child Rsx
tree.
When Rsx::draw
or Rsx::draw_parent
is called on the root Rsx
object:
- It iterates through its
RsxElement
s. - For
RsxElement::DynElement
, it calls the stored closure to generate aWidgetLoad
, then creates aDynWidget
viascreen.draw_box_dyn
. It registers all specified dependencies with this newDynWidget
. - For
RsxElement::Element
, it directly creates aStaticWidget
viascreen.draw_widget
. - If a parent widget is provided (for nested elements), it calls
parent.get_elem().draw_child(&new_widget)
. This informs the parentElement
about its new child. - It then recursively calls
draw_parent
on the childRsx
tree, passing the newly created widget as theparent
.
This process builds the complete Arc<Widget>
tree managed by the Screen
, setting up the initial hierarchy and reactive dependencies. The declarative rsx!
syntax simplifies this complex creation process into an intuitive structure.