Skip to main content

ui_transform/
ui_transform.rs

1//! An example demonstrating how to translate, rotate and scale UI elements.
2use 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/// A button that rotates the target node
26#[derive(Component)]
27pub struct RotateButton(pub Rot2);
28
29/// A button that scales the target node
30#[derive(Component)]
31pub struct ScaleButton(pub f32);
32
33/// Marker component so the systems know which entities to translate, rotate and scale
34#[derive(Component)]
35pub struct TargetNode;
36
37/// Handles button interactions
38fn 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    // Buttons that had `Pressed` removed this frame; change detection does not report removals.
54    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
87// move the rotating panel when the arrow keys are pressed
88fn 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    // UI camera
118    commands.spawn(Camera2d);
119
120    // Root node filling the whole screen
121    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                // Target node with its own set of buttons
183                (
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                // Right column of controls
279                (
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}