bevy_ui_builders/systems/
hover.rs1use bevy::prelude::*;
7use bevy_plugin_builder::define_plugin;
8
9#[derive(Component, Clone)]
15pub struct HoverScale(pub f32);
16
17impl Default for HoverScale {
18 fn default() -> Self {
19 Self(1.05) }
21}
22
23#[derive(Component, Clone)]
25pub struct HoverBrightness(pub f32);
26
27impl Default for HoverBrightness {
28 fn default() -> Self {
29 Self(1.2) }
31}
32
33#[derive(Component, Clone)]
35pub struct HoverOpacity {
36 pub normal: f32,
37 pub hovered: f32,
38}
39
40impl Default for HoverOpacity {
41 fn default() -> Self {
42 Self {
43 normal: 1.0,
44 hovered: 0.9,
45 }
46 }
47}
48
49#[derive(Component, Clone)]
51pub struct HoverOutline {
52 pub color: Color,
53 pub width: Val,
54 pub active: bool,
55}
56
57impl Default for HoverOutline {
58 fn default() -> Self {
59 Self {
60 color: Color::srgba(1.0, 1.0, 1.0, 0.3),
61 width: Val::Px(2.0),
62 active: false,
63 }
64 }
65}
66
67#[derive(Component, Clone)]
69pub struct HoverColors {
70 pub normal_bg: Color,
71 pub hover_bg: Color,
72 pub normal_border: Color,
73 pub hover_border: Color,
74}
75
76#[derive(Component)]
78pub struct OriginalColors {
79 pub background: Color,
80 pub border: Color,
81}
82
83#[derive(Component)]
85pub struct HoverAnimationState {
86 pub current_scale: f32,
88 pub target_scale: f32,
90 pub current_opacity: f32,
92 pub target_opacity: f32,
94 pub current_blend: f32,
96 pub target_blend: f32,
98 pub animation_speed: f32,
100}
101
102impl Default for HoverAnimationState {
103 fn default() -> Self {
104 Self {
105 current_scale: 1.0,
106 target_scale: 1.0,
107 current_opacity: 1.0,
108 target_opacity: 1.0,
109 current_blend: 0.0,
110 target_blend: 0.0,
111 animation_speed: 10.0,
112 }
113 }
114}
115
116#[derive(Resource)]
121pub struct HoverConfig {
122 pub default_scale: f32,
123 pub default_brightness: f32,
124 pub animation_speed: f32,
125 pub enable_sounds: bool,
126}
127
128impl Default for HoverConfig {
129 fn default() -> Self {
130 Self {
131 default_scale: 1.05,
132 default_brightness: 1.2,
133 animation_speed: 10.0,
134 enable_sounds: false,
135 }
136 }
137}
138
139pub fn universal_hover_scale(
145 mut query: Query<(
146 &Interaction,
147 &HoverScale,
148 &mut HoverAnimationState,
149 ), Changed<Interaction>>,
150) {
151 for (interaction, hover_scale, mut animation) in &mut query {
152 match interaction {
153 Interaction::Hovered => {
154 animation.target_scale = hover_scale.0;
155 }
156 Interaction::Pressed => {
157 animation.target_scale = hover_scale.0 * 0.98; }
159 Interaction::None => {
160 animation.target_scale = 1.0;
161 }
162 }
163 }
164}
165
166pub fn universal_hover_colors(
168 mut query: Query<(
169 &Interaction,
170 &HoverColors,
171 &mut HoverAnimationState,
172 ), Changed<Interaction>>,
173) {
174 for (interaction, _colors, mut animation) in &mut query {
175 match interaction {
176 Interaction::Hovered => {
177 animation.target_blend = 0.5; }
179 Interaction::Pressed => {
180 animation.target_blend = 1.0; }
182 Interaction::None => {
183 animation.target_blend = 0.0; }
185 }
186 }
187}
188
189pub fn universal_hover_opacity(
191 mut query: Query<(
192 &Interaction,
193 &HoverOpacity,
194 &mut HoverAnimationState,
195 ), Changed<Interaction>>,
196) {
197 for (interaction, hover_opacity, mut animation) in &mut query {
198 match interaction {
199 Interaction::Hovered | Interaction::Pressed => {
200 animation.target_opacity = hover_opacity.hovered;
201 }
202 Interaction::None => {
203 animation.target_opacity = hover_opacity.normal;
204 }
205 }
206 }
207}
208
209pub fn universal_hover_outline(
211 mut query: Query<(
212 Entity,
213 &Interaction,
214 &HoverOutline,
215 &mut BorderColor,
216 Option<&OriginalColors>,
217 ), Changed<Interaction>>,
218 mut commands: Commands,
219) {
220 for (entity, interaction, outline, mut border_color, original) in &mut query {
221
222 match interaction {
223 Interaction::Hovered | Interaction::Pressed => {
224 if !outline.active {
225 if original.is_none() {
227 commands.entity(entity).insert(OriginalColors {
228 background: Color::NONE,
229 border: border_color.0,
230 });
231 }
232 border_color.0 = outline.color;
233 }
235 }
236 Interaction::None => {
237 if let Some(orig) = original {
238 border_color.0 = orig.border;
239 }
240 }
241 }
242 }
243}
244
245pub fn animate_hover_transitions(
247 mut query: Query<(
248 &mut HoverAnimationState,
249 Option<&mut Transform>,
250 Option<(&HoverColors, &mut BackgroundColor, &mut BorderColor)>,
251 )>,
252 time: Res<Time>,
253) {
254 let delta = time.delta_secs();
255
256 for (mut animation, transform, colors) in &mut query {
257 if let Some(mut transform) = transform {
259 let scale_diff = animation.target_scale - animation.current_scale;
260 if scale_diff.abs() > 0.001 {
261 animation.current_scale += scale_diff * animation.animation_speed * delta;
262 transform.scale = Vec3::splat(animation.current_scale);
263 }
264 }
265
266 if let Some((hover_colors, mut bg_color, mut border_color)) = colors {
268 let blend_diff = animation.target_blend - animation.current_blend;
269 if blend_diff.abs() > 0.001 {
270 animation.current_blend += blend_diff * animation.animation_speed * delta;
271
272 bg_color.0 = lerp_color(
274 hover_colors.normal_bg,
275 hover_colors.hover_bg,
276 animation.current_blend,
277 );
278 border_color.0 = lerp_color(
279 hover_colors.normal_border,
280 hover_colors.hover_border,
281 animation.current_blend,
282 );
283 }
284 }
285
286 let opacity_diff = animation.target_opacity - animation.current_opacity;
288 if opacity_diff.abs() > 0.001 {
289 animation.current_opacity += opacity_diff * animation.animation_speed * delta;
290 }
292 }
293}
294
295pub fn init_hover_animation(
300 mut commands: Commands,
301 query: Query<
302 Entity,
303 Or<(
304 Added<HoverScale>,
305 Added<HoverColors>,
306 Added<HoverOpacity>,
307 )>,
308 >,
309) {
310 for entity in &query {
311 commands.entity(entity).try_insert(HoverAnimationState::default());
312 }
313}
314
315fn lerp_color(from: Color, to: Color, t: f32) -> Color {
321 let from_linear = from.to_linear();
322 let to_linear = to.to_linear();
323
324 Color::LinearRgba(LinearRgba {
325 red: from_linear.red + (to_linear.red - from_linear.red) * t,
326 green: from_linear.green + (to_linear.green - from_linear.green) * t,
327 blue: from_linear.blue + (to_linear.blue - from_linear.blue) * t,
328 alpha: from_linear.alpha + (to_linear.alpha - from_linear.alpha) * t,
329 })
330}
331
332pub fn apply_brightness(color: Color, brightness: f32) -> Color {
334 let rgba = color.to_linear();
335 Color::LinearRgba(LinearRgba {
336 red: (rgba.red * brightness).min(1.0),
337 green: (rgba.green * brightness).min(1.0),
338 blue: (rgba.blue * brightness).min(1.0),
339 alpha: rgba.alpha,
340 })
341}
342
343define_plugin!(HoverPlugin {
349 custom_init: |app: &mut App| {
350 app.insert_resource(HoverConfig::default());
351 },
352 update: [
353 init_hover_animation,
355
356 universal_hover_scale,
358 universal_hover_colors,
359 universal_hover_opacity,
360 universal_hover_outline,
361
362 animate_hover_transitions,
364 ]
365});