Skip to main content

ui_texture_atlas_slice/
ui_texture_atlas_slice.rs

1//! This example illustrates how to create buttons with their texture atlases sliced
2//! and kept in proportion instead of being stretched by the button dimensions
3
4use bevy::{
5    color::palettes::css::{GOLD, ORANGE},
6    picking::hover::Hovered,
7    platform::collections::HashSet,
8    prelude::*,
9    ui::{widget::NodeImageMode, Pressed},
10    ui_widgets::Button,
11};
12
13fn main() {
14    App::new()
15        .add_plugins(DefaultPlugins)
16        .add_systems(Startup, setup)
17        .add_systems(Update, button_system)
18        .run();
19}
20
21/// Updates each button label whenever its `Pressed` or `Hovered` state changes.
22/// The image node, defined from a spritesheet, will also update upon pressing the button.
23///
24/// `Hovered` always exists on the button, so `Ref::is_changed` catches hover transitions and
25/// the insertion of `Pressed`. Removal of `Pressed` is not reported by change detection, so we
26/// also restyle any button that just had `Pressed` removed this frame.
27fn button_system(
28    mut buttons: Query<
29        (
30            Entity,
31            Option<Ref<Pressed>>,
32            Ref<Hovered>,
33            &mut ImageNode,
34            &Children,
35        ),
36        With<Button>,
37    >,
38    mut removed_pressed: RemovedComponents<Pressed>,
39    mut text_query: Query<&mut Text>,
40) {
41    // Buttons that had `Pressed` removed this frame; change detection does not report removals.
42    let just_unpressed: HashSet<Entity> = removed_pressed.read().collect();
43    for (entity, pressed, hovered, mut image, children) in &mut buttons {
44        let changed = hovered.is_changed()
45            || pressed.as_ref().is_some_and(Ref::is_changed)
46            || just_unpressed.contains(&entity);
47        if changed {
48            set_button_style(
49                pressed.is_some(),
50                hovered.get(),
51                &mut image,
52                children,
53                &mut text_query,
54            );
55        }
56    }
57}
58
59/// Shared styling logic: pressed takes precedence over hover.
60fn set_button_style(
61    pressed: bool,
62    hovered: bool,
63    image: &mut ImageNode,
64    children: &Children,
65    text_query: &mut Query<&mut Text>,
66) {
67    let mut text = text_query.get_mut(children[0]).unwrap();
68    match (pressed, hovered) {
69        (true, _) => {
70            **text = "Press".to_string();
71            if let Some(atlas) = &mut image.texture_atlas {
72                atlas.index = (atlas.index + 1) % 30;
73            }
74            image.color = GOLD.into();
75        }
76        (false, true) => {
77            **text = "Hover".to_string();
78            image.color = ORANGE.into();
79        }
80        (false, false) => {
81            **text = "Button".to_string();
82            image.color = Color::WHITE;
83        }
84    }
85}
86
87fn setup(
88    mut commands: Commands,
89    asset_server: Res<AssetServer>,
90    mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
91) {
92    let texture_handle = asset_server.load("textures/fantasy_ui_borders/border_sheet.png");
93    let atlas_layout =
94        TextureAtlasLayout::from_grid(UVec2::new(50, 50), 6, 6, Some(UVec2::splat(2)), None);
95    let atlas_layout_handle = texture_atlases.add(atlas_layout);
96
97    let slicer = TextureSlicer {
98        border: BorderRect::all(24.0),
99        center_scale_mode: SliceScaleMode::Stretch,
100        sides_scale_mode: SliceScaleMode::Stretch,
101        max_corner_scale: 1.0,
102    };
103    // ui camera
104    commands.spawn(Camera2d);
105    commands
106        .spawn(Node {
107            width: percent(100),
108            height: percent(100),
109            align_items: AlignItems::Center,
110            justify_content: JustifyContent::Center,
111            ..default()
112        })
113        .with_children(|parent| {
114            for (idx, [w, h]) in [
115                (0, [150.0, 150.0]),
116                (7, [300.0, 150.0]),
117                (13, [150.0, 300.0]),
118            ] {
119                parent
120                    .spawn((
121                        Button,
122                        Hovered::default(),
123                        ImageNode::from_atlas_image(
124                            texture_handle.clone(),
125                            TextureAtlas {
126                                index: idx,
127                                layout: atlas_layout_handle.clone(),
128                            },
129                        )
130                        .with_mode(NodeImageMode::Sliced(slicer.clone())),
131                        Node {
132                            width: px(w),
133                            height: px(h),
134                            // horizontally center child text
135                            justify_content: JustifyContent::Center,
136                            // vertically center child text
137                            align_items: AlignItems::Center,
138                            margin: px(20).all(),
139                            ..default()
140                        },
141                    ))
142                    .with_children(|parent| {
143                        parent.spawn((
144                            Text::new("Button"),
145                            TextFont {
146                                font: asset_server.load("fonts/FiraSans-Bold.ttf").into(),
147                                font_size: FontSize::Px(33.0),
148                                ..default()
149                            },
150                            TextColor(Color::srgb(0.9, 0.9, 0.9)),
151                        ));
152                    });
153            }
154        });
155}