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 | } |