Skip to main content

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::{
4    color::palettes::css::*,
5    prelude::*,
6    text::FontSourceTemplate,
7    ui::Checked,
8    ui_widgets::{RadioButton, RadioGroup, ValueChange},
9};
10
11fn main() {
12    App::new()
13        .add_plugins(DefaultPlugins)
14        .add_systems(Startup, setup)
15        .add_observer(on_value_change_constraints)
16        .run();
17}
18
19const ACTIVE_BORDER_COLOR: Color = Color::Srgba(ANTIQUE_WHITE);
20const INACTIVE_BORDER_COLOR: Color = Color::BLACK;
21
22const ACTIVE_INNER_COLOR: Color = Color::WHITE;
23const INACTIVE_INNER_COLOR: Color = Color::Srgba(NAVY);
24
25const ACTIVE_TEXT_COLOR: Color = Color::BLACK;
26const HOVERED_TEXT_COLOR: Color = Color::WHITE;
27const UNHOVERED_TEXT_COLOR: Color = Color::srgb(0.5, 0.5, 0.5);
28
29/// A marker component for the UI Node which will be resized by user input.
30#[derive(Component, Clone, Default)]
31struct Bar;
32
33/// The four properties on the `Node` of the `Bar` entity that can be changed.
34#[derive(Copy, Clone, Debug, Component, PartialEq)]
35enum Constraint {
36    FlexBasis,
37    Width,
38    MinWidth,
39    MaxWidth,
40}
41
42#[derive(Copy, Clone, Component, Default, PartialEq)]
43struct RadioButtonValue(Val);
44
45fn setup(mut commands: Commands) {
46    // UI Camera
47    commands.spawn(Camera2d);
48
49    commands.spawn_scene(bsn! {
50        // Background Node
51        Node {
52            width: percent(100),
53            height: percent(100),
54            justify_content: JustifyContent::Center,
55            align_items: AlignItems::Center,
56        }
57        BackgroundColor(Color::BLACK)
58        Children [
59            // Centered column that contains all the content of the example.
60            Node {
61                flex_direction: FlexDirection::Column,
62                align_items: AlignItems::Center,
63                justify_content: JustifyContent::Center,
64            }
65            Children [
66                Text("Size Constraints Example")
67                @font_style_scene()
68                Node {
69                    margin: UiRect::bottom(px(25)),
70                }
71                --
72                @bar_scene()
73                --
74                // Controls (radio buttons)
75                Node {
76                    flex_direction: FlexDirection::Column,
77                    align_items: AlignItems::Stretch,
78                    padding: UiRect::all(px(10)),
79                    margin: UiRect::top(px(50)),
80                }
81                BackgroundColor(YELLOW)
82                Children [
83                    {
84                        [
85                            Constraint::MinWidth,
86                            Constraint::FlexBasis,
87                            Constraint::Width,
88                            Constraint::MaxWidth,
89                        ].into_iter().map(|constraint| {
90                            radio_group_scene(constraint)
91                        })
92                        .collect::<Vec<_>>()
93                    }
94                ]
95            ]
96        ]
97    });
98}
99
100fn font_style_scene() -> impl Scene {
101    bsn! {
102        TextFont {
103            font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"),
104            font_size: FontSize::Px(33.0),
105        }
106        TextColor(Color::srgb(0.9, 0.9, 0.9))
107    }
108}
109
110fn bar_scene() -> impl Scene {
111    bsn! {
112        Node {
113            flex_basis: percent(100),
114            align_self: AlignSelf::Stretch,
115            padding: UiRect::all(px(10)),
116        }
117        BackgroundColor(YELLOW)
118        Children [
119            Node {
120                align_items: AlignItems::Stretch,
121                width: percent(100),
122                height: px(100),
123                padding: UiRect::all(px(4)),
124            }
125            BackgroundColor(Color::BLACK)
126            Children [
127                // This bar will grow and shrink as the constraints are tinkered with.
128                Bar
129                Node
130                BackgroundColor(Color::WHITE)
131            ]
132        ]
133    }
134}
135
136fn radio_group_scene(constraint: Constraint) -> impl Scene {
137    let label = match constraint {
138        Constraint::FlexBasis => "flex_basis",
139        Constraint::Width => "size",
140        Constraint::MinWidth => "min_size",
141        Constraint::MaxWidth => "max_size",
142    };
143
144    bsn! {
145        Node {
146            flex_direction: FlexDirection::Column,
147            padding: UiRect::all(px(2)),
148            align_items: AlignItems::Stretch,
149        }
150        BackgroundColor(Color::BLACK)
151        Children [
152            Node {
153                flex_direction: FlexDirection::Row,
154                justify_content: JustifyContent::End,
155                padding: UiRect::all(px(2)),
156            }
157            Children [
158                // Row Label
159                Node {
160                    min_width: px(200),
161                    max_width: px(200),
162                    justify_content: JustifyContent::Center,
163                    align_items: AlignItems::Center,
164                }
165                Children [
166                    Text(label)
167                    @font_style_scene()
168                ]
169                --
170                // Row Buttons
171                Node
172                RadioGroup
173                Children [
174                    Checked
175                    @radio_button_scene(
176                        constraint,
177                        RadioButtonValue(auto()),
178                        "Auto".to_string(),
179                        true,
180                    )
181                    --
182                    {
183                        [0, 25, 50, 75, 100, 125].into_iter().map(|percent_value| {
184                            radio_button_scene(
185                                constraint,
186                                RadioButtonValue(percent(percent_value)),
187                                format!("{percent_value}%"),
188                                false,
189                            )
190                        }).collect::<Vec<_>>()
191                    }
192                ]
193            ]
194        ]
195    }
196}
197
198fn radio_button_scene(
199    constraint: Constraint,
200    action: RadioButtonValue,
201    label: String,
202    active: bool,
203) -> impl Scene {
204    bsn! {
205        RadioButton
206        Node {
207            align_items: AlignItems::Center,
208            justify_content: JustifyContent::Center,
209            border: UiRect::all(px(2)),
210            margin: UiRect::horizontal(px(2)),
211        }
212        BorderColor::all(if active {
213            ACTIVE_BORDER_COLOR
214        } else {
215            INACTIVE_BORDER_COLOR
216        })
217        constraint
218        action
219        Children [
220            Node {
221                width: px(100),
222                justify_content: JustifyContent::Center,
223            }
224            BackgroundColor({if active {
225                ACTIVE_INNER_COLOR
226            } else {
227                INACTIVE_INNER_COLOR
228            }})
229            Children [
230                Text(label)
231                @font_style_scene()
232                TextColor({if active {
233                    ACTIVE_TEXT_COLOR
234                } else {
235                    UNHOVERED_TEXT_COLOR
236                }})
237                TextLayout::justify(Justify::Center)
238            ]
239        ]
240        // Observers for updating text on hover/leave
241        on(|event: On<PointerOver>,
242            has_checked_query: Query<&Checked>,
243            child_q: Query<&Children>,
244            mut commands: Commands| {
245            if has_checked_query.contains(event.entity) {
246                return;
247            }
248
249            for text_entity in child_q.iter_leaves(event.entity) {
250                commands.entity(text_entity).insert(TextColor(HOVERED_TEXT_COLOR));
251            }
252        })
253        on(|event: On<PointerOut>,
254            has_checked_query: Query<&Checked>,
255            child_q: Query<&Children>,
256            mut commands: Commands| {
257            if has_checked_query.contains(event.entity) {
258                return;
259            }
260
261            for text_entity in child_q.iter_leaves(event.entity) {
262                commands.entity(text_entity).insert(TextColor(UNHOVERED_TEXT_COLOR));
263            }
264        })
265    }
266}
267
268/// This system updates the Bar when a new value for a constraint is selected, and marks
269/// the radio button as the one currently selected
270fn on_value_change_constraints(
271    event: On<ValueChange<Entity>>,
272    new_setting_query: Query<
273        (&Constraint, &RadioButtonValue, Entity, &Children),
274        (With<RadioButton>, Without<Checked>),
275    >,
276    previous_query: Query<
277        (&Constraint, &RadioButtonValue, Entity, &Children),
278        (With<RadioButton>, With<Checked>),
279    >,
280    child_q: Query<&Children>,
281    mut commands: Commands,
282    mut bar_node: Single<&mut Node, With<Bar>>,
283) {
284    if let Ok((constraint, value, entity, children)) = new_setting_query.get(event.value) {
285        for (previous_constraint, previous_value, previous_entity, previous_children) in
286            previous_query.iter()
287        {
288            if constraint == previous_constraint && value == previous_value {
289                // There is no change in constraint. We can exit out early.
290                return;
291            } else if constraint == previous_constraint {
292                commands.entity(previous_entity).remove::<Checked>();
293                commands
294                    .entity(previous_entity)
295                    .insert(BorderColor::all(INACTIVE_BORDER_COLOR));
296                // radio button entities only have one child which contains the inner background color.
297                commands
298                    .entity(*previous_children.first().unwrap())
299                    .insert(BackgroundColor(INACTIVE_INNER_COLOR));
300                for text_entity in child_q.iter_leaves(previous_entity) {
301                    commands
302                        .entity(text_entity)
303                        .insert(TextColor(UNHOVERED_TEXT_COLOR));
304                }
305
306                commands.entity(entity).insert(Checked);
307                commands
308                    .entity(entity)
309                    .insert(BorderColor::all(ACTIVE_BORDER_COLOR));
310                commands
311                    .entity(*children.first().unwrap())
312                    .insert(BackgroundColor(ACTIVE_INNER_COLOR));
313                for text_entity in child_q.iter_leaves(entity) {
314                    commands
315                        .entity(text_entity)
316                        .insert(TextColor(ACTIVE_TEXT_COLOR));
317                }
318            }
319        }
320
321        match constraint {
322            Constraint::FlexBasis => {
323                bar_node.flex_basis = value.0;
324            }
325            Constraint::Width => {
326                bar_node.width = value.0;
327            }
328            Constraint::MinWidth => {
329                bar_node.min_width = value.0;
330            }
331            Constraint::MaxWidth => {
332                bar_node.max_width = value.0;
333            }
334        }
335    }
336}