docs/bevy/examples/ui/images/ui_texture_slice.rs 4.7 K · 140 lines · raw · history

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
8 use 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
17 fn 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.
31 fn 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.
64 fn 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
88 fn 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 }