use bevy::{
color::palettes,
ecs::schedule::IntoScheduleConfigs,
input_focus::tab_navigation::TabIndex,
prelude::*,
text::{EditableText, TextCursorStyle},
ui::Checked,
ui_widgets::{
Checkbox, RadioButton, RadioGroup, Slider, SliderOrientation, SliderPrecision, SliderRange,
SliderThumb, SliderValue, TextInput, TrackClick, ValueChange,
},
window::{CursorIcon, PrimaryWindow, SystemCursorIcon},
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_resource::<Character>()
.add_systems(Startup, setup)
.add_systems(
Update,
(
on_changed_editable_text,
refresh_character.run_if(resource_exists_and::<Character>(|character| {
!character.changed_fields.is_empty()
})),
)
.chain(),
)
.run();
}
fn setup(mut commands: Commands, character: Res<Character>) {
commands.spawn_scene_list(bsn_list! {
Camera2d
--
@ui(&character)
--
@character_view(&character)
});
}
#[derive(Resource)]
struct Character {
name: String,
age: u32,
hat_type: HatType,
tint_yellow: bool,
changed_fields: Vec<ChangedField>,
}
impl Default for Character {
fn default() -> Self {
Character {
name: "Bevy".into(),
age: 5,
hat_type: HatType::default(),
tint_yellow: false,
changed_fields: vec![],
}
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq)]
enum HatType {
#[default]
None,
TopHat,
DunceCap,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum ChangedField {
Name,
Age,
HatType,
TintYellow,
}
const HAT_TYPES: [HatType; 3] = [HatType::None, HatType::TopHat, HatType::DunceCap];
#[derive(Component, Clone, Default)]
struct NameInput;
#[derive(Component, Clone, Default)]
struct AgeSlider;
#[derive(Component, Clone, Default)]
struct AgeSliderThumb;
#[derive(Component, Clone, Default)]
struct AgeSliderText;
#[derive(Component, Clone, Default)]
struct HatTypeRadioGroup;
#[derive(Component, Clone, Default)]
struct TintYellowCheckbox;
#[derive(Component, Clone, Default)]
struct CharacterView;
#[derive(Component, Clone, Default)]
struct CharacterSprite;
#[derive(Component, Clone, Default)]
struct CharacterHat;
#[derive(Component, Clone, Default)]
struct CharacterNameAndAge;
fn ui(character: &Character) -> impl Scene {
bsn! {
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
height: percent(100)
width: percent(50),
}
Children [
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceEvenly,
align_items: AlignItems::Center,
margin: percent(5),
width: percent(90),
padding: UiRect::vertical(px(10)),
border_radius: BorderRadius::all(px(5)),
row_gap: px(10),
}
BackgroundColor(palettes::basic::GRAY)
Children [
Node
Children[
Text("Character Creator")
]
--
@name_text_input_row(character)
--
@age_slider_row(character)
--
@hat_type_radio_group_row(character)
--
@tint_yellow_checkbox_row(character)
]
]
}
}
fn name_text_input_row(character: &Character) -> impl Scene {
bsn! {
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
}
Children [
Node
Children [
Text::new("Name: ")
]
--
@name_text_input(character)
]
}
}
fn name_text_input(character: &Character) -> impl Scene {
let name = character.name.clone();
bsn! {
Node {
width: px(200),
border: px(5),
border_radius: BorderRadius::all(px(10)),
padding: UiRect::axes(px(5), px(2)),
}
NameInput
TextInput
EditableText::new(name)
TabIndex(0)
TextCursorStyle::default()
BackgroundColor(Color::BLACK)
on(on_pointer_over_text_cursor)
on(on_pointer_out_default_cursor)
}
}
fn on_changed_editable_text(
name_input_q: Query<&EditableText, With<NameInput>>,
mut character: ResMut<Character>,
) {
let Ok(editable_text) = name_input_q.single_inner() else {
return;
};
let new_name = editable_text.value().to_string();
if character.name != new_name {
character.name = new_name;
character.changed_fields.push(ChangedField::Name);
}
}
fn age_slider_row(character: &Character) -> impl Scene {
let age = character.age;
bsn! {
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
column_gap: px(10),
}
Children [
Node
Children [
Text::new("Age:")
]
--
@age_slider(character)
--
Node {
width: px(30),
}
Children [
AgeSliderText
Text(format!("{}", age))
]
]
}
}
fn age_slider(character: &Character) -> impl Scene {
bsn! {
Node {
width: px(220),
border: px(5),
padding: UiRect::axes(px(5), px(2)),
}
AgeSlider
Slider {
track_click: TrackClick::Snap,
orientation: SliderOrientation::Horizontal,
}
SliderValue({character.age as f32})
SliderPrecision(0)
SliderRange::new(1., 100.)
BackgroundColor(Color::BLACK)
on(on_value_change_age_slider)
on(on_pointer_over_pointer_cursor)
on(on_pointer_drag_start_grabbing_cursor)
on(on_pointer_drag_end_grab_cursor)
on(on_pointer_out_default_cursor)
Children [
Node {
height: px(5),
border_radius: BorderRadius::all(px(3)),
}
BackgroundColor(Color::BLACK)
--
Node {
display: Display::Flex,
position_type: PositionType::Absolute,
left: px(0)
right: px(20),
top: px(0),
bottom: px(0),
}
Children [
AgeSliderThumb
SliderThumb
Node {
display: Display::Flex,
width: px(20),
height: px(10),
position_type: PositionType::Absolute,
left: percent((character.age as f32 - 1.) / (100. - 1.) * 100.),
}
BackgroundColor(Color::WHITE)
on(on_pointer_over_grab_cursor)
on(on_pointer_out_default_cursor)
on(on_pointer_drag_start_grabbing_cursor)
on(on_pointer_drag_end_grab_cursor)
]
]
}
}
fn on_value_change_age_slider(
event: On<ValueChange<f32>>,
age_slider_q: Query<(Entity, &SliderRange), With<AgeSlider>>,
mut age_slider_text_q: Query<&mut Text, With<AgeSliderText>>,
mut age_slider_thumb_q: Query<&mut Node, With<AgeSliderThumb>>,
mut character: ResMut<Character>,
mut commands: Commands,
) {
let Ok((entity, slider_range)) = age_slider_q.single_inner() else {
return;
};
if event.source != entity {
return;
}
character.age = event.value as u32;
character.changed_fields.push(ChangedField::Age);
commands
.entity(event.source)
.insert(SliderValue(character.age as f32));
for mut node in age_slider_thumb_q.iter_mut() {
node.left = percent(slider_range.thumb_position(character.age as f32) * 100.0);
}
for mut text in age_slider_text_q.iter_mut() {
*text = Text::new(format!("{}", character.age));
}
}
fn hat_type_radio_group_row(character: &Character) -> impl Scene {
bsn! {
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
}
RadioGroup
HatTypeRadioGroup
on(on_value_change_hat_type)
Children [
Node
Children [
Text("Hat: ")
]
--
{
HAT_TYPES.iter()
.map(|hat_type| hat_type_radio_button(*hat_type, character))
.collect::<Vec<_>>()
}
]
}
}
fn hat_type_radio_button(hat_type: HatType, character: &Character) -> Box<dyn Scene> {
let base_radio_button = || {
bsn! {
Node {
border: px(5),
border_radius: BorderRadius::all(px(10)),
padding: UiRect::axes(px(5), px(2)),
}
RadioButton
hat_type
BackgroundColor(Color::BLACK)
on(on_pointer_over_pointer_cursor)
on(on_pointer_out_default_cursor)
}
};
if character.hat_type == hat_type {
Box::new(bsn! {
@base_radio_button()
Checked
Children [
Text::new(format!("{hat_type:?}"))
TextColor(palettes::basic::GREEN)
]
})
} else {
Box::new(bsn! {
@base_radio_button()
Children [
Text(format!("{hat_type:?}"))
TextColor(palettes::basic::WHITE)
]
})
}
}
fn on_value_change_hat_type(
event: On<ValueChange<Entity>>,
hat_type_value_q: Query<(Entity, &HatType, Has<Checked>, &Children), With<RadioButton>>,
hat_type_radio_group_q: Single<Entity, With<HatTypeRadioGroup>>,
mut character: ResMut<Character>,
mut commands: Commands,
) {
if event.source != hat_type_radio_group_q.entity() {
return;
}
let Ok((_, new_hat_type, has_checked, _)) = hat_type_value_q.get(event.value) else {
return;
};
if has_checked {
return;
}
character.hat_type = *new_hat_type;
character.changed_fields.push(ChangedField::HatType);
for (button_entity, hat_type, has_checked, children) in hat_type_value_q.iter() {
if character.hat_type == *hat_type {
commands.entity(button_entity).insert(Checked);
commands
.entity(children[0])
.insert(TextColor(palettes::basic::GREEN.into()));
} else if has_checked {
commands.entity(button_entity).remove::<Checked>();
commands.entity(children[0]).insert(TextColor(Color::WHITE));
}
}
}
fn tint_yellow_checkbox_row(character: &Character) -> impl Scene {
bsn! {
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
}
Children [
Node
Children [
Text("Tint Yellow: ")
]
--
@tint_yellow_checkbox(character)
]
}
}
fn tint_yellow_checkbox(character: &Character) -> Box<dyn Scene> {
let base_checkbox = || {
bsn! {
Node {
padding: UiRect::horizontal(px(5)),
}
Checkbox
TintYellowCheckbox
BackgroundColor(Color::WHITE)
on(on_pointer_over_pointer_cursor)
on(on_pointer_out_default_cursor)
on(on_value_change_tint_yellow)
}
};
if character.tint_yellow {
Box::new(bsn! {
@base_checkbox()
Checked
Children [
Text("X")
TextColor(palettes::basic::GREEN)
]
})
} else {
Box::new(bsn! {
@base_checkbox()
Children [
Text(" ")
TextColor(palettes::basic::GREEN)
]
})
}
}
fn on_value_change_tint_yellow(
event: On<ValueChange<bool>>,
tint_yellow_checkbox_q: Query<(Entity, &Children), With<TintYellowCheckbox>>,
mut character: ResMut<Character>,
mut commands: Commands,
) {
let Ok((checkbox_entity, children)) = tint_yellow_checkbox_q.single_inner() else {
return;
};
if event.source != checkbox_entity {
return;
}
character.tint_yellow = event.value;
character.changed_fields.push(ChangedField::TintYellow);
if character.tint_yellow {
commands.entity(event.source).insert(Checked);
commands.entity(children[0]).insert(Text::new("X"));
} else {
commands.entity(event.source).remove::<Checked>();
commands.entity(children[0]).insert(Text::new(" "));
}
}
fn character_view(character: &Character) -> impl Scene {
bsn! {
CharacterView
Transform::from_xyz(320., 0., 0.)
Visibility::Inherited
Children [
@character_sprite(&character)
--
@character_hat(&character)
--
@character_name_and_age(&character)
]
}
}
fn refresh_character(
mut commands: Commands,
character_view_q: Single<(Entity, &Children), With<CharacterView>>,
view_type_q: Query<(
Entity,
Has<CharacterSprite>,
Has<CharacterHat>,
Has<CharacterNameAndAge>,
)>,
mut character: ResMut<Character>,
) {
let (character_view, children) = character_view_q.into_inner();
let (mut already_updated_name_age, mut already_updated_sprite, mut already_updated_hat) =
(false, false, false);
for changed_field in character.changed_fields.iter().copied() {
match changed_field {
ChangedField::Name | ChangedField::Age if !already_updated_name_age => {
for (child, _, _, is_name_and_age) in children
.iter()
.filter_map(|child| view_type_q.get(child).ok())
{
if is_name_and_age {
commands.entity(child).try_despawn();
}
}
let new_child = commands
.spawn_scene(character_name_and_age(&character))
.id();
commands.entity(character_view).add_child(new_child);
already_updated_name_age = true;
}
ChangedField::TintYellow if !already_updated_sprite => {
for (child, is_sprite, _, _) in children
.iter()
.filter_map(|child| view_type_q.get(child).ok())
{
if is_sprite {
commands.entity(child).try_despawn();
}
}
let new_child = commands.spawn_scene(character_sprite(&character)).id();
commands.entity(character_view).add_child(new_child);
already_updated_sprite = true;
}
ChangedField::HatType if !already_updated_hat => {
for (child, _, is_hat, _) in children
.iter()
.filter_map(|child| view_type_q.get(child).ok())
{
if is_hat {
commands.entity(child).try_despawn();
}
}
let new_child = commands.spawn_scene(character_hat(&character)).id();
commands.entity(character_view).add_child(new_child);
already_updated_hat = true;
}
_ => {}
}
}
character.changed_fields.clear();
}
fn character_sprite(character: &Character) -> Box<dyn Scene> {
if character.tint_yellow {
Box::new(bsn! {
CharacterSprite
Sprite {
image: "branding/icon.png",
color: palettes::basic::YELLOW
}
Transform
})
} else {
Box::new(bsn! {
CharacterSprite
Sprite {
image: "branding/icon.png",
}
Transform
})
}
}
fn character_hat(character: &Character) -> Box<dyn Scene> {
match character.hat_type {
HatType::None => Box::new(bsn! {
CharacterHat
}),
HatType::TopHat => Box::new(bsn! {
CharacterHat
Transform::from_rotation(Quat::from_rotation_z(0.78))
Visibility::Inherited
Children [
Mesh2d(asset_value(Rectangle::new(
40., 10.
)))
MeshMaterial2d<ColorMaterial>(asset_value(ColorMaterial::from_color(Color::BLACK)))
Transform::from_xyz(55., 60., 1.)
--
Mesh2d(asset_value(Rectangle::new(
20., 50.
)))
MeshMaterial2d<ColorMaterial>(asset_value(ColorMaterial::from_color(Color::BLACK)))
Transform::from_xyz(55., 85., 1.)
]
}),
HatType::DunceCap => Box::new(bsn! {
CharacterHat
Mesh2d(asset_value(Triangle2d::new(
Vec2::new(0., 100.),
Vec2::new(-20., 0.),
Vec2::new(20., 0.)
)))
MeshMaterial2d<ColorMaterial>(asset_value(ColorMaterial::from_color(palettes::basic::TEAL)))
Transform::from_xyz(0., 80., 1.).with_rotation(Quat::from_rotation_z(0.78))
}),
}
}
fn character_name_and_age(character: &Character) -> impl Scene {
let name = character.name.clone();
let age = character.age;
let years = if age == 1 { "year" } else { "years" };
bsn! {
CharacterNameAndAge
Text2d(format!("Hi! My name is {name}.\nI am {age} {years} old."))
Transform::from_xyz(0., -200., 0.)
}
}
fn on_pointer_over_text_cursor(
mut event: On<PointerOver>,
mut window_q: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
for window in window_q.iter_mut() {
commands
.entity(window)
.insert(CursorIcon::System(SystemCursorIcon::Text));
}
event.propagate(false);
}
fn on_pointer_over_grab_cursor(
mut event: On<PointerOver>,
mut window_q: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
for window in window_q.iter_mut() {
commands
.entity(window)
.insert(CursorIcon::System(SystemCursorIcon::Grab));
}
event.propagate(false);
}
fn on_pointer_drag_start_grabbing_cursor(
_event: On<PointerDragStart>,
mut window_q: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
for window in window_q.iter_mut() {
commands
.entity(window)
.insert(CursorIcon::System(SystemCursorIcon::Grabbing));
}
}
fn on_pointer_drag_end_grab_cursor(
_event: On<PointerDragEnd>,
mut window_q: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
for window in window_q.iter_mut() {
commands
.entity(window)
.insert(CursorIcon::System(SystemCursorIcon::Grab));
}
}
fn on_pointer_over_pointer_cursor(
mut event: On<PointerOver>,
mut window_q: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
for window in window_q.iter_mut() {
commands
.entity(window)
.insert(CursorIcon::System(SystemCursorIcon::Pointer));
}
event.propagate(false);
}
fn on_pointer_out_default_cursor(
mut event: On<PointerOut>,
mut window_q: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
for window in window_q.iter_mut() {
commands
.entity(window)
.insert(CursorIcon::System(SystemCursorIcon::Default));
}
event.propagate(false);
}