Skip to main content

ui_texture_slice/
ui_texture_slice.rs

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