UI (User Interface) / Image Node Resizing

Back to examples View in GitHub

This example demonstrates the behavior of NodeImageMode::Auto and NodeImageMode::Stretch by allowing keyboard input to resize an ImageGroup container. It visually shows how images are sized automatically versus stretched to fit their container.

use bevy::{color::palettes::tailwind, prelude::*};

const MIN_RESIZE_VAL: f32 = 1.0;
const IMAGE_GROUP_BOX_MIN_WIDTH: f32 = 50.0;
const IMAGE_GROUP_BOX_MAX_WIDTH: f32 = 100.0;
const IMAGE_GROUP_BOX_MIN_HEIGHT: f32 = 10.0;
const IMAGE_GROUP_BOX_MAX_HEIGHT: f32 = 50.0;
const IMAGE_GROUP_BOX_INIT_WIDTH: f32 =
    (IMAGE_GROUP_BOX_MIN_WIDTH + IMAGE_GROUP_BOX_MAX_WIDTH) / 2.;
const IMAGE_GROUP_BOX_INIT_HEIGHT: f32 =
    (IMAGE_GROUP_BOX_MIN_HEIGHT + IMAGE_GROUP_BOX_MAX_HEIGHT) / 2.;
const TEXT_PREFIX: &str = "Compare NodeImageMode(Auto, Stretch) press `Up`/`Down` to resize height, press `Left`/`Right` to resize width\n";

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Enable for image outline
        .insert_resource(GlobalUiDebugOptions {
            enabled: true,
            ..default()
        })
        .add_systems(Startup, setup)
        .add_systems(Update, update)
        .add_observer(on_trigger_image_group)
        .run();
}

#[derive(Debug, Component)]
struct ImageGroup;

#[derive(Debug, Event)]
enum ImageGroupResize {
    HeightGrow,
    HeightShrink,
    WidthGrow,
    WidthShrink,
}

// Text data for easy modification
#[derive(Debug, Component)]
struct TextData {
    height: f32,
    width: f32,
}

#[derive(Debug)]
enum Direction {
    Height,
    Width,
}

#[derive(Debug, EntityEvent)]
struct TextUpdate {
    entity: Entity,
    direction: Direction,
    change: f32,
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let image_handle = asset_server.load("branding/icon.png");
    let full_text = format!(
        "{}height : {}%, width : {}%",
        TEXT_PREFIX, IMAGE_GROUP_BOX_INIT_HEIGHT, IMAGE_GROUP_BOX_INIT_WIDTH,
    );

    commands.spawn(Camera2d);

    let container = commands
        .spawn((
            Node {
                display: Display::Grid,
                width: percent(100),
                height: percent(100),
                grid_template_rows: vec![GridTrack::min_content(), GridTrack::flex(1.0)],
                ..default()
            },
            BackgroundColor(Color::WHITE),
        ))
        .id();

    // Keyboard Text
    commands
        .spawn((
            TextData {
                height: IMAGE_GROUP_BOX_INIT_HEIGHT,
                width: IMAGE_GROUP_BOX_INIT_WIDTH,
            },
            Text::new(full_text),
            TextColor::BLACK,
            Node {
                grid_row: GridPlacement::span(1),
                padding: px(6).all(),
                ..default()
            },
            UiDebugOptions {
                enabled: false,
                ..default()
            },
            ChildOf(container),
        ))
        .observe(update_text);

    commands
        .spawn((
            Node {
                display: Display::Flex,
                grid_row: GridPlacement::span(1),
                flex_direction: FlexDirection::Column,
                justify_content: JustifyContent::SpaceAround,
                padding: px(10.).all(),
                ..default()
            },
            BackgroundColor(Color::BLACK),
            ChildOf(container),
        ))
        .with_children(|builder| {
            // `NodeImageMode::Auto` will resize the image automatically by taking the size of the source image and applying any layout constraints.
            builder
                .spawn((
                    ImageGroup,
                    Node {
                        display: Display::Flex,
                        justify_content: JustifyContent::Start,
                        width: percent(IMAGE_GROUP_BOX_INIT_WIDTH),
                        height: percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
                        ..default()
                    },
                    BackgroundColor(Color::from(tailwind::BLUE_100)),
                ))
                .with_children(|parent| {
                    for _ in 0..4 {
                        // child node will apply Flex layout
                        parent.spawn((
                            Node::default(),
                            ImageNode {
                                image: image_handle.clone(),
                                image_mode: NodeImageMode::Auto,
                                ..default()
                            },
                        ));
                    }
                });
            // `NodeImageMode::Stretch` will resize the image to match the size of the `Node` component
            builder
                .spawn((
                    ImageGroup,
                    Node {
                        display: Display::Flex,
                        justify_content: JustifyContent::Start,
                        width: percent(IMAGE_GROUP_BOX_INIT_WIDTH),
                        height: percent(IMAGE_GROUP_BOX_INIT_HEIGHT),
                        ..default()
                    },
                    BackgroundColor(Color::from(tailwind::BLUE_100)),
                ))
                .with_children(|parent| {
                    for width in [10., 20., 30., 40.] {
                        parent.spawn((
                            Node {
                                height: percent(100),
                                width: percent(width),
                                ..default()
                            },
                            ImageNode {
                                image: image_handle.clone(),
                                image_mode: NodeImageMode::Stretch,
                                ..default()
                            },
                        ));
                    }
                });
        });
}

// Trigger event
fn update(
    keycode: Res<ButtonInput<KeyCode>>,
    mut commands: Commands,
    query: Query<Entity, With<TextData>>,
) {
    let entity = query.single().unwrap();
    if keycode.pressed(KeyCode::ArrowUp) {
        commands.trigger(ImageGroupResize::HeightGrow);
        commands.trigger(TextUpdate {
            entity,
            direction: Direction::Height,
            change: MIN_RESIZE_VAL,
        });
    }
    if keycode.pressed(KeyCode::ArrowDown) {
        commands.trigger(ImageGroupResize::HeightShrink);
        commands.trigger(TextUpdate {
            entity,
            direction: Direction::Height,
            change: -MIN_RESIZE_VAL,
        });
    }
    if keycode.pressed(KeyCode::ArrowLeft) {
        commands.trigger(ImageGroupResize::WidthShrink);
        commands.trigger(TextUpdate {
            entity,
            direction: Direction::Width,
            change: -MIN_RESIZE_VAL,
        });
    }
    if keycode.pressed(KeyCode::ArrowRight) {
        commands.trigger(ImageGroupResize::WidthGrow);
        commands.trigger(TextUpdate {
            entity,
            direction: Direction::Width,
            change: MIN_RESIZE_VAL,
        });
    }
}

fn update_text(
    event: On<TextUpdate>,
    mut textmeta: Single<&mut TextData>,
    mut text: Single<&mut Text>,
) {
    let mut new_text = Text::new(TEXT_PREFIX);
    match event.direction {
        Direction::Height => {
            textmeta.height = (textmeta.height + event.change)
                .clamp(IMAGE_GROUP_BOX_MIN_HEIGHT, IMAGE_GROUP_BOX_MAX_HEIGHT);
            new_text.push_str(&format!(
                "height : {}%, width : {}%",
                textmeta.height, textmeta.width
            ));
        }
        Direction::Width => {
            textmeta.width = (textmeta.width + event.change)
                .clamp(IMAGE_GROUP_BOX_MIN_WIDTH, IMAGE_GROUP_BOX_MAX_WIDTH);
            new_text.push_str(&format!(
                "height : {}%, width : {}%",
                textmeta.height, textmeta.width
            ));
        }
    }
    text.0 = new_text.0;
}

fn on_trigger_image_group(event: On<ImageGroupResize>, query: Query<&mut Node, With<ImageGroup>>) {
    for mut node in query {
        match event.event() {
            ImageGroupResize::HeightGrow => {
                if let Val::Percent(val) = &mut node.height {
                    *val = (*val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_HEIGHT);
                }
            }
            ImageGroupResize::HeightShrink => {
                if let Val::Percent(val) = &mut node.height {
                    *val = (*val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_HEIGHT);
                }
            }
            ImageGroupResize::WidthGrow => {
                if let Val::Percent(val) = &mut node.width {
                    *val = (*val + MIN_RESIZE_VAL).min(IMAGE_GROUP_BOX_MAX_WIDTH);
                }
            }
            ImageGroupResize::WidthShrink => {
                if let Val::Percent(val) = &mut node.width {
                    *val = (*val - MIN_RESIZE_VAL).max(IMAGE_GROUP_BOX_MIN_WIDTH);
                }
            }
        }
    }
}