docs/bevy/examples/window/custom_cursor_image.rs 7 K · 227 lines · raw · history

1 //! Illustrates how to use a custom cursor image with a texture atlas and
2 //! animation.
3
4 use std::time::Duration;
5
6 use bevy::{
7 prelude::*,
8 window::{CursorIcon, CustomCursor, CustomCursorImage},
9 };
10
11 fn main() {
12 App::new()
13 .add_plugins(DefaultPlugins)
14 .add_systems(
15 Startup,
16 (setup_cursor_icon, setup_camera, setup_instructions),
17 )
18 .add_systems(
19 Update,
20 (
21 execute_animation,
22 toggle_texture_atlas,
23 toggle_flip_x,
24 toggle_flip_y,
25 cycle_rect,
26 ),
27 )
28 .run();
29 }
30
31 fn setup_cursor_icon(
32 mut commands: Commands,
33 asset_server: Res<AssetServer>,
34 mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
35 window: Single<Entity, With<Window>>,
36 ) {
37 let layout =
38 TextureAtlasLayout::from_grid(UVec2::splat(64), 20, 10, Some(UVec2::splat(5)), None);
39 let texture_atlas_layout = texture_atlas_layouts.add(layout);
40
41 let animation_config = AnimationConfig::new(0, 199, 1, 4);
42
43 commands.entity(*window).insert((
44 CursorIcon::Custom(CustomCursor::Image(CustomCursorImage {
45 // Image to use as the cursor.
46 handle: asset_server
47 .load("cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png"),
48 // Optional texture atlas allows you to pick a section of the image
49 // and animate it.
50 texture_atlas: Some(TextureAtlas {
51 layout: texture_atlas_layout.clone(),
52 index: animation_config.first_sprite_index,
53 }),
54 flip_x: false,
55 flip_y: false,
56 // Optional section of the image to use as the cursor.
57 rect: None,
58 // The hotspot is the point in the cursor image that will be
59 // positioned at the mouse cursor's position.
60 hotspot: (0, 0),
61 })),
62 animation_config,
63 ));
64 }
65
66 fn setup_camera(mut commands: Commands) {
67 commands.spawn(Camera3d::default());
68 }
69
70 fn setup_instructions(mut commands: Commands) {
71 commands.spawn((
72 Text::new(
73 "Press T to toggle the cursor's `texture_atlas`.\n
74 Press X to toggle the cursor's `flip_x` setting.\n
75 Press Y to toggle the cursor's `flip_y` setting.\n
76 Press C to cycle through the sections of the cursor's image using `rect`.",
77 ),
78 Node {
79 position_type: PositionType::Absolute,
80 bottom: px(12),
81 left: px(12),
82 ..default()
83 },
84 ));
85 }
86
87 #[derive(Component)]
88 struct AnimationConfig {
89 first_sprite_index: usize,
90 last_sprite_index: usize,
91 increment: usize,
92 fps: u8,
93 frame_timer: Timer,
94 }
95
96 impl AnimationConfig {
97 fn new(first: usize, last: usize, increment: usize, fps: u8) -> Self {
98 Self {
99 first_sprite_index: first,
100 last_sprite_index: last,
101 increment,
102 fps,
103 frame_timer: Self::timer_from_fps(fps),
104 }
105 }
106
107 fn timer_from_fps(fps: u8) -> Timer {
108 Timer::new(Duration::from_secs_f32(1.0 / (fps as f32)), TimerMode::Once)
109 }
110 }
111
112 /// This system loops through all the sprites in the [`CursorIcon`]'s
113 /// [`TextureAtlas`], from [`AnimationConfig`]'s `first_sprite_index` to
114 /// `last_sprite_index`.
115 fn execute_animation(time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut CursorIcon)>) {
116 for (mut config, mut cursor_icon) in &mut query {
117 if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
118 config.frame_timer.tick(time.delta());
119
120 if config.frame_timer.is_finished()
121 && let Some(atlas) = image.texture_atlas.as_mut()
122 {
123 atlas.index += config.increment;
124
125 if atlas.index > config.last_sprite_index {
126 atlas.index = config.first_sprite_index;
127 }
128
129 config.frame_timer = AnimationConfig::timer_from_fps(config.fps);
130 }
131 }
132 }
133 }
134
135 fn toggle_texture_atlas(
136 input: Res<ButtonInput<KeyCode>>,
137 mut query: Query<&mut CursorIcon, With<Window>>,
138 mut cached_atlas: Local<Option<TextureAtlas>>, // this lets us restore the previous value
139 ) {
140 if input.just_pressed(KeyCode::KeyT) {
141 for mut cursor_icon in &mut query {
142 if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
143 match image.texture_atlas.take() {
144 Some(a) => {
145 // Save the current texture atlas.
146 *cached_atlas = Some(a.clone());
147 }
148 None => {
149 // Restore the cached texture atlas.
150 if let Some(cached_a) = cached_atlas.take() {
151 image.texture_atlas = Some(cached_a);
152 }
153 }
154 }
155 }
156 }
157 }
158 }
159
160 fn toggle_flip_x(
161 input: Res<ButtonInput<KeyCode>>,
162 mut query: Query<&mut CursorIcon, With<Window>>,
163 ) {
164 if input.just_pressed(KeyCode::KeyX) {
165 for mut cursor_icon in &mut query {
166 if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
167 image.flip_x = !image.flip_x;
168 }
169 }
170 }
171 }
172
173 fn toggle_flip_y(
174 input: Res<ButtonInput<KeyCode>>,
175 mut query: Query<&mut CursorIcon, With<Window>>,
176 ) {
177 if input.just_pressed(KeyCode::KeyY) {
178 for mut cursor_icon in &mut query {
179 if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
180 image.flip_y = !image.flip_y;
181 }
182 }
183 }
184 }
185
186 /// This system alternates the [`CursorIcon`]'s `rect` field between `None` and
187 /// 4 sections/rectangles of the cursor's image.
188 fn cycle_rect(input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut CursorIcon, With<Window>>) {
189 if !input.just_pressed(KeyCode::KeyC) {
190 return;
191 }
192
193 const RECT_SIZE: u32 = 32; // half the size of a tile in the texture atlas
194
195 const SECTIONS: [Option<URect>; 5] = [
196 Some(URect {
197 min: UVec2::ZERO,
198 max: UVec2::splat(RECT_SIZE),
199 }),
200 Some(URect {
201 min: UVec2::new(RECT_SIZE, 0),
202 max: UVec2::new(2 * RECT_SIZE, RECT_SIZE),
203 }),
204 Some(URect {
205 min: UVec2::new(0, RECT_SIZE),
206 max: UVec2::new(RECT_SIZE, 2 * RECT_SIZE),
207 }),
208 Some(URect {
209 min: UVec2::new(RECT_SIZE, RECT_SIZE),
210 max: UVec2::splat(2 * RECT_SIZE),
211 }),
212 None, // reset to None
213 ];
214
215 for mut cursor_icon in &mut query {
216 if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
217 let next_rect = SECTIONS
218 .iter()
219 .cycle()
220 .skip_while(|&&corner| corner != image.rect)
221 .nth(1) // move to the next element
222 .unwrap_or(&None);
223
224 image.rect = *next_rect;
225 }
226 }
227 }