bevy_button_transitions/
lib.rs

1//! Provides Unity style button interactions
2//!
3//! Currently supports color tinting and image swapping.
4//!
5//! # Getting started
6//! Import the crate to bring all necessary types into scope:
7//! ```
8//! use bevy_button_transitions::*;
9//! ```
10//!
11//! Add [`ButtonTransitionsPlugin`] to setup the system:
12//! ```
13//! # /*
14//! app.add_plugins(ButtonTransitionsPlugin);
15//! # */
16//! ```
17//!
18//! Add the [`ButtonTransition`] component to your buttons
19//! ```
20//! commands.spawn((
21//!     ButtonTransition::ColorTint(ColorTint::default()),
22//!     ImageNode {
23//!         image: asset_server.load("normal_image.png"),
24//!         ..default()
25//!     },
26//!     Node {
27//!         width: Val::Px(250.0),
28//!         height: Val::Px(80.0),
29//!         ..default()
30//!     },
31//! ));
32//! ```
33//!
34//! Be aware that the color tint button transition needs an image to tint!
35use bevy::{color::palettes::css::WHITE, prelude::*};
36
37/// A [`Plugin`] that sets up button transitions.
38pub struct ButtonTransitionsPlugin;
39
40impl Plugin for ButtonTransitionsPlugin {
41    fn build(&self, app: &mut bevy::prelude::App) {
42        app.register_type::<ButtonTransition>()
43            .register_type::<Interactable>()
44            .add_systems(Update, update_button_interactions);
45    }
46}
47
48/// A [`Component`] that automatically adds transitions for a button.
49///
50/// # Remarks
51/// Remember to add an ImageNode with a base image for the ColorTint Transition - otherwise
52/// there is nothing to tint!
53#[derive(Component, Reflect)]
54#[reflect(Component)]
55#[require(Button, Interactable, ImageNode)]
56pub enum ButtonTransition {
57    ColorTint(ColorTint),
58    ImageSwap(ImageSwap),
59}
60
61/// Defines the different tints of a [`ButtonTransition::ColorTint`].
62#[derive(Reflect)]
63pub struct ColorTint {
64    pub normal_color: Color,
65    pub hovered_color: Color,
66    pub pressed_color: Color,
67    pub disabled_color: Color,
68}
69
70impl Default for ColorTint {
71    fn default() -> Self {
72        ColorTint {
73            normal_color: WHITE.into(),
74            hovered_color: Color::srgba(0.9607843, 0.9607843, 0.9607843, 1.0),
75            pressed_color: Color::srgba(0.7843137, 0.7843137, 0.7843137, 1.0),
76            disabled_color: Color::srgba(0.7843137, 0.7843137, 0.7843137, 0.5019608),
77        }
78    }
79}
80
81/// Defines the different image swaps of a [`ButtonTransition::ImageSwap`].
82#[derive(Reflect)]
83pub struct ImageSwap {
84    pub normal_image: Handle<Image>,
85    pub hovered_image: Handle<Image>,
86    pub pressed_image: Handle<Image>,
87    pub disabled_image: Handle<Image>,
88}
89
90/// A [`Component`] that determines if a button can be pressed.
91///
92/// Will automatically be added with the ButtonTransition but has to
93/// be queried by a user manually if a button is pressed.
94/// # Example
95/// ```
96/// fn check_button_press(query: Query<(&Interaction, &Interactable)>) {
97///     for (interaction, interactable) in &query {
98///         if interactable.0 == false {
99///             continue;
100///         }
101///         match interaction {
102///             _ => todo!("add interactions here")
103///         }
104///     }
105/// }
106/// ```
107#[derive(Component, Deref, DerefMut, Reflect)]
108#[reflect(Component)]
109pub struct Interactable(pub bool);
110
111impl Default for Interactable {
112    fn default() -> Self {
113        Interactable(true)
114    }
115}
116
117fn update_button_interactions(
118    mut query: Query<(
119        &mut ImageNode,
120        &Interactable,
121        &ButtonTransition,
122        &Interaction,
123    )>,
124) {
125    for (mut image_node, interactable, transition, interaction) in &mut query {
126        match transition {
127            ButtonTransition::ColorTint(tint) => {
128                let color = if !interactable.0 {
129                    tint.disabled_color
130                } else {
131                    match interaction {
132                        Interaction::Hovered => tint.hovered_color,
133                        Interaction::Pressed => tint.pressed_color,
134                        Interaction::None => tint.normal_color,
135                    }
136                };
137                image_node.color = color;
138            }
139            ButtonTransition::ImageSwap(swap) => {
140                let image = if !interactable.0 {
141                    swap.disabled_image.clone()
142                } else {
143                    match interaction {
144                        Interaction::Hovered => swap.hovered_image.clone(),
145                        Interaction::Pressed => swap.pressed_image.clone(),
146                        Interaction::None => swap.normal_image.clone(),
147                    }
148                };
149                image_node.image = image;
150            }
151        }
152    }
153}