Skip to content

Commit e00869b

Browse files
Fodesualice-i-cecilekfc35ickshonpe
authored
ImageNode resizing example (#22606)
# Objective #22550 ## Solution It demonstrates the behavior of `NodeImageMode::Auto` and `NodeImageMode::Stretch` by resizing a group of images using keyboard input --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com> Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
1 parent c188140 commit e00869b

File tree

3 files changed

+292
-0
lines changed

3 files changed

+292
-0
lines changed

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4196,6 +4196,18 @@ description = "Demonstrates how to create an image node"
41964196
category = "UI (User Interface)"
41974197
wasm = true
41984198

4199+
[[example]]
4200+
name = "image_node_resizing"
4201+
path = "examples/ui/image_node_resizing.rs"
4202+
# Causes an ICE on docs.rs
4203+
doc-scrape-examples = false
4204+
4205+
[package.metadata.example.image_node_resizing]
4206+
name = "Image Node Resizing"
4207+
description = "Demonstrates how to resize an image node"
4208+
category = "UI (User Interface)"
4209+
wasm = true
4210+
41994211
# Window
42004212
[[example]]
42014213
name = "clear_color"

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ Example | Description
590590
[Ghost Nodes](../examples/ui/ghost_nodes.rs) | Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy
591591
[Gradients](../examples/ui/gradients.rs) | An example demonstrating gradients
592592
[Image Node](../examples/ui/image_node.rs) | Demonstrates how to create an image node
593+
[Image Node Resizing](../examples/ui/image_node_resizing.rs) | Demonstrates how to resize an image node
593594
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
594595
[Overflow Clip Margin](../examples/ui/overflow_clip_margin.rs) | Simple example demonstrating the OverflowClipMargin style property
595596
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior

examples/ui/image_node_resizing.rs

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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+
4+
use bevy::{color::palettes::tailwind, prelude::*};
5+
6+
const MIN_RESIZE_VAL: f32 = 1.0;
7+
const IMAGE_GROUP_BOX_MIN_WIDTH: f32 = 50.0;
8+
const IMAGE_GROUP_BOX_MAX_WIDTH: f32 = 100.0;
9+
const IMAGE_GROUP_BOX_MIN_HEIGHT: f32 = 10.0;
10+
const IMAGE_GROUP_BOX_MAX_HEIGHT: f32 = 50.0;
11+
const IMAGE_GROUP_BOX_INIT_WIDTH: f32 =
12+
(IMAGE_GROUP_BOX_MIN_WIDTH + IMAGE_GROUP_BOX_MAX_WIDTH) / 2.;
13+
const IMAGE_GROUP_BOX_INIT_HEIGHT: f32 =
14+
(IMAGE_GROUP_BOX_MIN_HEIGHT + IMAGE_GROUP_BOX_MAX_HEIGHT) / 2.;
15+
const TEXT_PREFIX: &str = "Compare NodeImageMode(Auto, Stretch) press `Up`/`Down` to resize height, press `Left`/`Right` to resize width\n";
16+
17+
fn main() {
18+
App::new()
19+
.add_plugins(DefaultPlugins)
20+
// Enable for image outline
21+
.insert_resource(UiDebugOptions {
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)]
32+
struct ImageGroup;
33+
34+
#[derive(Debug, Event)]
35+
enum ImageGroupResize {
36+
HeightGrow,
37+
HeightShrink,
38+
WidthGrow,
39+
WidthShrink,
40+
}
41+
42+
// Text data for easy modification
43+
#[derive(Debug, Component)]
44+
struct TextData {
45+
height: f32,
46+
width: f32,
47+
}
48+
49+
#[derive(Debug)]
50+
enum Direction {
51+
Height,
52+
Width,
53+
}
54+
55+
#[derive(Debug, EntityEvent)]
56+
struct TextUpdate {
57+
entity: Entity,
58+
direction: Direction,
59+
change: f32,
60+
}
61+
62+
fn 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: UiRect::all(px(6)),
96+
..default()
97+
},
98+
UiDebugOptions {
99+
enabled: false,
100+
..default()
101+
},
102+
UiDebugOptions {
103+
enabled: false,
104+
..default()
105+
},
106+
ChildOf(container),
107+
))
108+
.observe(update_text);
109+
110+
commands
111+
.spawn((
112+
Node {
113+
display: Display::Flex,
114+
grid_row: GridPlacement::span(1),
115+
flex_direction: FlexDirection::Column,
116+
justify_content: JustifyContent::SpaceAround,
117+
padding: UiRect::all(Val::Px(10.)),
118+
..default()
119+
},
120+
BackgroundColor(Color::BLACK),
121+
ChildOf(container),
122+
))
123+
.with_children(|builder| {
124+
// `NodeImageMode::Auto` will resize the image automatically by taking the size of the source image and applying any layout constraints.
125+
builder
126+
.spawn((
127+
ImageGroup,
128+
Node {
129+
display: Display::Flex,
130+
justify_content: JustifyContent::Start,
131+
width: Val::Percent(IMAGE_GROUP_BOX_INIT_WIDTH),
132+
height: Val::Percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
133+
..default()
134+
},
135+
BackgroundColor(Color::from(tailwind::BLUE_100)),
136+
))
137+
.with_children(|parent| {
138+
for _ in 0..4 {
139+
// child node will apply Flex layout
140+
parent.spawn((
141+
Node::default(),
142+
ImageNode {
143+
image: image_handle.clone(),
144+
image_mode: NodeImageMode::Auto,
145+
..default()
146+
},
147+
));
148+
}
149+
});
150+
// `NodeImageMode::Stretch` will resize the image to match the size of the `Node` component
151+
builder
152+
.spawn((
153+
ImageGroup,
154+
Node {
155+
display: Display::Flex,
156+
justify_content: JustifyContent::Start,
157+
width: Val::Percent(IMAGE_GROUP_BOX_INIT_WIDTH),
158+
height: Val::Percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
159+
..default()
160+
},
161+
BackgroundColor(Color::from(tailwind::BLUE_100)),
162+
))
163+
.with_children(|parent| {
164+
for width in [10., 20., 30., 40.] {
165+
parent.spawn((
166+
Node {
167+
height: Val::Percent(100.),
168+
width: Val::Percent(width),
169+
..default()
170+
},
171+
ImageNode {
172+
image: image_handle.clone(),
173+
image_mode: NodeImageMode::Stretch,
174+
..default()
175+
},
176+
));
177+
}
178+
});
179+
});
180+
}
181+
182+
// Trigger event
183+
fn update(
184+
keycode: Res<ButtonInput<KeyCode>>,
185+
mut commands: Commands,
186+
query: Query<Entity, With<TextData>>,
187+
) {
188+
let entity = query.single().unwrap();
189+
if keycode.pressed(KeyCode::ArrowUp) {
190+
commands.trigger(ImageGroupResize::HeightGrow);
191+
commands.trigger(TextUpdate {
192+
entity,
193+
direction: Direction::Height,
194+
change: MIN_RESIZE_VAL,
195+
});
196+
}
197+
if keycode.pressed(KeyCode::ArrowDown) {
198+
commands.trigger(ImageGroupResize::HeightShrink);
199+
commands.trigger(TextUpdate {
200+
entity,
201+
direction: Direction::Height,
202+
change: -MIN_RESIZE_VAL,
203+
});
204+
}
205+
if keycode.pressed(KeyCode::ArrowLeft) {
206+
commands.trigger(ImageGroupResize::WidthShrink);
207+
commands.trigger(TextUpdate {
208+
entity,
209+
direction: Direction::Width,
210+
change: -MIN_RESIZE_VAL,
211+
});
212+
}
213+
if keycode.pressed(KeyCode::ArrowRight) {
214+
commands.trigger(ImageGroupResize::WidthGrow);
215+
commands.trigger(TextUpdate {
216+
entity,
217+
direction: Direction::Width,
218+
change: MIN_RESIZE_VAL,
219+
});
220+
}
221+
}
222+
223+
fn update_text(
224+
event: On<TextUpdate>,
225+
mut textmeta: Single<&mut TextData>,
226+
mut text: Single<&mut Text>,
227+
) {
228+
let mut new_text = Text::new(TEXT_PREFIX);
229+
match event.direction {
230+
Direction::Height => {
231+
textmeta.height = (textmeta.height + event.change)
232+
.clamp(IMAGE_GROUP_BOX_MIN_HEIGHT, IMAGE_GROUP_BOX_MAX_HEIGHT);
233+
new_text.push_str(&format!(
234+
"height : {}%, width : {}%",
235+
textmeta.height, textmeta.width
236+
));
237+
}
238+
Direction::Width => {
239+
textmeta.width = (textmeta.width + event.change)
240+
.clamp(IMAGE_GROUP_BOX_MIN_WIDTH, IMAGE_GROUP_BOX_MAX_WIDTH);
241+
new_text.push_str(&format!(
242+
"height : {}%, width : {}%",
243+
textmeta.height, textmeta.width
244+
));
245+
}
246+
}
247+
text.0 = new_text.0;
248+
}
249+
250+
fn on_trigger_image_group(event: On<ImageGroupResize>, query: Query<&mut Node, With<ImageGroup>>) {
251+
for mut node in query {
252+
match event.event() {
253+
ImageGroupResize::HeightGrow => {
254+
if let Val::Percent(val) = node.height {
255+
let new_val = (val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_HEIGHT);
256+
node.height = Val::Percent(new_val);
257+
}
258+
}
259+
ImageGroupResize::HeightShrink => {
260+
if let Val::Percent(val) = node.height {
261+
let new_val = (val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_HEIGHT);
262+
node.height = Val::Percent(new_val);
263+
}
264+
}
265+
ImageGroupResize::WidthGrow => {
266+
if let Val::Percent(val) = node.width {
267+
let new_val = (val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_WIDTH);
268+
node.width = Val::Percent(new_val);
269+
}
270+
}
271+
ImageGroupResize::WidthShrink => {
272+
if let Val::Percent(val) = node.width {
273+
let new_val = (val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_WIDTH);
274+
node.width = Val::Percent(new_val);
275+
}
276+
}
277+
}
278+
}
279+
}

0 commit comments

Comments
 (0)