docs/bevy/examples/ui/widgets/button.rs 5.6 K · 150 lines · raw · history

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
25 use bevy::{
26 color::palettes::basic::*,
27 picking::hover::Hovered,
28 prelude::*,
29 ui::Pressed,
30 ui_widgets::{observe, Activate, Button},
31 };
32
33 fn 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
44 const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
45 const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
46 const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
47
48 fn 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
74 fn 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.
111 fn 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 }