size_constraints/
size_constraints.rs

1//! Demonstrates how the to use the size constraints to control the size of a UI node.
2
3use bevy::{color::palettes::css::*, prelude::*};
4
5fn main() {
6    App::new()
7        .add_plugins(DefaultPlugins)
8        .add_event::<ButtonActivatedEvent>()
9        .add_systems(Startup, setup)
10        .add_systems(Update, (update_buttons, update_radio_buttons_colors))
11        .run();
12}
13
14const ACTIVE_BORDER_COLOR: Color = Color::Srgba(ANTIQUE_WHITE);
15const INACTIVE_BORDER_COLOR: Color = Color::BLACK;
16
17const ACTIVE_INNER_COLOR: Color = Color::WHITE;
18const INACTIVE_INNER_COLOR: Color = Color::Srgba(NAVY);
19
20const ACTIVE_TEXT_COLOR: Color = Color::BLACK;
21const HOVERED_TEXT_COLOR: Color = Color::WHITE;
22const UNHOVERED_TEXT_COLOR: Color = Color::srgb(0.5, 0.5, 0.5);
23
24#[derive(Component)]
25struct Bar;
26
27#[derive(Copy, Clone, Debug, Component, PartialEq)]
28enum Constraint {
29    FlexBasis,
30    Width,
31    MinWidth,
32    MaxWidth,
33}
34
35#[derive(Copy, Clone, Component)]
36struct ButtonValue(Val);
37
38#[derive(Event)]
39struct ButtonActivatedEvent(Entity);
40
41fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
42    // ui camera
43    commands.spawn(Camera2d);
44
45    let text_font = (
46        TextFont {
47            font: asset_server.load("fonts/FiraSans-Bold.ttf"),
48            font_size: 33.0,
49            ..Default::default()
50        },
51        TextColor(Color::srgb(0.9, 0.9, 0.9)),
52    );
53
54    commands
55        .spawn((
56            Node {
57                width: Val::Percent(100.0),
58                height: Val::Percent(100.0),
59                justify_content: JustifyContent::Center,
60                align_items: AlignItems::Center,
61                ..default()
62            },
63            BackgroundColor(Color::BLACK),
64        ))
65        .with_children(|parent| {
66            parent
67                .spawn(Node {
68                    flex_direction: FlexDirection::Column,
69                    align_items: AlignItems::Center,
70                    justify_content: JustifyContent::Center,
71                    ..default()
72                })
73                .with_children(|parent| {
74                    parent.spawn((
75                        Text::new("Size Constraints Example"),
76                        text_font.clone(),
77                        Node {
78                            margin: UiRect::bottom(Val::Px(25.)),
79                            ..Default::default()
80                        },
81                    ));
82
83                    spawn_bar(parent);
84
85                    parent
86                        .spawn((
87                            Node {
88                                flex_direction: FlexDirection::Column,
89                                align_items: AlignItems::Stretch,
90                                padding: UiRect::all(Val::Px(10.)),
91                                margin: UiRect::top(Val::Px(50.)),
92                                ..default()
93                            },
94                            BackgroundColor(YELLOW.into()),
95                        ))
96                        .with_children(|parent| {
97                            for constraint in [
98                                Constraint::MinWidth,
99                                Constraint::FlexBasis,
100                                Constraint::Width,
101                                Constraint::MaxWidth,
102                            ] {
103                                spawn_button_row(parent, constraint, text_font.clone());
104                            }
105                        });
106                });
107        });
108}
109
110fn spawn_bar(parent: &mut ChildSpawnerCommands) {
111    parent
112        .spawn((
113            Node {
114                flex_basis: Val::Percent(100.0),
115                align_self: AlignSelf::Stretch,
116                padding: UiRect::all(Val::Px(10.)),
117                ..default()
118            },
119            BackgroundColor(YELLOW.into()),
120        ))
121        .with_children(|parent| {
122            parent
123                .spawn((
124                    Node {
125                        align_items: AlignItems::Stretch,
126                        width: Val::Percent(100.),
127                        height: Val::Px(100.),
128                        padding: UiRect::all(Val::Px(4.)),
129                        ..default()
130                    },
131                    BackgroundColor(Color::BLACK),
132                ))
133                .with_children(|parent| {
134                    parent.spawn((Node::default(), BackgroundColor(Color::WHITE), Bar));
135                });
136        });
137}
138
139fn spawn_button_row(
140    parent: &mut ChildSpawnerCommands,
141    constraint: Constraint,
142    text_style: (TextFont, TextColor),
143) {
144    let label = match constraint {
145        Constraint::FlexBasis => "flex_basis",
146        Constraint::Width => "size",
147        Constraint::MinWidth => "min_size",
148        Constraint::MaxWidth => "max_size",
149    };
150
151    parent
152        .spawn((
153            Node {
154                flex_direction: FlexDirection::Column,
155                padding: UiRect::all(Val::Px(2.)),
156                align_items: AlignItems::Stretch,
157                ..default()
158            },
159            BackgroundColor(Color::BLACK),
160        ))
161        .with_children(|parent| {
162            parent
163                .spawn(Node {
164                    flex_direction: FlexDirection::Row,
165                    justify_content: JustifyContent::End,
166                    padding: UiRect::all(Val::Px(2.)),
167                    ..default()
168                })
169                .with_children(|parent| {
170                    // spawn row label
171                    parent
172                        .spawn((Node {
173                            min_width: Val::Px(200.),
174                            max_width: Val::Px(200.),
175                            justify_content: JustifyContent::Center,
176                            align_items: AlignItems::Center,
177                            ..default()
178                        },))
179                        .with_child((Text::new(label), text_style.clone()));
180
181                    // spawn row buttons
182                    parent.spawn(Node::default()).with_children(|parent| {
183                        spawn_button(
184                            parent,
185                            constraint,
186                            ButtonValue(Val::Auto),
187                            "Auto".to_string(),
188                            text_style.clone(),
189                            true,
190                        );
191                        for percent in [0., 25., 50., 75., 100., 125.] {
192                            spawn_button(
193                                parent,
194                                constraint,
195                                ButtonValue(Val::Percent(percent)),
196                                format!("{percent}%"),
197                                text_style.clone(),
198                                false,
199                            );
200                        }
201                    });
202                });
203        });
204}
205
206fn spawn_button(
207    parent: &mut ChildSpawnerCommands,
208    constraint: Constraint,
209    action: ButtonValue,
210    label: String,
211    text_style: (TextFont, TextColor),
212    active: bool,
213) {
214    parent
215        .spawn((
216            Button,
217            Node {
218                align_items: AlignItems::Center,
219                justify_content: JustifyContent::Center,
220                border: UiRect::all(Val::Px(2.)),
221                margin: UiRect::horizontal(Val::Px(2.)),
222                ..Default::default()
223            },
224            BorderColor(if active {
225                ACTIVE_BORDER_COLOR
226            } else {
227                INACTIVE_BORDER_COLOR
228            }),
229            constraint,
230            action,
231        ))
232        .with_children(|parent| {
233            parent
234                .spawn((
235                    Node {
236                        width: Val::Px(100.),
237                        justify_content: JustifyContent::Center,
238                        ..default()
239                    },
240                    BackgroundColor(if active {
241                        ACTIVE_INNER_COLOR
242                    } else {
243                        INACTIVE_INNER_COLOR
244                    }),
245                ))
246                .with_child((
247                    Text::new(label),
248                    text_style.0,
249                    TextColor(if active {
250                        ACTIVE_TEXT_COLOR
251                    } else {
252                        UNHOVERED_TEXT_COLOR
253                    }),
254                    TextLayout::new_with_justify(JustifyText::Center),
255                ));
256        });
257}
258
259fn update_buttons(
260    mut button_query: Query<
261        (Entity, &Interaction, &Constraint, &ButtonValue),
262        Changed<Interaction>,
263    >,
264    mut bar_node: Single<&mut Node, With<Bar>>,
265    mut text_query: Query<&mut TextColor>,
266    children_query: Query<&Children>,
267    mut button_activated_event: EventWriter<ButtonActivatedEvent>,
268) {
269    for (button_id, interaction, constraint, value) in button_query.iter_mut() {
270        match interaction {
271            Interaction::Pressed => {
272                button_activated_event.write(ButtonActivatedEvent(button_id));
273                match constraint {
274                    Constraint::FlexBasis => {
275                        bar_node.flex_basis = value.0;
276                    }
277                    Constraint::Width => {
278                        bar_node.width = value.0;
279                    }
280                    Constraint::MinWidth => {
281                        bar_node.min_width = value.0;
282                    }
283                    Constraint::MaxWidth => {
284                        bar_node.max_width = value.0;
285                    }
286                }
287            }
288            Interaction::Hovered => {
289                if let Ok(children) = children_query.get(button_id) {
290                    for &child in children {
291                        if let Ok(grand_children) = children_query.get(child) {
292                            for &grandchild in grand_children {
293                                if let Ok(mut text_color) = text_query.get_mut(grandchild) {
294                                    if text_color.0 != ACTIVE_TEXT_COLOR {
295                                        text_color.0 = HOVERED_TEXT_COLOR;
296                                    }
297                                }
298                            }
299                        }
300                    }
301                }
302            }
303            Interaction::None => {
304                if let Ok(children) = children_query.get(button_id) {
305                    for &child in children {
306                        if let Ok(grand_children) = children_query.get(child) {
307                            for &grandchild in grand_children {
308                                if let Ok(mut text_color) = text_query.get_mut(grandchild) {
309                                    if text_color.0 != ACTIVE_TEXT_COLOR {
310                                        text_color.0 = UNHOVERED_TEXT_COLOR;
311                                    }
312                                }
313                            }
314                        }
315                    }
316                }
317            }
318        }
319    }
320}
321
322fn update_radio_buttons_colors(
323    mut event_reader: EventReader<ButtonActivatedEvent>,
324    button_query: Query<(Entity, &Constraint, &Interaction)>,
325    mut border_query: Query<&mut BorderColor>,
326    mut color_query: Query<&mut BackgroundColor>,
327    mut text_query: Query<&mut TextColor>,
328    children_query: Query<&Children>,
329) {
330    for &ButtonActivatedEvent(button_id) in event_reader.read() {
331        let (_, target_constraint, _) = button_query.get(button_id).unwrap();
332        for (id, constraint, interaction) in button_query.iter() {
333            if target_constraint == constraint {
334                let (border_color, inner_color, label_color) = if id == button_id {
335                    (ACTIVE_BORDER_COLOR, ACTIVE_INNER_COLOR, ACTIVE_TEXT_COLOR)
336                } else {
337                    (
338                        INACTIVE_BORDER_COLOR,
339                        INACTIVE_INNER_COLOR,
340                        if matches!(interaction, Interaction::Hovered) {
341                            HOVERED_TEXT_COLOR
342                        } else {
343                            UNHOVERED_TEXT_COLOR
344                        },
345                    )
346                };
347
348                border_query.get_mut(id).unwrap().0 = border_color;
349                for &child in children_query.get(id).into_iter().flatten() {
350                    color_query.get_mut(child).unwrap().0 = inner_color;
351                    for &grandchild in children_query.get(child).into_iter().flatten() {
352                        if let Ok(mut text_color) = text_query.get_mut(grandchild) {
353                            text_color.0 = label_color;
354                        }
355                    }
356                }
357            }
358        }
359    }
360}