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}