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}