1use bevy::{
19 color::palettes,
20 ecs::schedule::IntoScheduleConfigs,
21 input_focus::tab_navigation::TabIndex,
22 prelude::*,
23 text::{EditableText, TextCursorStyle},
24 ui::Checked,
25 ui_widgets::{
26 Checkbox, RadioButton, RadioGroup, Slider, SliderOrientation, SliderPrecision, SliderRange,
27 SliderThumb, SliderValue, TextInput, TrackClick, ValueChange,
28 },
29 window::{CursorIcon, PrimaryWindow, SystemCursorIcon},
30};
31
32fn main() {
33 App::new()
34 .add_plugins(DefaultPlugins)
35 .init_resource::<Character>()
36 .add_systems(Startup, setup)
37 .add_systems(
38 Update,
39 (
40 on_changed_editable_text,
44 refresh_character.run_if(resource_exists_and::<Character>(|character| {
46 !character.changed_fields.is_empty()
47 })),
48 )
49 .chain(),
50 )
51 .run();
52}
53
54fn setup(mut commands: Commands, character: Res<Character>) {
55 commands.spawn_scene_list(bsn_list! {
56 Camera2d
57 --
58 @ui(&character)
65 --
66 @character_view(&character)
69 });
70}
71
72#[derive(Resource)]
75struct Character {
76 name: String,
77 age: u32,
78 hat_type: HatType,
79 tint_yellow: bool,
80 changed_fields: Vec<ChangedField>,
83}
84
85impl Default for Character {
86 fn default() -> Self {
87 Character {
88 name: "Bevy".into(),
89 age: 5,
90 hat_type: HatType::default(),
91 tint_yellow: false,
92 changed_fields: vec![],
93 }
94 }
95}
96
97#[derive(Component, Clone, Copy, Debug, Default, PartialEq)]
101enum HatType {
102 #[default]
103 None,
104 TopHat,
105 DunceCap,
106}
107
108#[derive(Clone, Copy, Debug, PartialEq)]
109enum ChangedField {
110 Name,
111 Age,
112 HatType,
113 TintYellow,
114}
115
116const HAT_TYPES: [HatType; 3] = [HatType::None, HatType::TopHat, HatType::DunceCap];
117
118#[derive(Component, Clone, Default)]
121struct NameInput;
122
123#[derive(Component, Clone, Default)]
124struct AgeSlider;
125
126#[derive(Component, Clone, Default)]
127struct AgeSliderThumb;
128
129#[derive(Component, Clone, Default)]
130struct AgeSliderText;
131
132#[derive(Component, Clone, Default)]
133struct HatTypeRadioGroup;
134
135#[derive(Component, Clone, Default)]
136struct TintYellowCheckbox;
137
138#[derive(Component, Clone, Default)]
139struct CharacterView;
140
141#[derive(Component, Clone, Default)]
142struct CharacterSprite;
143
144#[derive(Component, Clone, Default)]
145struct CharacterHat;
146
147#[derive(Component, Clone, Default)]
148struct CharacterNameAndAge;
149
150fn ui(character: &Character) -> impl Scene {
157 bsn! {
158 Node {
160 flex_direction: FlexDirection::Column,
161 justify_content: JustifyContent::Center,
162 align_items: AlignItems::Center,
163 height: percent(100)
164 width: percent(50),
165 }
166 Children [
167 Node {
169 flex_direction: FlexDirection::Column,
170 justify_content: JustifyContent::SpaceEvenly,
171 align_items: AlignItems::Center,
172 margin: percent(5),
173 width: percent(90),
174 padding: UiRect::vertical(px(10)),
175 border_radius: BorderRadius::all(px(5)),
176 row_gap: px(10),
177 }
178 BackgroundColor(palettes::basic::GRAY)
179 Children [
180 Node
182 Children[
183 Text("Character Creator")
184 ]
185 --
186 @name_text_input_row(character)
187 --
188 @age_slider_row(character)
189 --
190 @hat_type_radio_group_row(character)
191 --
192 @tint_yellow_checkbox_row(character)
193 ]
194 ]
195 }
196}
197
198fn name_text_input_row(character: &Character) -> impl Scene {
202 bsn! {
203 Node {
204 flex_direction: FlexDirection::Row,
205 align_items: AlignItems::Center,
206 justify_content: JustifyContent::SpaceBetween,
207 }
208 Children [
209 Node
210 Children [
211 Text::new("Name: ")
212 ]
213 --
214 @name_text_input(character)
215 ]
216 }
217}
218
219fn name_text_input(character: &Character) -> impl Scene {
220 let name = character.name.clone();
221 bsn! {
222 Node {
223 width: px(200),
224 border: px(5),
225 border_radius: BorderRadius::all(px(10)),
226 padding: UiRect::axes(px(5), px(2)),
227 }
228 NameInput
229 TextInput
230 EditableText::new(name)
231 TabIndex(0)
233 TextCursorStyle::default()
235 BackgroundColor(Color::BLACK)
236 on(on_pointer_over_text_cursor)
237 on(on_pointer_out_default_cursor)
238 }
239}
240
241fn on_changed_editable_text(
246 name_input_q: Query<&EditableText, With<NameInput>>,
247 mut character: ResMut<Character>,
248) {
249 let Ok(editable_text) = name_input_q.single_inner() else {
250 return;
251 };
252 let new_name = editable_text.value().to_string();
253 if character.name != new_name {
254 character.name = new_name;
255 character.changed_fields.push(ChangedField::Name);
256 }
257
258 }
263
264fn age_slider_row(character: &Character) -> impl Scene {
270 let age = character.age;
271 bsn! {
272 Node {
273 flex_direction: FlexDirection::Row,
274 align_items: AlignItems::Center,
275 justify_content: JustifyContent::SpaceBetween,
276 column_gap: px(10),
277 }
278 Children [
279 Node
280 Children [
281 Text::new("Age:")
282 ]
283 --
284 @age_slider(character)
285 --
286 Node {
287 width: px(30),
288 }
289 Children [
290 AgeSliderText
291 Text(format!("{}", age))
292 ]
293 ]
294 }
295}
296
297fn age_slider(character: &Character) -> impl Scene {
298 bsn! {
299 Node {
300 width: px(220),
301 border: px(5),
302 padding: UiRect::axes(px(5), px(2)),
303 }
304 AgeSlider
305 Slider {
306 track_click: TrackClick::Snap,
307 orientation: SliderOrientation::Horizontal,
308 }
309 SliderValue({character.age as f32})
310 SliderPrecision(0)
312 SliderRange::new(1., 100.)
313 BackgroundColor(Color::BLACK)
314 on(on_value_change_age_slider)
316 on(on_pointer_over_pointer_cursor)
317 on(on_pointer_drag_start_grabbing_cursor)
318 on(on_pointer_drag_end_grab_cursor)
319 on(on_pointer_out_default_cursor)
320 Children [
321 Node {
324 height: px(5),
325 border_radius: BorderRadius::all(px(3)),
326 }
327 BackgroundColor(Color::BLACK)
328 --
329 Node {
333 display: Display::Flex,
334 position_type: PositionType::Absolute,
335 left: px(0)
336 right: px(20),
339 top: px(0),
340 bottom: px(0),
341 }
342 Children [
343 AgeSliderThumb
344 SliderThumb
345 Node {
346 display: Display::Flex,
347 width: px(20),
348 height: px(10),
349 position_type: PositionType::Absolute,
350 left: percent((character.age as f32 - 1.) / (100. - 1.) * 100.),
352 }
353 BackgroundColor(Color::WHITE)
354 on(on_pointer_over_grab_cursor)
355 on(on_pointer_out_default_cursor)
356 on(on_pointer_drag_start_grabbing_cursor)
357 on(on_pointer_drag_end_grab_cursor)
358 ]
359 ]
360 }
361}
362
363fn on_value_change_age_slider(
369 event: On<ValueChange<f32>>,
370 age_slider_q: Query<(Entity, &SliderRange), With<AgeSlider>>,
371 mut age_slider_text_q: Query<&mut Text, With<AgeSliderText>>,
372 mut age_slider_thumb_q: Query<&mut Node, With<AgeSliderThumb>>,
373 mut character: ResMut<Character>,
374 mut commands: Commands,
375) {
376 let Ok((entity, slider_range)) = age_slider_q.single_inner() else {
377 return;
378 };
379 if event.source != entity {
380 return;
381 }
382
383 character.age = event.value as u32;
386 character.changed_fields.push(ChangedField::Age);
387
388 commands
390 .entity(event.source)
391 .insert(SliderValue(character.age as f32));
392 for mut node in age_slider_thumb_q.iter_mut() {
393 node.left = percent(slider_range.thumb_position(character.age as f32) * 100.0);
394 }
395 for mut text in age_slider_text_q.iter_mut() {
396 *text = Text::new(format!("{}", character.age));
397 }
398
399 }
401
402fn hat_type_radio_group_row(character: &Character) -> impl Scene {
408 bsn! {
409 Node {
410 flex_direction: FlexDirection::Row,
411 align_items: AlignItems::Center,
412 justify_content: JustifyContent::SpaceBetween,
413 }
414 RadioGroup
415 HatTypeRadioGroup
416 on(on_value_change_hat_type)
418 Children [
419 Node
420 Children [
421 Text("Hat: ")
422 ]
423 --
424 {
425 HAT_TYPES.iter()
426 .map(|hat_type| hat_type_radio_button(*hat_type, character))
427 .collect::<Vec<_>>()
428 }
429 ]
430 }
431}
432
433fn hat_type_radio_button(hat_type: HatType, character: &Character) -> Box<dyn Scene> {
434 let base_radio_button = || {
435 bsn! {
436 Node {
437 border: px(5),
438 border_radius: BorderRadius::all(px(10)),
439 padding: UiRect::axes(px(5), px(2)),
440 }
441 RadioButton
442 hat_type
443 BackgroundColor(Color::BLACK)
444 on(on_pointer_over_pointer_cursor)
445 on(on_pointer_out_default_cursor)
446 }
447 };
448 if character.hat_type == hat_type {
449 Box::new(bsn! {
450 @base_radio_button()
451 Checked
453 Children [
454 Text::new(format!("{hat_type:?}"))
455 TextColor(palettes::basic::GREEN)
457 ]
458 })
459 } else {
460 Box::new(bsn! {
461 @base_radio_button()
462 Children [
463 Text(format!("{hat_type:?}"))
464 TextColor(palettes::basic::WHITE)
465 ]
466 })
467 }
468}
469
470fn on_value_change_hat_type(
476 event: On<ValueChange<Entity>>,
477 hat_type_value_q: Query<(Entity, &HatType, Has<Checked>, &Children), With<RadioButton>>,
478 hat_type_radio_group_q: Single<Entity, With<HatTypeRadioGroup>>,
479 mut character: ResMut<Character>,
480 mut commands: Commands,
481) {
482 if event.source != hat_type_radio_group_q.entity() {
486 return;
487 }
488
489 let Ok((_, new_hat_type, has_checked, _)) = hat_type_value_q.get(event.value) else {
490 return;
491 };
492
493 if has_checked {
494 return;
496 }
497
498 character.hat_type = *new_hat_type;
500 character.changed_fields.push(ChangedField::HatType);
501
502 for (button_entity, hat_type, has_checked, children) in hat_type_value_q.iter() {
504 if character.hat_type == *hat_type {
505 commands.entity(button_entity).insert(Checked);
506 commands
508 .entity(children[0])
509 .insert(TextColor(palettes::basic::GREEN.into()));
510 } else if has_checked {
511 commands.entity(button_entity).remove::<Checked>();
512 commands.entity(children[0]).insert(TextColor(Color::WHITE));
513 }
514 }
515 }
517
518fn tint_yellow_checkbox_row(character: &Character) -> impl Scene {
524 bsn! {
525 Node {
526 flex_direction: FlexDirection::Row,
527 align_items: AlignItems::Center,
528 justify_content: JustifyContent::SpaceBetween,
529 }
530 Children [
531 Node
532 Children [
533 Text("Tint Yellow: ")
534 ]
535 --
536 @tint_yellow_checkbox(character)
537 ]
538 }
539}
540
541fn tint_yellow_checkbox(character: &Character) -> Box<dyn Scene> {
542 let base_checkbox = || {
543 bsn! {
544 Node {
545 padding: UiRect::horizontal(px(5)),
546 }
547 Checkbox
548 TintYellowCheckbox
549 BackgroundColor(Color::WHITE)
550 on(on_pointer_over_pointer_cursor)
551 on(on_pointer_out_default_cursor)
552 on(on_value_change_tint_yellow)
554 }
555 };
556
557 if character.tint_yellow {
558 Box::new(bsn! {
559 @base_checkbox()
560 Checked
561 Children [
562 Text("X")
563 TextColor(palettes::basic::GREEN)
564 ]
565 })
566 } else {
567 Box::new(bsn! {
568 @base_checkbox()
569 Children [
570 Text(" ")
571 TextColor(palettes::basic::GREEN)
572 ]
573 })
574 }
575}
576
577fn on_value_change_tint_yellow(
583 event: On<ValueChange<bool>>,
584 tint_yellow_checkbox_q: Query<(Entity, &Children), With<TintYellowCheckbox>>,
585 mut character: ResMut<Character>,
586 mut commands: Commands,
587) {
588 let Ok((checkbox_entity, children)) = tint_yellow_checkbox_q.single_inner() else {
589 return;
590 };
591
592 if event.source != checkbox_entity {
596 return;
597 }
598
599 character.tint_yellow = event.value;
601 character.changed_fields.push(ChangedField::TintYellow);
602
603 if character.tint_yellow {
605 commands.entity(event.source).insert(Checked);
606 commands.entity(children[0]).insert(Text::new("X"));
608 } else {
609 commands.entity(event.source).remove::<Checked>();
610 commands.entity(children[0]).insert(Text::new(" "));
611 }
612 }
614
615fn character_view(character: &Character) -> impl Scene {
623 bsn! {
624 CharacterView
625 Transform::from_xyz(320., 0., 0.)
626 Visibility::Inherited
627 Children [
628 @character_sprite(&character)
629 --
630 @character_hat(&character)
631 --
632 @character_name_and_age(&character)
633 ]
634 }
635}
636
637fn refresh_character(
639 mut commands: Commands,
640 character_view_q: Single<(Entity, &Children), With<CharacterView>>,
641 view_type_q: Query<(
642 Entity,
643 Has<CharacterSprite>,
644 Has<CharacterHat>,
645 Has<CharacterNameAndAge>,
646 )>,
647 mut character: ResMut<Character>,
648) {
649 let (character_view, children) = character_view_q.into_inner();
650 let (mut already_updated_name_age, mut already_updated_sprite, mut already_updated_hat) =
651 (false, false, false);
652 for changed_field in character.changed_fields.iter().copied() {
653 match changed_field {
656 ChangedField::Name | ChangedField::Age if !already_updated_name_age => {
657 for (child, _, _, is_name_and_age) in children
658 .iter()
659 .filter_map(|child| view_type_q.get(child).ok())
660 {
661 if is_name_and_age {
662 commands.entity(child).try_despawn();
663 }
664 }
665 let new_child = commands
666 .spawn_scene(character_name_and_age(&character))
667 .id();
668 commands.entity(character_view).add_child(new_child);
669
670 already_updated_name_age = true;
671 }
672 ChangedField::TintYellow if !already_updated_sprite => {
673 for (child, is_sprite, _, _) in children
674 .iter()
675 .filter_map(|child| view_type_q.get(child).ok())
676 {
677 if is_sprite {
678 commands.entity(child).try_despawn();
679 }
680 }
681 let new_child = commands.spawn_scene(character_sprite(&character)).id();
682 commands.entity(character_view).add_child(new_child);
683
684 already_updated_sprite = true;
685 }
686 ChangedField::HatType if !already_updated_hat => {
687 for (child, _, is_hat, _) in children
688 .iter()
689 .filter_map(|child| view_type_q.get(child).ok())
690 {
691 if is_hat {
692 commands.entity(child).try_despawn();
693 }
694 }
695 let new_child = commands.spawn_scene(character_hat(&character)).id();
696 commands.entity(character_view).add_child(new_child);
697
698 already_updated_hat = true;
699 }
700 _ => {}
701 }
702 }
703 character.changed_fields.clear();
704}
705
706fn character_sprite(character: &Character) -> Box<dyn Scene> {
707 if character.tint_yellow {
708 Box::new(bsn! {
709 CharacterSprite
710 Sprite {
711 image: "branding/icon.png",
712 color: palettes::basic::YELLOW
713 }
714 Transform
715 })
716 } else {
717 Box::new(bsn! {
718 CharacterSprite
719 Sprite {
720 image: "branding/icon.png",
721 }
722 Transform
723 })
724 }
725}
726
727fn character_hat(character: &Character) -> Box<dyn Scene> {
728 match character.hat_type {
729 HatType::None => Box::new(bsn! {
730 CharacterHat
731 }),
732 HatType::TopHat => Box::new(bsn! {
733 CharacterHat
734 Transform::from_rotation(Quat::from_rotation_z(0.78))
736 Visibility::Inherited
737 Children [
738 Mesh2d(asset_value(Rectangle::new(
740 40., 10.
741 )))
742 MeshMaterial2d<ColorMaterial>(asset_value(ColorMaterial::from_color(Color::BLACK)))
743 Transform::from_xyz(55., 60., 1.)
744 --
745 Mesh2d(asset_value(Rectangle::new(
747 20., 50.
748 )))
749 MeshMaterial2d<ColorMaterial>(asset_value(ColorMaterial::from_color(Color::BLACK)))
750 Transform::from_xyz(55., 85., 1.)
751 ]
752 }),
753 HatType::DunceCap => Box::new(bsn! {
754 CharacterHat
755 Mesh2d(asset_value(Triangle2d::new(
756 Vec2::new(0., 100.),
757 Vec2::new(-20., 0.),
758 Vec2::new(20., 0.)
759 )))
760 MeshMaterial2d<ColorMaterial>(asset_value(ColorMaterial::from_color(palettes::basic::TEAL)))
761 Transform::from_xyz(0., 80., 1.).with_rotation(Quat::from_rotation_z(0.78))
762 }),
763 }
764}
765
766fn character_name_and_age(character: &Character) -> impl Scene {
767 let name = character.name.clone();
768 let age = character.age;
769 let years = if age == 1 { "year" } else { "years" };
770 bsn! {
771 CharacterNameAndAge
772 Text2d(format!("Hi! My name is {name}.\nI am {age} {years} old."))
773 Transform::from_xyz(0., -200., 0.)
774 }
775}
776
777fn on_pointer_over_text_cursor(
784 mut event: On<PointerOver>,
785 mut window_q: Query<Entity, With<PrimaryWindow>>,
786 mut commands: Commands,
787) {
788 for window in window_q.iter_mut() {
789 commands
790 .entity(window)
791 .insert(CursorIcon::System(SystemCursorIcon::Text));
792 }
793
794 event.propagate(false);
795}
796
797fn on_pointer_over_grab_cursor(
800 mut event: On<PointerOver>,
801 mut window_q: Query<Entity, With<PrimaryWindow>>,
802 mut commands: Commands,
803) {
804 for window in window_q.iter_mut() {
805 commands
806 .entity(window)
807 .insert(CursorIcon::System(SystemCursorIcon::Grab));
808 }
809 event.propagate(false);
810}
811
812fn on_pointer_drag_start_grabbing_cursor(
815 _event: On<PointerDragStart>,
816 mut window_q: Query<Entity, With<PrimaryWindow>>,
817 mut commands: Commands,
818) {
819 for window in window_q.iter_mut() {
820 commands
821 .entity(window)
822 .insert(CursorIcon::System(SystemCursorIcon::Grabbing));
823 }
824 }
828
829fn on_pointer_drag_end_grab_cursor(
832 _event: On<PointerDragEnd>,
833 mut window_q: Query<Entity, With<PrimaryWindow>>,
834 mut commands: Commands,
835) {
836 for window in window_q.iter_mut() {
837 commands
838 .entity(window)
839 .insert(CursorIcon::System(SystemCursorIcon::Grab));
840 }
841 }
845
846fn on_pointer_over_pointer_cursor(
849 mut event: On<PointerOver>,
850 mut window_q: Query<Entity, With<PrimaryWindow>>,
851 mut commands: Commands,
852) {
853 for window in window_q.iter_mut() {
854 commands
855 .entity(window)
856 .insert(CursorIcon::System(SystemCursorIcon::Pointer));
857 }
858 event.propagate(false);
859}
860
861fn on_pointer_out_default_cursor(
864 mut event: On<PointerOut>,
865 mut window_q: Query<Entity, With<PrimaryWindow>>,
866 mut commands: Commands,
867) {
868 for window in window_q.iter_mut() {
869 commands
870 .entity(window)
871 .insert(CursorIcon::System(SystemCursorIcon::Default));
872 }
873 event.propagate(false);
874}
875
876