Skip to main content

button/
button.rs

1//! This example illustrates how to create a button using the `bevy_ui_widgets` widget set:
2//! a headless `Button` that fires an `Activate` event when clicked.
3//!
4//! The `bevy_ui_widgets` widgets are behavior-only — the `Button` tracks its own pressed state
5//! and detects clicks, but comes with no styling. We supply the look ourselves.
6//!
7//! ## What's next?
8//!
9//! - Keyboard & accessibility: add `bevy::input_focus::tab_navigation::TabNavigationPlugin`, wrap
10//!   your UI in a `TabGroup`, and give the button a `TabIndex`. It can then be focused with Tab and
11//!   activated with Enter/Space, firing the same `Activate` event. (`Button` already reports itself
12//!   to accessibility tools via the `AccessibilityNode` it requires.)
13//!
14//! - Disabling a button: insert the `bevy::ui::InteractionDisabled` marker to stop it from
15//!   activating, and branch on `Has<InteractionDisabled>` in `update_button_appearance` to grey it
16//!   out. See the `standard_widgets` and `standard_widgets_observers` examples for that pattern.
17//!
18//! - Activate on press instead of on release: add the `ActivateOnPress` marker (useful for things
19//!   like menu buttons that should fire the instant they're pressed).
20//!
21//! - Reacting via observers instead of a polling system: rather than updating appearance every
22//!   frame, you can observe component changes (e.g. `On<Insert, Pressed>`). The
23//!   `standard_widgets_observers` example demonstrates this approach.
24
25use bevy::{
26    color::palettes::basic::*,
27    picking::hover::Hovered,
28    prelude::*,
29    ui::Pressed,
30    ui_widgets::{observe, Activate, Button},
31};
32
33fn main() {
34    App::new()
35        // `DefaultPlugins` already includes the `bevy_ui_widgets` plugins, so the `Button`
36        // widget's behavior (and its `Activate` event) works out of the box.
37        .add_plugins(DefaultPlugins)
38        .add_systems(Startup, setup)
39        // Update the button's appearance every frame based on its current state.
40        .add_systems(Update, update_button_appearance)
41        .run();
42}
43
44const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
45const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
46const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
47
48fn setup(mut commands: Commands, assets: Res<AssetServer>) {
49    // ui camera
50    commands.spawn(Camera2d);
51
52    // A full-screen container that centers the button.
53    commands.spawn((
54        Node {
55            width: percent(100),
56            height: percent(100),
57            align_items: AlignItems::Center,
58            justify_content: JustifyContent::Center,
59            ..default()
60        },
61        children![(
62            button(&assets),
63            // React to the button being clicked. `Button` fires an `Activate` event on a
64            // completed click, so we attach an observer for it right here on the button entity.
65            //
66            // Try it: this is where you'd run your own logic (start a game, open a menu, etc.).
67            observe(|_activate: On<Activate>| {
68                info!("Button clicked!");
69            }),
70        )],
71    ));
72}
73
74fn button(asset_server: &AssetServer) -> impl Bundle {
75    (
76        // The headless button widget. It handles pointer/keyboard input and pressed-state
77        // tracking for us; we only provide the look below.
78        Button,
79        // `Hovered` is used by the picking backend to track if the pointer is over the button.
80        Hovered::default(),
81        Node {
82            width: px(150),
83            height: px(65),
84            border: UiRect::all(px(5)),
85            // horizontally center child text
86            justify_content: JustifyContent::Center,
87            // vertically center child text
88            align_items: AlignItems::Center,
89            border_radius: BorderRadius::MAX,
90            ..default()
91        },
92        BorderColor::all(Color::BLACK),
93        BackgroundColor(NORMAL_BUTTON),
94        children![(
95            Text::new("Button"),
96            TextFont {
97                font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
98                font_size: FontSize::Px(33.0),
99                ..default()
100            },
101            TextColor(Color::srgb(0.9, 0.9, 0.9)),
102            TextShadow::default(),
103        )],
104    )
105}
106
107/// Restyle the button and update its label to reflect its current state.
108///
109/// The `Button` widget maintains a `Pressed` component while the button is held down, and the
110/// picking backend keeps `Hovered` up to date. We simply read those each frame and pick a look.
111fn update_button_appearance(
112    mut buttons: Query<
113        (
114            &Hovered,
115            Has<Pressed>,
116            &mut BackgroundColor,
117            &mut BorderColor,
118            &Children,
119        ),
120        With<Button>,
121    >,
122    mut text_query: Query<&mut Text>,
123) {
124    for (hovered, pressed, mut color, mut border_color, children) in &mut buttons {
125        let Ok(mut text) = text_query.get_mut(children[0]) else {
126            continue;
127        };
128
129        match (hovered.get(), pressed) {
130            // Pressed (and, since you can only press what you're hovering, also hovered).
131            (_, true) => {
132                **text = "Press".to_string();
133                *color = PRESSED_BUTTON.into();
134                border_color.set_all(RED);
135            }
136            // Hovered but not pressed.
137            (true, false) => {
138                **text = "Hover".to_string();
139                *color = HOVERED_BUTTON.into();
140                border_color.set_all(WHITE);
141            }
142            // Neither hovered nor pressed.
143            (false, false) => {
144                **text = "Button".to_string();
145                *color = NORMAL_BUTTON.into();
146                border_color.set_all(BLACK);
147            }
148        }
149    }
150}