custom_cursor_image/
custom_cursor_image.rs

1//! Illustrates how to use a custom cursor image with a texture atlas and
2//! animation.
3
4use std::time::Duration;
5
6use bevy::{
7    prelude::*,
8    winit::cursor::{CursorIcon, CustomCursor, CustomCursorImage},
9};
10
11fn 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
31fn 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
66fn setup_camera(mut commands: Commands) {
67    commands.spawn(Camera3d::default());
68}
69
70fn setup_instructions(mut commands: Commands) {
71    commands.spawn((
72        Text::new(
73            "Press T to toggle the cursor's `texture_atlas`.\n
74Press X to toggle the cursor's `flip_x` setting.\n
75Press Y to toggle the cursor's `flip_y` setting.\n
76Press C to cycle through the sections of the cursor's image using `rect`.",
77        ),
78        Node {
79            position_type: PositionType::Absolute,
80            bottom: Val::Px(12.0),
81            left: Val::Px(12.0),
82            ..default()
83        },
84    ));
85}
86
87#[derive(Component)]
88struct AnimationConfig {
89    first_sprite_index: usize,
90    last_sprite_index: usize,
91    increment: usize,
92    fps: u8,
93    frame_timer: Timer,
94}
95
96impl 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`.
115fn 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.finished() {
121                if let Some(atlas) = image.texture_atlas.as_mut() {
122                    atlas.index += config.increment;
123
124                    if atlas.index > config.last_sprite_index {
125                        atlas.index = config.first_sprite_index;
126                    }
127
128                    config.frame_timer = AnimationConfig::timer_from_fps(config.fps);
129                }
130            }
131        }
132    }
133}
134
135fn 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
160fn 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
173fn 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.
188fn 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}