The project is ongoing and in its early stages, and is not recommended for use in a production environment. A large number of test cases are missing.
Inspired by projects such as xilem,tachys.
- Compile-time view, No procedural macro, Use tuple Type Builder to build views, members, and styles
- Performance: Views are constructed only once by default, and reconstruction occurs only within the controllable range of changes in reactive data. Compiled-time views are utilized, and can be placed on the stack.
- Fine-grained reactivity, support signals(fork from tachy_reaccy)
- Componentization: It's easy to componentize views. support static prop, signal prop, events, slots
- Control flow: Support for 'x_if', 'x_iter', 'x_iter_keyed', 'x_iter_source', etc
- Cascading style: Strongly-typed, shareable, prioritized, supports hover and active interactive styles.
- Flexible and scalable
- High reuse, composable
- High performance, zero cost abstraction
- Minimal boilerplate
- Support multiple renderer (currently only support Bevy, make its can be used in a variety of scenarios, such as: games, desktop software, embedded platform, the native UI, etc
- Supports mainstream operating systems and Web platforms
- Text Edit
- More UI components and examples
- More Debug features, more test cases, Element inspector
- Other Renderers
- Bevy deeper integration, as a scenario to use? (like '@react-three/fiber'), Schema as Prefab?
- Theme,tailwind
- DSL, style hot reload, dynamic styles
- View Designer
MIT License (LICENSE-MIT)
Code sharing is welcome!
crate is not currently published to crates.io
But you can specify git repository dependencies
[dependencies]
rxy_ui = {git = "https://github.com/ycysdf/rxy_ui", features = ["bevy"]}
bevy = {version = "0.13"}
Counter
use bevy::prelude::*;
use rxy_ui::prelude::*;
fn main() {
let mut app = App::new();
app.add_plugins((
DefaultPlugins,
RxyPlugin::default(),
RxyStyleSheetPlugin::default(),
))
.add_systems(Startup, setup);
app.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
commands.spawn_rxy_ui(counter_by_signal);
}
fn counter_by_signal() -> impl IntoView<BevyRenderer> {
let count = use_rw_signal(0);
div().size_screen().flex().center().children(
div().flex_col().items_center().gap(16).children((
div().children(("Counter: ", rx(move || count.get().to_string()))),
div()
.children("Increment")
.style((
x().px(16).py(8).bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
x_active().outline_color(Color::BLUE).outline_width(2),
))
.on_pointer_click(move || count.update(|signal| *signal += 1)),
)),
)
}
Custom Checkbox UI Component
#[derive(TypedStyle)]
pub struct CheckboxStyle;
#[schema]
pub fn schema_checkbox(
mut ctx: SchemaCtx,
value: ReadSignal<bool>,
readonly: ReadSignal<bool>,
onchange: Sender<bool>,
) -> impl IntoElementView<BevyRenderer> {
let is_checked = ctx.use_controlled_state(value, onchange);
ctx.default_typed_style(CheckboxStyle, || {
let size = 20;
(
x().center()
.size(size)
.border(1)
.border_color(Color::DARK_GRAY),
x_hover().bg_color(Color::DARK_GRAY),
)
});
div()
.name("checkbox")
.style(CheckboxStyle)
.bg_color(rx(move || is_checked.get().then_some(COLOR_PRIMARY)))
.rx_member(move || {
(!readonly.get()).then_some(().on_pointer_click(move || {
is_checked.update(|is_checked| *is_checked = !*is_checked);
}))
})
}
Game Menu and Setting
https://github.com/ycysdf/rxy_ui/blob/main/examples/game_ui_challenges/game_menu.rs
use bevy::prelude::*;
use rxy_bevy::navigation::RxyKeyboardNavigationPlugin;
use rxy_ui::prelude::*;
use bevy::app::AppExit;
use std::fmt::Debug;
mod checkbox;
mod select;
mod slider;
use checkbox::*;
use select::*;
use slider::*;
pub const COLOR_PRIMARY: Color = Color::BLUE;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
enum GameState {
#[default]
MainMenu,
Setting,
InGame,
}
fn main() {
let mut app = App::new();
app.add_plugins((
DefaultPlugins,
RxyPlugin::default(),
RxyStyleSheetPlugin::default(),
RxyKeyboardNavigationPlugin::default(),
))
.add_state::<GameState>()
.add_systems(Startup, setup);
app.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
commands.spawn_rxy_ui(game_ui);
}
#[derive(TypedStyle)]
struct FocusStyle;
fn game_ui() -> impl IntoView<BevyRenderer> {
(
FocusStyle::def(
x_focus()
.outline_width(2)
.outline_offset(2)
.outline_color(COLOR_PRIMARY),
),
x_res(|state: &State<GameState>| match state.get() {
GameState::MainMenu => main_menu().into_dynamic(),
GameState::Setting => setting().into_dynamic(),
GameState::InGame => in_game().into_dynamic(),
}),
)
}
#[derive(Copy, Clone, Debug)]
pub struct XConfirm;
impl ElementEventIds for XConfirm {
fn iter_event_ids(self) -> impl Iterator<Item = ElementEventId> + Send + 'static {
(
x_just_pressed(KeyCode::Return),
x_just_pressed(GamepadButton::new(Gamepad::new(1), GamepadButtonType::West)),
x_pointer_click(),
)
.iter_event_ids()
}
}
#[schema]
fn schema_main_menu() -> impl IntoView<BevyRenderer> {
#[derive(TypedStyle)]
struct MenuBtnStyle;
(
MenuBtnStyle::def((
x().width(160)
.py(8)
.flex()
.center()
.bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
x_active().bg_color(COLOR_PRIMARY),
FocusStyle,
)),
div().style(x().size_screen().center()).children(
div().style(x().flex_col().gap(8).padding(20)).children({
(
button().style(MenuBtnStyle).children("New Game").on(
XConfirm,
|mut next_state: ResMut<NextState<GameState>>| {
next_state.set(GameState::InGame);
},
),
button().style(MenuBtnStyle).children("Setting").on(
XConfirm,
|mut next_state: ResMut<NextState<GameState>>| {
next_state.set(GameState::Setting);
},
),
button().style(MenuBtnStyle).children("Exit").on(
XConfirm,
|mut app_exit: EventWriter<AppExit>| {
app_exit.send(AppExit);
},
),
)
}),
),
)
}
#[schema]
fn schema_setting() -> impl IntoView<BevyRenderer> {
let options = ["Option 1", "Option 2", "Option 3"];
fn setting_item(
label: impl IntoView<BevyRenderer>,
content: impl IntoView<BevyRenderer>,
) -> impl IntoView<BevyRenderer> {
div()
.style((
x().flex()
.min_h(45)
.justify_between()
.items_center()
.gap(20)
.py(8)
.px(16),
x_hover().bg_color(Color::rgba(0.25, 0.25, 0.25, 0.4)),
))
.children((label, content))
}
fn label(str: impl Into<String>) -> impl IntoView<BevyRenderer> {
span(str.into()).font_size(17.)
}
div().style(x().size_screen().center()).children(
div().style(x().min_w(500).gap(8).flex_col()).children((
span("Game Setting").font_size(24.).mb(20),
setting_item(
label("Select"),
select::<&'static str>()
.value(options[0])
.slot_content(view_builder(move |_, _| {
x_iter(options.map(|n| {
selection_item(n, |item| {
button()
.style((
x().flex().py(6).center(),
x_hover().bg_color(Color::DARK_GRAY),
FocusStyle,
))
.bg_color(item.is_selected.then_some(Color::BLUE))
.children((item.value,))
})
}))
})),
),
setting_item(
label("CheckBox"),
checkbox().value(true).onchange(|_value| {
println!("checkbox value: {}", _value);
}),
),
setting_item(label("Slider"), slider().value(0.3)),
setting_item(label("Select Item"), {
let section_item = |item: SelectionItem<&'static str>| {
button()
.style((
x().flex().py(8).px(16).center(),
x_hover().bg_color(Color::DARK_GRAY),
FocusStyle,
))
.bg_color(item.is_selected.then_some(Color::BLUE))
.children((item.value,))
};
selection_list::<&'static str>()
.style(x().flex_row().py(4))
.value("One")
.slot_content((
selection_item("One", section_item),
selection_item("Two", section_item),
))
}),
)),
)
}
#[schema]
fn schema_in_game() -> impl IntoView<BevyRenderer> {
div()
.style(x().size_screen().center().flex_col().gap(8))
.children("InGame")
}
- Counter:examples/counter
- 10 Challenges for Bevy UI Frameworks
-
- Game Menu:examples/game_ui_challenges/game_menu
-
- Inventory:examples/game_ui_challenges/inventory
todo!()
-
- 7GUIs
todo!()
- TodoList:
todo!()
Rxy UI's view is composed of a tuple of View
elements. Any type that implements IntoView
can be a part of the view.
As long as all members of the tuple implement View
, the tuple itself also implements View
, allowing views to be composed in this way.
Since strings implement IntoView
, they can be directly included in the view.
div
is the most commonly used view type (analogous to NodeBundle
in Bevy and similar to the HTML div).
fn my_view() -> impl IntoView<BevyRenderer> {
(
div().children("Hello World"),
"Hello World",
)
}
Adding members to the View
type through chained calls, the children
method is used to configure the view's child views.
You can set the attrs of the view using attributes such as width
, height
, flex
, border
, outline
, and so on.
a complete list of currently supported attributes: attrs,composite_attrs,tailwind_attrs
Any type that implements ViewMember
can be used as a member of the view, and members can be manually added using the member
method.
Similar to View
, as long as all members of the tuple implement ViewMember
, the tuple itself also implements ViewMember
.
fn my_view() -> impl IntoView<BevyRenderer> {
let my_member = ().width(100).height(100);
(
div().member(my_member.clone()),
div().member(my_member)
)
}
View
describes the data required for a view, where a regular View
only has attribute values, while other View
types additionally include some extra information, that's all. It is not a genuine view node.
Thanks to the typed compile-time views, all these data attribute values can be stored entirely on the stack, resulting in low overhead.
You can add events using methods like on_pointer_click
, and the callback function provided is a Bevy System
. This means you can use Res
, Commands
, EventWriter
, etc. as parameters for the callback function.
fn signal_example()-> impl IntoView<BevyRenderer> {
div().children("Button")
.on_pointer_click(|res: Res<TestRes>, event_writer: EventWriter<TestEvent>, commands: Commands| {
println!("click")
})
}
The current signal implementation at xy_reactive, is forked from tachy_reaccy. It serves as the next-generation signal library for leptos, and its usage is generally similar to the leptos_reactive library.
Rxy UI supports the use of signals to rebuild views and their members.
An important function is rx
, which requires passing a closure. Inside this closure, you can use the get method of the signal to retrieve its value. rx
returns a type of Reactive
.
When the value of the signal changes, the associated View
or ViewMember
will be rebuilt.
If the closure's return value implements IntoView
, then the Reactive
type implements View
, and the rx
function can be directly used in view,
If the signal types RwSignal<T>
or ReadSignal<T>
, where T
implements IntoView
, then the signal also implements View
.
fn signal_example() -> impl IntoView<BevyRenderer> {
let count = use_rw_signal(0);
div()
.children(rx(move || count.get().to_string())) // `Reactive` 作为 `View`
.padding(20)
.bg_color(Color::BLUE)
.border_y(count) // 直接将 `RwSignal` 信号 作为 `ViewMember`
.border_color(Color::RED)
.width(100)
.height(rx(move || count.get() * 4 + 100)) // `Reactive` 作为 `ViewMember`
.on_pointer_click(move || {
count.update(|x| *x += 1);
})
}
Option<T>
,Either<A,B>
,Stream<T>
、Future<T>
, and similar types all implement IntoView
. Thereforce, they can be used directly as views.
For example, String
and &'static str
implement IntoView
, and so do Option<String>
, Future<Option<String>>
, and others; they can be nested.
The same logic applies to view members (or the values they can accept). If T itself can be a view member, then Option<T>
, Future<Option<T>>
, and similar types can also be used directly as view members.
For example, code: div().width(10)
, the value 10
is accepted by the width
member. Therefore, Some(10)
, async {Some(10)}
, and the like can also be used directly as values for the member.
width can directly accept values:
10
(equivalent toVal::Px(10.)
),10.
(equivalent toVal::Px(10.)
),Val::Percent(100)
, etc.
Use case examples:
Use case for Option
: Controlling whether to build a view or view member
while
rx
combined withOption
can control the visibility of the view,x_if
is better choice for this purpose, and it will be introduced later.
fn btn(is_show: RwSignal<bool>) -> impl IntoView<BevyRenderer> {
div()
.children("Btn")
.padding(20)
.bg_color(Color::GRAY)
.on_pointer_click(move || {
is_show.update(|x| *x = !*x);
})
}
fn sample_option_view() -> impl IntoView<BevyRenderer> {
let is_show = use_rw_signal(false);
(
btn(is_show),
rx(move || {
let view = div()
.padding(10)
.bg_color(Color::BLUE)
.flex()
.center()
.children("Show");
if is_show.get() {
Some(view)
} else {
None
}
// is_show.get().then_some(view) 这种写法也可以
}),
)
}
fn sample_option_view_member() -> impl IntoView<BevyRenderer> {
let is_show = use_rw_signal(false);
(
btn(is_show),
div()
.padding(10)
.flex()
.center()
.children("Show")
.member(rx(move || {
is_show.get().then_some(
().bg_color(Color::BLUE)
.outline_color(Color::RED)
.outline_width(2),
)
}))
// The following syntax is equivalent to the one above.
// .rx_member(move || {
// is_show.get().then_some(
// ().bg_color(Color::BLUE)
// .outline_color(Color::RED)
// .outline_width(2),
// )
// }),
)
}
Use case for Either
: Chossing which view or view member to build
While
rx
combined withEither
can achieve switching between views,x_if_else
is a better choice for this purpose, and it will be introduced later.
fn sample_either_view() -> impl IntoView<BevyRenderer> {
let is_show = use_rw_signal(false);
(
btn(is_show),
rx(move || {
let left_view = "Left";
let right_view = "Right";
if is_show.get() {
left_view.either_left() // This is equivalent to Either::Left(left_view)
} else {
right_view.either_right() // This is equivalent to Either::Right(right_view)
}
}),
)
}
fn sample_either_view_member() -> impl IntoView<BevyRenderer> {
let is_show = use_rw_signal(false);
(
btn(is_show),
div()
.padding(10)
.flex()
.center()
.children("Show")
.member(rx(move || {
let left_vm = ().bg_color(Color::BLUE);
let right_vm = ().outline_color(Color::RED).outline_width(2);
if is_show.get() {
left_vm.either_left() // This is equivalent to Either::Left(left_vm)
} else {
right_vm.either_right() // This is equivalent to Either::Right(right_vm)
}
}))
)
}
Future
Example:
fn sample_future() -> impl IntoView<BevyRenderer> {
let (sender, receiver) = oneshot::channel();
let mut sender = SyncCell::new(Some(sender));
let (sender2, receiver2) = async_channel::unbounded();
(
div()
.padding(20)
.bg_color({
let receiver2 = receiver2.clone();
async move { receiver2.recv().await.unwrap() }.boxed()
})
.children("Ok")
.on_pointer_click(move || {
let _ = sender2.try_send(Color::RED);
if let Some(sender) = sender.get().take() {
sender.send("Ok").unwrap();
}
}),
// For non-Boxed Future, you can use rx_future
x_future(async {
let r = receiver.await;
div().padding(20).children(format!("Future: {:?}", r))
}),
// a Boxed Future can be used directly as a view
async move {
let color = receiver2.recv().await;
div().padding(20).children(format!("Color: {:?}", color))
}
.boxed(),
)
}
Stream
Example:
x_if
is used to control whether to build the view. The first parameter passed to x_if
is similar to the view member, it can be bool
, Reactive
, ReadSingal
, etc.
fn sample_x_if() -> impl IntoView<BevyRenderer> {
let is_show = use_rw_signal(false);
(
div()
.children("Btn")
.padding(20)
.bg_color(Color::GRAY)
.on_pointer_click(move || {
is_show.update(|x| *x = !*x);
}),
x_if(is_show, span("Show1").padding(10).text_color(Color::RED)),
x_if(
is_show,
div().padding(10).flex().center().children("Show2"),
),
)
}
x_iter
is used to build list views, It takes an IntoIterator
, and the Item
needs to implement IntoView
. It uses the item's index as the key.
x_iter_keyed
requires manually specifying the key and expects Item
to be of type Keyed<K, IV>
. It has two members, the first being the key (which should implement Hash
), and the second being the view.
fn sample_x_iter() -> impl IntoView<BevyRenderer> {
div()
.flex_col()
.gap(8)
.children(x_iter((0..10).map(|n| format!("Item: {}", n))))
}
fn sample_x_iter_keyed() -> impl IntoView<BevyRenderer> {
div()
.flex_col()
.gap(1)
.children(x_iter_keyed((0..25).map(|n| {
Keyed(
n,
span(format!("Item: {}", n))
.padding(10)
.text_color(Color::rgb_u8(n * 10, 0, 0)),
)
})))
}
If you need to update the list, you can use rx
to wrap it.
fn sample_x_iter_keyed_rx() -> impl IntoView<BevyRenderer> {
let signal = use_rw_signal(3);
(
div()
.padding(10)
.bg_color(Color::BLUE)
.children("Add")
.on_pointer_click(move || {
signal.update(|x| *x += 1);
}),
rx(move || {
div()
.flex_col()
.gap(1)
.children(x_iter_keyed((0..signal.get()).map(|n| {
Keyed(
n,
span(format!("Item: {}", n))
.padding(10)
.text_color(Color::rgb_u8(n * 2, 255, 255)),
)
})))
}),
)
}
For frequently changing list views, you can also use x_iter_source
with use_list
. It is more efficient at updating lists than x_iter_keyed
and performs well in terms of performance.
Example:
fn ui() -> impl IntoView<BevyRenderer> {
let (ops, source) = use_list([1, 2, 3]);
let container_style = x()
.flex()
.size_screen()
.gap(16)
.py(16)
.justify_center()
.items_start();
#[derive(TypedStyle)]
struct BtnStyle;
(
BtnStyle::def((
x().py(8).px(16).center().bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
)),
div().style(container_style).children((
div().flex_col().gap(8).children((
div().children("Push").style(BtnStyle).on_pointer_click({
let ops = ops.clone();
move || {
ops.callback(|vec| {
vec.push(vec.len() as u32);
});
}
}),
div()
.children("Update First")
.style(BtnStyle)
.on_pointer_click({
let ops = ops.clone();
move || {
ops.callback(|vec| {
if vec.is_empty() {
return;
}
vec.update(0, 100);
});
}
}),
div()
.children("Patch Last")
.style(BtnStyle)
.on_pointer_click({
let ops = ops.clone();
move || {
ops.callback(|vec| {
if vec.is_empty() {
return;
}
let last = vec.len() - 1;
vec.update(last, vec[last] + 10)
})
}
}),
div().children("Pop").style(BtnStyle).on_pointer_click({
let ops = ops.clone();
move || {
ops.pop();
}
}),
div().children("Clear").style(BtnStyle).on_pointer_click({
let ops = ops.clone();
move || {
ops.clear();
}
}),
)),
div().flex_col().gap(8).children((
"--Header--",
x_iter_source(source, |n,_| n.to_string()),
"--Footer--",
)),
)),
)
}
In the previous examples, attrs style where directly written in the views, and such styles cannot cascade. Although it's possible to override them (with some problems), it doesn't support interactive styles like hover and press(active).
Cascading styles can indeed stack, and they have priorities, including directly set attributes. Their priorities are as follows:
Directly set attrs > Interactive styles > Inline styles > Shared styles > Styles positioned later
To add styles using the style
method, similar to View
and ViewMember
, it accepts a tuple where you can include multiple style sheets.
Normal styles are constructed using x()
, and interactive styles are construct using x_hover()
,x_active()
,x_focus()
.
fn sample_style_sheet() -> impl IntoView<BevyRenderer> {
div()
.margin(50)
.style((
x().py(8 * 2).px(16 * 2).center().bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
x_active().outline_color(Color::GREEN).outline_width(2),
))
.children("Button")
}
Currently, styles within the style sheet are static and do not allow the use of rx
, Future
, Stream
, Option
, etc, However, you can call style
method multiple times to add styles.
fn sample_dynamic_style_sheet() -> impl IntoView<BevyRenderer> {
let signal = use_rw_signal(false);
div()
.on_pointer_click(move || {
signal.update(|n| *n = !*n);
})
.style(Some((x().bg_color(Color::GRAY).height(100.).width(100.),)))
.rx_style(move || {
signal
.get()
.then_some((x().bg_color(Color::RED), x_hover().bg_color(Color::WHITE)))
})
}
Define a shared style identifier struct by using the TypedStyle
derive macro in the structure. Then, in the view, use the def
method to define a shared typed cascading style.
To use the shared style, simply call style
method and pass in the type.
fn sample_shared_typed_style_sheet() -> impl IntoView<BevyRenderer> {
#[derive(TypedStyle)]
struct MenuBtnStyle;
(
MenuBtnStyle::def((
x().width(160)
.py(8)
.flex()
.center()
.bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
x_active().outline_color(Color::GREEN).outline_width(2),
)),
div().padding(50).gap(10).flex_col().children((
div().style(MenuBtnStyle).children("Button 1"),
div()
.style((MenuBtnStyle, x().bg_color(Color::RED)))
.children("Button 2"),
div().style(MenuBtnStyle).children("Button 3"),
)),
)
}
Currently, there are no means to modify styles in the shared style sheet, but there will be added means for modification in the future.
Firstly, you can directly reuse views through functions. However, there are some problems:
- Rust lacks optional parameters.
- The function may be called multiple times (especially if it is within an
rx
, where it is called each time the signal changes). - It's just an ordinary function, lacking context, making it challenging to perform some encapsulation.
- There are some other problems as well, not to go into further detail.
So, we need a better way to reuse views, and that's where Schema
comes in. This is a tentative name, and if you have a better one, fell free to suggest.
It's preferable for the name to be a single word and not use "Component" because it's already used by ECS. Avoiding "Widget" is because I plan to use it for custom-drawn controls in future.
In the future, we will also explore the possibility of using
Schema
as perfab for game scenes.
Schema
essentially addresses all the problems mentioned above:
Schema
ensures that the function runs only once, meaning the views inside it will be builded only once.- Default properties are optional, and it supports required properties.
- It supports slots, events, schema context, and more
Conventions for defining a Schema
:
- Use the attribute macro
schema
. - Function names should start with
schema_
- The return type must be either
impl IntoView<Renderer>
orimpl IntoElementView<Renderer>
(the difference betweenIntoElementView
andIntoView
will be explained later)`) - Valid parameter types define attributes, events, slots, etc. for the
Schema
throught the function parameters.
There are the following types of Props:
ReadSignal
: SignalReceiverProp
: Simple reactive prop, Currently not recommended for use because it is weaker and less user-friendly compared to signals.Static
: Static prop that does not update
By default, props are optional, If you want them to be required, you can use Required
to wrap the type (more on this later)
Event type: Sender
, representing an event (actually a re-export of async-channel
's Sender
type).
Slot types: Slot
, a slot; CloneableSlot
, a cloneable slot. Slots implement IntoView
and can be directly placed in the view.
SchemaCtx
: Context type. Currently, it can be used to:
- Obtain the
World
- define shared styles or default shared styles outside the view.
- Use
use_controlled_state
to get al controlled state.
Below is an example code for custom Checkbox
:
#[derive(TypedStyle)]
pub struct CheckboxStyle;
#[schema]
pub fn schema_checkbox(
mut ctx: SchemaCtx,
value: ReadSignal<bool>,
readonly: ReadSignal<bool>,
onchange: Sender<bool>,
) -> impl IntoElementView<BevyRenderer> {
let is_checked = ctx.use_controlled_state(value, onchange); // 获取一个受控的状态
ctx.default_typed_style(CheckboxStyle, || { // 定义默认共享样式
let size = 20;
(
x().center()
.size(size)
.border(1)
.border_color(Color::DARK_GRAY),
x_hover().bg_color(Color::DARK_GRAY),
)
});
div()
.name("checkbox")
.style(CheckboxStyle)
.bg_color(rx(move || is_checked.get().then_some(Color::BLUE)))
.rx_member(move || {
(!readonly.get()).then_some(().on_pointer_click(move || {
is_checked.update(|is_checked| *is_checked = !*is_checked);
}))
})
}
If you are using you custom type in prop,such as
ReadSignal<CustomType>
, you need to add thePropValueWrapper
derive macro toCustomType
. This restriction may be removed in the future.
In the xample above, use_controlled_state
is used, which, by taking a signal and an event as inputs, returns a new signal.
When the input signal or the retured signal changes. it sends an event with the latest value as as parameter.
What does the schema
attribute macro do? It doesn't modify the original function. Instead, it based on the function information, it generates a new function. The original function with with the schema_
prefix removed is its name. Additionally, it adds an implementation of a trait for the return type of this function (for details, you can check the code generated by the macro).
Code Example:
fn sample_checkbox() -> impl IntoView<BevyRenderer> {
checkbox()
.padding(50)
.style(x().border_color(Color::RED).border(2))
.on_pointer_up(|| {
println!("checkbox pointer up");
})
.value(false)
.readonly(false)
.onchange(|value| {
println!("checkbox value: {}", value);
})
}
The checkbox
function is generated by schema_checkbox
.
You can pass attributes or events like padding
,style
,on_pointer_up
, etc. to the root element of schema_checkbox
. These attributes or events will be forwarded to the root element.
In addition to these, there are attributes like value
, readonly
, onchange
, etc. You can use them to set the corresponding props or events of schema_checkbox
Apart from accepting static values, you can also wrap values with reactive types like Option
, Reactive
, ReadSignal
, etc.
Unlike
ViewMember
, schema props currently cannot nestOption
within reactive types likeReactive
,Memo
, etc.
In IntoView
, the root element can have zero to many children, while in IntoElementView
, it can have only one root element.
If the return type of a Schema
is IntoView
, external code cannot append members or envets to its root element because the root element may have multiple or zero children.
After defining a slot in Schema
, you can use the slot_<slot_name>
method to specify the content of the slot. It takes the type IntoView
. If the slot is of type CloneableSlot
, then the IntoView
type it takes needs to implement Clone
. Many views do not implement Clone
, and in such cases, you can use view_builder
to wrap it.
Code Example:
#[schema]
fn schema_sample(head: Slot, foot: Slot) -> impl IntoView<BevyRenderer> {
div()
.children((
head,
div().bg_color(Color::GRAY).p(20).children("Body"),
foot,
))
.p(20)
.flex_col()
.gap(8)
}
fn sample_schema_sample() -> impl IntoView<BevyRenderer> {
sample()
.slot_head(div().children("Head"))
.slot_foot(div().children("Foot"))
}
By default, props in Schema
are optional. Therefore, the types of prop values, like T
in ReadSignal<T>
, must implement Default
.
You can use Required
to wrap prop such as Static
,ReadSignal
,ReceiverProp
,Slot
,CloneableSlot
, etc. to indicate that they are required. In this case, the prop value T
does not need to implement Defeault
.
After marking an prop as required, the function generated by Schema
will no longer be parameterless. Instead, you will need to pass the required parameters in order.
Code Example:
#[schema]
fn schema_required_sample(
Required(content): Required<Slot>,
Required(static_p): Required<Static<bool>>,
) -> impl IntoView<BevyRenderer> {
div().children((content, "schema_required_sample"))
}
fn sample_schema_required_sample() -> impl IntoView<BevyRenderer> {
required_sample("Content Slot", true)
}
Sometimes you need to return different types, but impl
doesn't allow returning different types.
For two different types, you can use Either<LeftView, RightView>
to wrap them to solve the problem.
For multiple different types, you can also use Either<LeftView, Either<RightView, Either<...>>>
to solve this problem, but this approach can be cumbersome.
Note: The type of
View
wrapsViewMember
, meaning differentViewMember
members will result in diferentView
types
Code Example:
#[derive(Clone, Debug)]
enum SampleState {
Init,
Loading,
Loaded,
}
fn sample_dynamic() -> impl IntoView<BevyRenderer> {
let signal = use_rw_signal(SampleState::Init);
(
div()
.children("Change State")
.style((
x().padding(20).flex().center().bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
x_active().outline_color(Color::GREEN).outline_width(2),
))
.on_pointer_click(move || {
signal.update(|state| match state {
SampleState::Init => *state = SampleState::Loading,
SampleState::Loading => *state = SampleState::Loaded,
SampleState::Loaded => *state = SampleState::Init,
})
}),
rx(move || match signal.get() {
SampleState::Init => span("Init").margin(30).into_dynamic(),
SampleState::Loading => div()
.padding(30)
.bg_color(Color::RED)
.children("Loading")
.into_dynamic(),
SampleState::Loaded => span("Loaded")
.margin(30)
.text_color(Color::BLUE)
.into_dynamic(),
}),
)
}
system_once
will run the proveded System
once when it is builded (if it's within an rx
, then it will run each time the signal changes).
fn sample_system_once() -> impl IntoView<BevyRenderer> {
system_once(|entities: &Entities, cmd_sender: Res<CmdSender>| {
let cmd_sender = cmd_sender.clone();
(
div()
.font_size(30)
.children(format!("Entites Count: {}", entities.len())),
div()
.children("Spawn 2d Material")
.style((
x().padding(20).flex().center().bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
x_active().outline_color(Color::GREEN).outline_width(2),
))
.on_pointer_click(move || {
cmd_sender.add(|world: &mut World| {
let mut meshes = world.resource_mut::<Assets<Mesh>>();
let mesh = meshes.add(shape::Quad::default().into()).into();
let mut materials = world.resource_mut::<Assets<ColorMaterial>>();
let material = materials.add(Color::PURPLE.into());
world.spawn(MaterialMesh2dBundle {
mesh,
transform: Transform::default().with_scale(Vec3::splat(128.)),
material,
..default()
});
});
}),
)
})
}
By using system
, you can treat a Bevy System
as a view. You can use configure
to configure the System
However, this method is currently unsafe because there is currently no means of removing a System
In Bevy, So you have to make sure that this view is always there
fn sample_system() -> impl IntoView<BevyRenderer> {
div().flex_col().gap(10).children(unsafe {
system_with_config(Update,|config| config.run_if(|| true), |query: Query<Entity, With<Style>>| {
x_iter_keyed(query.iter().map(|entity| {
Keyed(
entity,
span(format!("Style Entity: {:?}", entity)).margin(10),
)
}))
})
})
}
x_res
retrieves a resource of type T
and rebuilds when the resource changes.
In the following code example, the view will be rebuild every time the FrameCount
resource changes:
fn sample_x_res() -> impl IntoView<BevyRenderer> {
div().gap(10).p(20).flex_col().children((
x_res(|frame_count: &FrameCount| {
span(format!("FrameCount: {:?}", frame_count.0))
.p(20)
.font_size(30)
}),
))
}
By wrapping Bevy's Bundle
with the x_bundle
method, you obtain XBundle<T>
, which implements the ViewMember
trait. This allows you to include the Bundle
as a view member within a view.
Additionally, a chainable method .bundle(YourBundle)
is provided, which is equivalent to .member(x_bundle(YourBundle))
.
Of course, you can also wrap XBundle
using rx
.
Code Example:
fn sample_bundle() -> impl IntoView<BevyRenderer> {
let signal = use_rw_signal(false);
#[derive(Component)]
struct CustomComponent;
#[derive(Component)]
struct CustomComponent2(bool);
#[derive(Component)]
struct CustomComponent3;
div()
.bundle(CustomComponent3) // equivalent to:`.member(x_bundle(CustomComponent))`
.rx_member(move || signal.get().then_some(x_bundle(CustomComponent))) // `rx_member()` equivalent to `member(rx())`
.rx_member(move || x_bundle(CustomComponent2(signal.get())))
}
view_builder
, the view builder,accepts a callback function that takes two parameters: the first is ViewCtx
, and the second is BuildFlags
.
You can use ViewCtx
within the callback to construct your view.
As mentioned earlier, view_builder
also allow views that cannot be Clone
to be placed inside it, The type returned by view_builder
implements Clone
.
fn sample_view_builder() -> impl IntoView<BevyRenderer> {
view_builder(|ctx, flags| {
let parent = ctx.parent;
// let world = ctx.world;
format!("Parent: {:?}, Flags: {:?}", parent, flags)
})
}
You can provide context using provide_context
, which supplies the context type to its child views.
Child views can obtain the context through the context
-related methods of the first parameter, ViewCtx
, in view_builder
.
In Schema
, you can directly obtain the context through the Context
parameter.
Code Example:
#[derive(TypedStyle)]
struct BtnStyle;
#[derive(Clone)]
struct MyContext {
signal: RwSignal<bool>,
}
#[schema]
fn schema_context_sample(Context(my_context): Context<MyContext>) -> impl IntoView<BevyRenderer> {
div()
.children("Set Signal To True")
.style(BtnStyle)
.on_pointer_click(move || {
my_context.signal.set(true);
})
}
fn sample_context() -> impl IntoView<BevyRenderer> {
let signal = use_rw_signal(false);
(
BtnStyle::def((
x().width(160)
.py(8)
.flex()
.center()
.bg_color(Color::DARK_GRAY),
x_hover().bg_color(Color::GRAY),
x_active().outline_color(Color::GREEN).outline_width(2),
)),
provide_context(
MyContext { signal },
div().p(20).flex().gap(10).children((
view_builder(|ctx, _| {
let my_context = ctx.context::<MyContext>();
rx(move || format!("Signal: {}", my_context.signal.get()))
}),
x_if(signal, "Signal Is True"),
div()
.children("Set Signal To False")
.style(BtnStyle)
.on_pointer_click(move || {
signal.set(false);
}),
context_sample(),
)),
),
)
}
schema
support function to use generic parameter
Code Example:
#[schema]
pub fn schema_select<T>(
mut ctx: SchemaCtx,
content: CloneableSlot,
value: ReadSignal<T>,
readonly: ReadSignal<bool>,
onchange: Sender<T>,
) -> impl IntoElementView<BevyRenderer>
where
T: Default + Debug + Send + Sync + PartialEq + Clone + 'static,
{
///...
}
fn sample_schema_select() -> impl IntoView<BevyRenderer> {
enum SelectValue {
A,
B,
C,
}
schema_select::<SelectValue>()
// ...
}