Skip to main content

character_creation/
character_creation.rs

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
18use 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                // 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
54fn 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)]
75struct 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
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/// 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)]
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// --- START MARKER COMPONENTS --- //
119
120#[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
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.
156fn 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.
201fn 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 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.
245fn 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.
269fn 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        // 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.
368fn 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.
407fn 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
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            // 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.
475fn 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.
523fn 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            // 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.
582fn 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.
622fn 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.
638fn 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
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            // 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
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
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.
783fn 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.
799fn 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.
814fn 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.
831fn 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.
848fn 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.
863fn 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) --- //