docs/bevy/examples/usage/character_creation.rs 28.7 K · 876 lines · raw · history

1 //! This example illustrates how to manage data input from select
2 //! widgets in the `ui_widgets` crate.
3 //!
4 //! When a UI widget is interacted with by an application user,
5 //! various events may be triggered that that the user presumably wants
6 //! to be recognized by the application. State may be directly changed
7 //! on the widgets themselves depending on the widget. It is up to the application to
8 //! handle and interpret these events and changes, which usually means updating its own internal
9 //! state and refreshing its user interface so that the user sees the most
10 //! recent data.
11 //!
12 //! The example will implement a traditional Model View Controller (MVC) design
13 //! using the various ui widgets to power a character creation screen.
14 //!
15 //! To read more about state management, consult the [`bevy::ui_widgets`]
16 //! crate level documentation.
17
18 use 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
32 fn main() {
33 App::new()
34 .add_plugins(DefaultPlugins)
35 .init_resource::<Character>()
36 .add_systems(Startup, setup)
37 .add_systems(
38 Update,
39 (
40 // Updates the Model if the user changed the name via text input.
41 // This is a Controller system, and is not an Observer because of
42 // the way the text input widget is designed.
43 on_changed_editable_text,
44 // Updates the View after any Model changes
45 refresh_character.run_if(resource_exists_and::<Character>(|character| {
46 !character.changed_fields.is_empty()
47 })),
48 )
49 .chain(),
50 )
51 .run();
52 }
53
54 fn setup(mut commands: Commands, character: Res<Character>) {
55 commands.spawn_scene_list(bsn_list! {
56 Camera2d
57 --
58 // This scene will serve as one half of our "View"
59 // and most of the "Controller" in our MVC design.
60 // The user interacts with UI widgets, which are processed by the "Controller"
61 // via observers and systems. The observers and systems update the
62 // state and also update the look of the UI widgets
63 // (i.e. changing text color of a selected option, inserting an X).
64 @ui(&character)
65 --
66 // This scene will serve as the other half of our "View" in our MVC design.
67 // The user will see the character they are creating.
68 @character_view(&character)
69 });
70 }
71
72 /// This resource serves as the "Model" in our MVC Design.
73 /// It serves as our source of truth for the character being created.
74 #[derive(Resource)]
75 struct Character {
76 name: String,
77 age: u32,
78 hat_type: HatType,
79 tint_yellow: bool,
80 // This is used by the app to more efficiently queue refreshes
81 // of the character view by only refreshing the necessary entities.
82 changed_fields: Vec<ChangedField>,
83 }
84
85 impl 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 /// Hat options for the character to have.
98 /// This derives `Component` so each enum value can be placed on individual radio buttons
99 /// for ease of identifying which radio button was selected.
100 #[derive(Component, Clone, Copy, Debug, Default, PartialEq)]
101 enum HatType {
102 #[default]
103 None,
104 TopHat,
105 DunceCap,
106 }
107
108 #[derive(Clone, Copy, Debug, PartialEq)]
109 enum ChangedField {
110 Name,
111 Age,
112 HatType,
113 TintYellow,
114 }
115
116 const HAT_TYPES: [HatType; 3] = [HatType::None, HatType::TopHat, HatType::DunceCap];
117
118 // --- START MARKER COMPONENTS --- //
119
120 #[derive(Component, Clone, Default)]
121 struct NameInput;
122
123 #[derive(Component, Clone, Default)]
124 struct AgeSlider;
125
126 #[derive(Component, Clone, Default)]
127 struct AgeSliderThumb;
128
129 #[derive(Component, Clone, Default)]
130 struct AgeSliderText;
131
132 #[derive(Component, Clone, Default)]
133 struct HatTypeRadioGroup;
134
135 #[derive(Component, Clone, Default)]
136 struct TintYellowCheckbox;
137
138 #[derive(Component, Clone, Default)]
139 struct CharacterView;
140
141 #[derive(Component, Clone, Default)]
142 struct CharacterSprite;
143
144 #[derive(Component, Clone, Default)]
145 struct CharacterHat;
146
147 #[derive(Component, Clone, Default)]
148 struct CharacterNameAndAge;
149
150 // --- END MARKER COMPONENTS --- //
151
152 // --- WIDGET VIEW & CONTROLLER --- //
153
154 /// Spawns the ui widgets that will serve as half of our "View",
155 /// with "Controller" logic as important observers in our MVC design.
156 fn ui(character: &Character) -> impl Scene {
157 bsn! {
158 // UI will take up the left half of the screen.
159 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 // The character creation pane
168 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 // Pane header
181 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
198 // --- START TEXT INPUT -- //
199
200 /// Creates the text input row that allows the user to input a name for the character.
201 fn 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
219 fn 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 Component is necessary for the text input to receive focus for typing.
232 TabIndex(0)
233 // This is necessary so that the text cursor pops up when the input has focus.
234 TextCursorStyle::default()
235 BackgroundColor(Color::BLACK)
236 on(on_pointer_over_text_cursor)
237 on(on_pointer_out_default_cursor)
238 }
239 }
240
241 /// A system that implements Controller logic to update the Name Input.
242 /// This particular system updates the Model upon any change in value to the text input's `EditableText`.
243 /// The text input widget does not regularly emit any events on value change. Instead, the
244 /// `EditableText`'s value can be polled under app-specific conditions to update the Model when appropriate.
245 fn 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 // We do not need to update the ui widget view in our app because `EditableText`
259 // manages its own state internally; it updates the text the user sees automatically.
260
261 // Because character has been modified, refresh_character will run and update the other half of the "View".
262 }
263
264 // --- END TEXT INPUT -- //
265
266 // --- START SLIDER -- //
267
268 /// Creates the age slider that allows the user to input the age of the character.
269 fn 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
297 fn 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 // Move every whole number.
311 SliderPrecision(0)
312 SliderRange::new(1., 100.)
313 BackgroundColor(Color::BLACK)
314 // This observer is part of the Controller -- it reacts to the user's input!
315 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 // Visible Slider Track
322 // It is 220px in width via its parent.
323 Node {
324 height: px(5),
325 border_radius: BorderRadius::all(px(3)),
326 }
327 BackgroundColor(Color::BLACK)
328 --
329 // Invisible shorter track (does not have background color) that the
330 // SliderThumb glides on. This is so that the thumb
331 // does not go past the left and right sides of the visible slider track.
332 Node {
333 display: Display::Flex,
334 position_type: PositionType::Absolute,
335 left: px(0)
336 // Shortened by the slider thumb's width on the right side.
337 // This means that it is 200px in width.
338 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 // Where the thumb is along the track will be updated by `on_value_change_age_slider`
351 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
363 /// A system that implements Controller logic to update the Age.
364 /// This particular system updates the Model upon any change in value to the Age Slider.
365 /// Sliders emit a `ValueChange<f32>` event when the user drags the slider.
366 /// The value of the event is the new value of the slider.
367 /// The source of the event is the `Slider` parent entity.
368 fn 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 // Update the Model
384 // `SliderPrecision` ensures that this value is a whole number.
385 character.age = event.value as u32;
386 character.changed_fields.push(ChangedField::Age);
387
388 // Update the Widget portion of the View
389 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 // Because character has been modified, refresh_character will run and update the other half of the "View".
400 }
401
402 // --- END SLIDER -- //
403
404 // --- START RADIO GROUP -- //
405
406 /// Creates the radio group row that allows the user to select a hat for the character.
407 fn 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 // This observer is part of the Controller -- it reacts to the user's input!
417 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
433 fn 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 // The selected hat_type must have the `Checked` component.
452 Checked
453 Children [
454 Text::new(format!("{hat_type:?}"))
455 // The selected hat_type's text is green as opposed to white.
456 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
470 /// This observer is part of the Controller logic.
471 /// This observer will update the `Character` Resource based on a change to the Hat Type Radio Group.
472 /// Radio groups emit a `ValueChange<Entity>` event when the user clicks on a radio button.
473 /// The value of the event is of the clicked radio button.
474 /// The source of the event is the parent radio group.
475 fn 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 // Ensure this value change event is for the Hat Type Radio Group
483 // Although unnecessary in this example, apps with multiple radio groups need to distinguish
484 // what the value change is for.
485 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 // The hat type has actually not changed, so we do not need to do anything.
495 return;
496 }
497
498 // Update the Model
499 character.hat_type = *new_hat_type;
500 character.changed_fields.push(ChangedField::HatType);
501
502 // Update the Widget portion of the View
503 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 // The radio button only has one child for the text and color of the button
507 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 // Because character has been modified, refresh_character will run and update the other half of the "View".
516 }
517
518 // --- END RADIO GROUP -- //
519
520 // --- START CHECK BOX -- //
521
522 /// Creates the checkbox row that allows the user to toggle a yellow tint of the character.
523 fn 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
541 fn 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 // This observer is part of the controller -- it reacts to the user's input!
553 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
577 /// This observer is part of the Controller logic.
578 /// This observer will update the `Character` Resource based on a change to the Tint Yellow Checkbox.
579 /// Checkboxes emit a `ValueChange<bool>` event when the user clicks on a checkbox.
580 /// The value of the event is the value of the toggle.
581 /// The source of the event is the checkbox.
582 fn 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 // Ensure this value change event is for the checkbox.
593 // Although unnecessary in this example, apps with multiple checkboxes need to distinguish
594 // what the value change is for.
595 if event.source != checkbox_entity {
596 return;
597 }
598
599 // Update the Model
600 character.tint_yellow = event.value;
601 character.changed_fields.push(ChangedField::TintYellow);
602
603 // Update the Widget portion of the View
604 if character.tint_yellow {
605 commands.entity(event.source).insert(Checked);
606 // The checkbox only has one child for the X and its color
607 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 // Because character has been modified, refresh_character will run and update the other half of the "View".
613 }
614
615 // --- END CHECK BOX -- //
616
617 // --- END WIDGET VIEW & CONTROLLER --- //
618
619 // --- START CHARACTER VIEW --- //
620
621 /// Returns the "View", powered by data from the `Character` Model.
622 fn 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
637 /// A system that updates the "View" whenever the underlying `Character` "Model" has changed.
638 fn 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 // First, find the correct child to despawn
654 // Then, add an updated child.
655 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
706 fn 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
727 fn 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 // 0.78 radians ~ PI / 4
735 Transform::from_rotation(Quat::from_rotation_z(0.78))
736 Visibility::Inherited
737 Children [
738 // bottom wider portion of the top hat.
739 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 // top longer portion of the top hat
746 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
766 fn 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
777 // --- END CHARACTER VIEW --- //
778
779 // --- START MISC OBSERVERS (STYLING) --- //
780
781 /// An observer that styles the cursor, used primarily for text input.
782 /// This is not part of the Controller.
783 fn 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
797 /// An observer that styles the cursor, used primarily for sliders.
798 /// This is not part of the Controller.
799 fn 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
812 /// An observer that styles the cursor, used primarily for sliders.
813 /// This is not part of the Controller.
814 fn 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 // Note that this event does not stop propagation!
825 // This is because the slider widget processes drag events in order to
826 // emit value change events.
827 }
828
829 /// An observer that styles the cursor, used primarily for sliders.
830 /// This is not part of the Controller.
831 fn 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 // Note that this event does not stop propagation!
842 // This is because the slider widget processes drag events in order to
843 // emit value change events.
844 }
845
846 /// An observer that styles the cursor, used primarily for buttons / checkboxes.
847 /// This is not part of the Controller.
848 fn 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
861 /// An observer that styles the cursor, used for all widgets.
862 /// This is not part of the Controller.
863 fn 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 // --- END MISC OBSERVERS (STYLING) --- //