Skip to main content

image_node_resizing/
image_node_resizing.rs

1//! This example demonstrates the behavior of `NodeImageMode::Auto` and `NodeImageMode::Stretch` by allowing keyboard input to resize an `ImageGroup` container.
2//! It visually shows how images are sized automatically versus stretched to fit their container.
3
4use bevy::{color::palettes::tailwind, prelude::*};
5
6const MIN_RESIZE_VAL: f32 = 1.0;
7const IMAGE_GROUP_BOX_MIN_WIDTH: f32 = 50.0;
8const IMAGE_GROUP_BOX_MAX_WIDTH: f32 = 100.0;
9const IMAGE_GROUP_BOX_MIN_HEIGHT: f32 = 10.0;
10const IMAGE_GROUP_BOX_MAX_HEIGHT: f32 = 50.0;
11const IMAGE_GROUP_BOX_INIT_WIDTH: f32 =
12    (IMAGE_GROUP_BOX_MIN_WIDTH + IMAGE_GROUP_BOX_MAX_WIDTH) / 2.;
13const IMAGE_GROUP_BOX_INIT_HEIGHT: f32 =
14    (IMAGE_GROUP_BOX_MIN_HEIGHT + IMAGE_GROUP_BOX_MAX_HEIGHT) / 2.;
15const TEXT_PREFIX: &str = "Compare NodeImageMode(Auto, Stretch) press `Up`/`Down` to resize height, press `Left`/`Right` to resize width\n";
16
17fn main() {
18    App::new()
19        .add_plugins(DefaultPlugins)
20        // Enable for image outline
21        .insert_resource(GlobalUiDebugOptions {
22            enabled: true,
23            ..default()
24        })
25        .add_systems(Startup, setup)
26        .add_systems(Update, update)
27        .add_observer(on_trigger_image_group)
28        .run();
29}
30
31#[derive(Debug, Component)]
32struct ImageGroup;
33
34#[derive(Debug, Event)]
35enum ImageGroupResize {
36    HeightGrow,
37    HeightShrink,
38    WidthGrow,
39    WidthShrink,
40}
41
42// Text data for easy modification
43#[derive(Debug, Component)]
44struct TextData {
45    height: f32,
46    width: f32,
47}
48
49#[derive(Debug)]
50enum Direction {
51    Height,
52    Width,
53}
54
55#[derive(Debug, EntityEvent)]
56struct TextUpdate {
57    entity: Entity,
58    direction: Direction,
59    change: f32,
60}
61
62fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
63    let image_handle = asset_server.load("branding/icon.png");
64    let full_text = format!(
65        "{}height : {}%, width : {}%",
66        TEXT_PREFIX, IMAGE_GROUP_BOX_INIT_HEIGHT, IMAGE_GROUP_BOX_INIT_WIDTH,
67    );
68
69    commands.spawn(Camera2d);
70
71    let container = commands
72        .spawn((
73            Node {
74                display: Display::Grid,
75                width: percent(100),
76                height: percent(100),
77                grid_template_rows: vec![GridTrack::min_content(), GridTrack::flex(1.0)],
78                ..default()
79            },
80            BackgroundColor(Color::WHITE),
81        ))
82        .id();
83
84    // Keyboard Text
85    commands
86        .spawn((
87            TextData {
88                height: IMAGE_GROUP_BOX_INIT_HEIGHT,
89                width: IMAGE_GROUP_BOX_INIT_WIDTH,
90            },
91            Text::new(full_text),
92            TextColor::BLACK,
93            Node {
94                grid_row: GridPlacement::span(1),
95                padding: px(6).all(),
96                ..default()
97            },
98            UiDebugOptions {
99                enabled: false,
100                ..default()
101            },
102            ChildOf(container),
103        ))
104        .observe(update_text);
105
106    commands
107        .spawn((
108            Node {
109                display: Display::Flex,
110                grid_row: GridPlacement::span(1),
111                flex_direction: FlexDirection::Column,
112                justify_content: JustifyContent::SpaceAround,
113                padding: px(10.).all(),
114                ..default()
115            },
116            BackgroundColor(Color::BLACK),
117            ChildOf(container),
118        ))
119        .with_children(|builder| {
120            // `NodeImageMode::Auto` will resize the image automatically by taking the size of the source image and applying any layout constraints.
121            builder
122                .spawn((
123                    ImageGroup,
124                    Node {
125                        display: Display::Flex,
126                        justify_content: JustifyContent::Start,
127                        width: percent(IMAGE_GROUP_BOX_INIT_WIDTH),
128                        height: percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
129                        ..default()
130                    },
131                    BackgroundColor(Color::from(tailwind::BLUE_100)),
132                ))
133                .with_children(|parent| {
134                    for _ in 0..4 {
135                        // child node will apply Flex layout
136                        parent.spawn((
137                            Node::default(),
138                            ImageNode {
139                                image: image_handle.clone(),
140                                image_mode: NodeImageMode::Auto,
141                                ..default()
142                            },
143                        ));
144                    }
145                });
146            // `NodeImageMode::Stretch` will resize the image to match the size of the `Node` component
147            builder
148                .spawn((
149                    ImageGroup,
150                    Node {
151                        display: Display::Flex,
152                        justify_content: JustifyContent::Start,
153                        width: percent(IMAGE_GROUP_BOX_INIT_WIDTH),
154                        height: percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
155                        ..default()
156                    },
157                    BackgroundColor(Color::from(tailwind::BLUE_100)),
158                ))
159                .with_children(|parent| {
160                    for width in [10., 20., 30., 40.] {
161                        parent.spawn((
162                            Node {
163                                height: percent(100),
164                                width: percent(width),
165                                ..default()
166                            },
167                            ImageNode {
168                                image: image_handle.clone(),
169                                image_mode: NodeImageMode::Stretch,
170                                ..default()
171                            },
172                        ));
173                    }
174                });
175        });
176}
177
178// Trigger event
179fn update(
180    keycode: Res<ButtonInput<KeyCode>>,
181    mut commands: Commands,
182    query: Query<Entity, With<TextData>>,
183) {
184    let entity = query.single().unwrap();
185    if keycode.pressed(KeyCode::ArrowUp) {
186        commands.trigger(ImageGroupResize::HeightGrow);
187        commands.trigger(TextUpdate {
188            entity,
189            direction: Direction::Height,
190            change: MIN_RESIZE_VAL,
191        });
192    }
193    if keycode.pressed(KeyCode::ArrowDown) {
194        commands.trigger(ImageGroupResize::HeightShrink);
195        commands.trigger(TextUpdate {
196            entity,
197            direction: Direction::Height,
198            change: -MIN_RESIZE_VAL,
199        });
200    }
201    if keycode.pressed(KeyCode::ArrowLeft) {
202        commands.trigger(ImageGroupResize::WidthShrink);
203        commands.trigger(TextUpdate {
204            entity,
205            direction: Direction::Width,
206            change: -MIN_RESIZE_VAL,
207        });
208    }
209    if keycode.pressed(KeyCode::ArrowRight) {
210        commands.trigger(ImageGroupResize::WidthGrow);
211        commands.trigger(TextUpdate {
212            entity,
213            direction: Direction::Width,
214            change: MIN_RESIZE_VAL,
215        });
216    }
217}
218
219fn update_text(
220    event: On<TextUpdate>,
221    mut textmeta: Single<&mut TextData>,
222    mut text: Single<&mut Text>,
223) {
224    let mut new_text = Text::new(TEXT_PREFIX);
225    match event.direction {
226        Direction::Height => {
227            textmeta.height = (textmeta.height + event.change)
228                .clamp(IMAGE_GROUP_BOX_MIN_HEIGHT, IMAGE_GROUP_BOX_MAX_HEIGHT);
229            new_text.push_str(&format!(
230                "height : {}%, width : {}%",
231                textmeta.height, textmeta.width
232            ));
233        }
234        Direction::Width => {
235            textmeta.width = (textmeta.width + event.change)
236                .clamp(IMAGE_GROUP_BOX_MIN_WIDTH, IMAGE_GROUP_BOX_MAX_WIDTH);
237            new_text.push_str(&format!(
238                "height : {}%, width : {}%",
239                textmeta.height, textmeta.width
240            ));
241        }
242    }
243    text.0 = new_text.0;
244}
245
246fn on_trigger_image_group(event: On<ImageGroupResize>, query: Query<&mut Node, With<ImageGroup>>) {
247    for mut node in query {
248        match event.event() {
249            ImageGroupResize::HeightGrow => {
250                if let Val::Percent(val) = &mut node.height {
251                    *val = (*val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_HEIGHT);
252                }
253            }
254            ImageGroupResize::HeightShrink => {
255                if let Val::Percent(val) = &mut node.height {
256                    *val = (*val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_HEIGHT);
257                }
258            }
259            ImageGroupResize::WidthGrow => {
260                if let Val::Percent(val) = &mut node.width {
261                    *val = (*val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_WIDTH);
262                }
263            }
264            ImageGroupResize::WidthShrink => {
265                if let Val::Percent(val) = &mut node.width {
266                    *val = (*val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_WIDTH);
267                }
268            }
269        }
270    }
271}