1use bevy::color::palettes::css::DARK_GRAY;
3use bevy::color::palettes::css::RED;
4use bevy::color::palettes::css::YELLOW;
5use bevy::picking::hover::Hovered;
6use bevy::platform::collections::HashSet;
7use bevy::prelude::*;
8use bevy::ui::Pressed;
9use bevy::ui_widgets::Button;
10use core::f32::consts::FRAC_PI_8;
11
12fn main() {
13 App::new()
14 .add_plugins(DefaultPlugins)
15 .add_systems(Startup, setup)
16 .add_systems(Update, button_system)
17 .add_systems(Update, translation_system)
18 .run();
19}
20
21const NORMAL_BUTTON: Color = Color::WHITE;
22const HOVERED_BUTTON: Color = Color::Srgba(YELLOW);
23const PRESSED_BUTTON: Color = Color::Srgba(RED);
24
25#[derive(Component)]
27pub struct RotateButton(pub Rot2);
28
29#[derive(Component)]
31pub struct ScaleButton(pub f32);
32
33#[derive(Component)]
35pub struct TargetNode;
36
37fn button_system(
39 mut button_query: Query<
40 (
41 Entity,
42 &mut BackgroundColor,
43 Ref<Hovered>,
44 Option<Ref<Pressed>>,
45 Option<&RotateButton>,
46 Option<&ScaleButton>,
47 ),
48 With<Button>,
49 >,
50 mut removed_pressed: RemovedComponents<Pressed>,
51 mut rotator_query: Query<&mut UiTransform, With<TargetNode>>,
52) {
53 let just_unpressed: HashSet<Entity> = removed_pressed.read().collect();
55 for (entity, mut color, hovered, pressed, maybe_rotate, maybe_scale) in &mut button_query {
56 let changed = hovered.is_changed()
57 || pressed.as_ref().is_some_and(Ref::is_changed)
58 || just_unpressed.contains(&entity);
59 if changed {
60 match (pressed, hovered.get()) {
61 (Some(_), _) => {
62 *color = PRESSED_BUTTON.into();
63 if let Some(step) = maybe_rotate {
64 for mut transform in rotator_query.iter_mut() {
65 transform.rotation *= step.0;
66 }
67 }
68 if let Some(step) = maybe_scale {
69 for mut transform in rotator_query.iter_mut() {
70 transform.scale += step.0;
71 transform.scale =
72 transform.scale.clamp(Vec2::splat(0.25), Vec2::splat(3.0));
73 }
74 }
75 }
76 (_, true) => {
77 *color = HOVERED_BUTTON.into();
78 }
79 _ => {
80 *color = NORMAL_BUTTON.into();
81 }
82 }
83 }
84 }
85}
86
87fn translation_system(
89 time: Res<Time>,
90 input: Res<ButtonInput<KeyCode>>,
91 mut translation_query: Query<&mut UiTransform, With<TargetNode>>,
92) {
93 let controls = [
94 (KeyCode::ArrowLeft, -Vec2::X),
95 (KeyCode::ArrowRight, Vec2::X),
96 (KeyCode::ArrowUp, -Vec2::Y),
97 (KeyCode::ArrowDown, Vec2::Y),
98 ];
99 for &(key_code, direction) in &controls {
100 if input.pressed(key_code) {
101 for mut transform in translation_query.iter_mut() {
102 let d = direction * 50.0 * time.delta_secs();
103 let (Val::Px(x), Val::Px(y)) = (transform.translation.x, transform.translation.y)
104 else {
105 continue;
106 };
107 let x = (x + d.x).clamp(-150., 150.);
108 let y = (y + d.y).clamp(-150., 150.);
109
110 transform.translation = Val2::px(x, y);
111 }
112 }
113 }
114}
115
116fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
117 commands.spawn(Camera2d);
119
120 commands.spawn((
122 Node {
123 width: percent(100),
124 height: percent(100),
125 align_items: AlignItems::Center,
126 justify_content: JustifyContent::Center,
127 ..default()
128 },
129 BackgroundColor(Color::BLACK),
130 children![(
131 Node {
132 align_items: AlignItems::Center,
133 justify_content: JustifyContent::SpaceEvenly,
134 column_gap: px(25),
135 row_gap: px(25),
136 ..default()
137 },
138 BackgroundColor(Color::BLACK),
139 children![
140 (
141 Node {
142 flex_direction: FlexDirection::Column,
143 justify_content: JustifyContent::Center,
144 row_gap: px(10),
145 column_gap: px(10),
146 padding: UiRect::all(px(10)),
147 ..default()
148 },
149 BackgroundColor(Color::BLACK),
150 GlobalZIndex(1),
151 children![
152 (
153 Button,
154 Hovered::default(),
155 Node {
156 height: px(50),
157 width: px(50),
158 align_items: AlignItems::Center,
159 justify_content: JustifyContent::Center,
160 ..default()
161 },
162 BackgroundColor(Color::WHITE),
163 RotateButton(Rot2::radians(-FRAC_PI_8)),
164 children![(Text::new("<--"), TextColor(Color::BLACK),)]
165 ),
166 (
167 Button,
168 Hovered::default(),
169 Node {
170 height: px(50),
171 width: px(50),
172 align_items: AlignItems::Center,
173 justify_content: JustifyContent::Center,
174 ..default()
175 },
176 BackgroundColor(Color::WHITE),
177 ScaleButton(-0.25),
178 children![(Text::new("-"), TextColor(Color::BLACK),)]
179 ),
180 ]
181 ),
182 (
184 Node {
185 flex_direction: FlexDirection::Column,
186 justify_content: JustifyContent::SpaceBetween,
187 align_items: AlignItems::Center,
188 width: px(300),
189 height: px(300),
190 ..default()
191 },
192 BackgroundColor(DARK_GRAY.into()),
193 TargetNode,
194 children![
195 (
196 Button,
197 Hovered::default(),
198 Node {
199 width: px(80),
200 height: px(80),
201 align_items: AlignItems::Center,
202 justify_content: JustifyContent::Center,
203 ..default()
204 },
205 BackgroundColor(Color::WHITE),
206 children![(Text::new("Top"), TextColor(Color::BLACK))]
207 ),
208 (
209 Node {
210 align_self: AlignSelf::Stretch,
211 justify_content: JustifyContent::SpaceBetween,
212 align_items: AlignItems::Center,
213 ..default()
214 },
215 children![
216 (
217 Button,
218 Hovered::default(),
219 Node {
220 width: px(80),
221 height: px(80),
222 align_items: AlignItems::Center,
223 justify_content: JustifyContent::Center,
224 ..default()
225 },
226 BackgroundColor(Color::WHITE),
227 UiTransform::from_rotation(Rot2::radians(
228 -std::f32::consts::FRAC_PI_2
229 )),
230 children![(Text::new("Left"), TextColor(Color::BLACK),)]
231 ),
232 (
233 Node {
234 width: px(100),
235 height: px(100),
236 ..Default::default()
237 },
238 ImageNode {
239 image: asset_server.load("branding/icon.png"),
240 image_mode: NodeImageMode::Stretch,
241 ..default()
242 }
243 ),
244 (
245 Button,
246 Hovered::default(),
247 Node {
248 width: px(80),
249 height: px(80),
250 align_items: AlignItems::Center,
251 justify_content: JustifyContent::Center,
252 ..default()
253 },
254 UiTransform::from_rotation(Rot2::radians(
255 core::f32::consts::FRAC_PI_2
256 )),
257 BackgroundColor(Color::WHITE),
258 children![(Text::new("Right"), TextColor(Color::BLACK))]
259 ),
260 ]
261 ),
262 (
263 Button,
264 Hovered::default(),
265 Node {
266 width: px(80),
267 height: px(80),
268 align_items: AlignItems::Center,
269 justify_content: JustifyContent::Center,
270 ..default()
271 },
272 BackgroundColor(Color::WHITE),
273 UiTransform::from_rotation(Rot2::radians(std::f32::consts::PI)),
274 children![(Text::new("Bottom"), TextColor(Color::BLACK),)]
275 ),
276 ]
277 ),
278 (
280 Node {
281 flex_direction: FlexDirection::Column,
282 justify_content: JustifyContent::Center,
283 row_gap: px(10),
284 column_gap: px(10),
285 padding: UiRect::all(px(10)),
286 ..default()
287 },
288 BackgroundColor(Color::BLACK),
289 GlobalZIndex(1),
290 children![
291 (
292 Button,
293 Hovered::default(),
294 Node {
295 height: px(50),
296 width: px(50),
297 align_items: AlignItems::Center,
298 justify_content: JustifyContent::Center,
299 ..default()
300 },
301 BackgroundColor(Color::WHITE),
302 RotateButton(Rot2::radians(FRAC_PI_8)),
303 children![(Text::new("-->"), TextColor(Color::BLACK),)]
304 ),
305 (
306 Button,
307 Hovered::default(),
308 Node {
309 height: px(50),
310 width: px(50),
311 align_items: AlignItems::Center,
312 justify_content: JustifyContent::Center,
313 ..default()
314 },
315 BackgroundColor(Color::WHITE),
316 ScaleButton(0.25),
317 children![(Text::new("+"), TextColor(Color::BLACK),)]
318 ),
319 ]
320 )
321 ]
322 )],
323 ));
324}