 14ad25227b
			
		
	
	
		14ad25227b
		
			
		
	
	
	
	
		
			
			# Objective - Make `CustomCursor::Image` easier to work with by splitting the enum variants off into `CustomCursorImage` and `CustomCursorUrl` structs and deriving `Default` on those structs. - Refs #17276. ## Testing - Ran two examples: `cargo run --example custom_cursor_image --features=custom_cursor` and `cargo run --example window_settings --features=custom_cursor` - CI. --- ## Migration Guide The `CustomCursor` enum's variants now hold instances of `CustomCursorImage` or `CustomCursorUrl`. Update your uses of `CustomCursor` accordingly.
		
			
				
	
	
		
			228 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			228 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Illustrates how to use a custom cursor image with a texture atlas and
 | |
| //! animation.
 | |
| 
 | |
| use std::time::Duration;
 | |
| 
 | |
| use bevy::{
 | |
|     prelude::*,
 | |
|     winit::cursor::{CursorIcon, CustomCursor, CustomCursorImage},
 | |
| };
 | |
| 
 | |
| fn main() {
 | |
|     App::new()
 | |
|         .add_plugins(DefaultPlugins)
 | |
|         .add_systems(
 | |
|             Startup,
 | |
|             (setup_cursor_icon, setup_camera, setup_instructions),
 | |
|         )
 | |
|         .add_systems(
 | |
|             Update,
 | |
|             (
 | |
|                 execute_animation,
 | |
|                 toggle_texture_atlas,
 | |
|                 toggle_flip_x,
 | |
|                 toggle_flip_y,
 | |
|                 cycle_rect,
 | |
|             ),
 | |
|         )
 | |
|         .run();
 | |
| }
 | |
| 
 | |
| fn setup_cursor_icon(
 | |
|     mut commands: Commands,
 | |
|     asset_server: Res<AssetServer>,
 | |
|     mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
 | |
|     window: Single<Entity, With<Window>>,
 | |
| ) {
 | |
|     let layout =
 | |
|         TextureAtlasLayout::from_grid(UVec2::splat(64), 20, 10, Some(UVec2::splat(5)), None);
 | |
|     let texture_atlas_layout = texture_atlas_layouts.add(layout);
 | |
| 
 | |
|     let animation_config = AnimationConfig::new(0, 199, 1, 4);
 | |
| 
 | |
|     commands.entity(*window).insert((
 | |
|         CursorIcon::Custom(CustomCursor::Image(CustomCursorImage {
 | |
|             // Image to use as the cursor.
 | |
|             handle: asset_server
 | |
|                 .load("cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png"),
 | |
|             // Optional texture atlas allows you to pick a section of the image
 | |
|             // and animate it.
 | |
|             texture_atlas: Some(TextureAtlas {
 | |
|                 layout: texture_atlas_layout.clone(),
 | |
|                 index: animation_config.first_sprite_index,
 | |
|             }),
 | |
|             flip_x: false,
 | |
|             flip_y: false,
 | |
|             // Optional section of the image to use as the cursor.
 | |
|             rect: None,
 | |
|             // The hotspot is the point in the cursor image that will be
 | |
|             // positioned at the mouse cursor's position.
 | |
|             hotspot: (0, 0),
 | |
|         })),
 | |
|         animation_config,
 | |
|     ));
 | |
| }
 | |
| 
 | |
| fn setup_camera(mut commands: Commands) {
 | |
|     commands.spawn(Camera3d::default());
 | |
| }
 | |
| 
 | |
| fn setup_instructions(mut commands: Commands) {
 | |
|     commands.spawn((
 | |
|         Text::new(
 | |
|             "Press T to toggle the cursor's `texture_atlas`.\n
 | |
| Press X to toggle the cursor's `flip_x` setting.\n
 | |
| Press Y to toggle the cursor's `flip_y` setting.\n
 | |
| Press C to cycle through the sections of the cursor's image using `rect`.",
 | |
|         ),
 | |
|         Node {
 | |
|             position_type: PositionType::Absolute,
 | |
|             bottom: Val::Px(12.0),
 | |
|             left: Val::Px(12.0),
 | |
|             ..default()
 | |
|         },
 | |
|     ));
 | |
| }
 | |
| 
 | |
| #[derive(Component)]
 | |
| struct AnimationConfig {
 | |
|     first_sprite_index: usize,
 | |
|     last_sprite_index: usize,
 | |
|     increment: usize,
 | |
|     fps: u8,
 | |
|     frame_timer: Timer,
 | |
| }
 | |
| 
 | |
| impl AnimationConfig {
 | |
|     fn new(first: usize, last: usize, increment: usize, fps: u8) -> Self {
 | |
|         Self {
 | |
|             first_sprite_index: first,
 | |
|             last_sprite_index: last,
 | |
|             increment,
 | |
|             fps,
 | |
|             frame_timer: Self::timer_from_fps(fps),
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn timer_from_fps(fps: u8) -> Timer {
 | |
|         Timer::new(Duration::from_secs_f32(1.0 / (fps as f32)), TimerMode::Once)
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// This system loops through all the sprites in the [`CursorIcon`]'s
 | |
| /// [`TextureAtlas`], from [`AnimationConfig`]'s `first_sprite_index` to
 | |
| /// `last_sprite_index`.
 | |
| fn execute_animation(time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut CursorIcon)>) {
 | |
|     for (mut config, mut cursor_icon) in &mut query {
 | |
|         if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
 | |
|             config.frame_timer.tick(time.delta());
 | |
| 
 | |
|             if config.frame_timer.finished() {
 | |
|                 if let Some(atlas) = image.texture_atlas.as_mut() {
 | |
|                     atlas.index += config.increment;
 | |
| 
 | |
|                     if atlas.index > config.last_sprite_index {
 | |
|                         atlas.index = config.first_sprite_index;
 | |
|                     }
 | |
| 
 | |
|                     config.frame_timer = AnimationConfig::timer_from_fps(config.fps);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn toggle_texture_atlas(
 | |
|     input: Res<ButtonInput<KeyCode>>,
 | |
|     mut query: Query<&mut CursorIcon, With<Window>>,
 | |
|     mut cached_atlas: Local<Option<TextureAtlas>>, // this lets us restore the previous value
 | |
| ) {
 | |
|     if input.just_pressed(KeyCode::KeyT) {
 | |
|         for mut cursor_icon in &mut query {
 | |
|             if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
 | |
|                 match image.texture_atlas.take() {
 | |
|                     Some(a) => {
 | |
|                         // Save the current texture atlas.
 | |
|                         *cached_atlas = Some(a.clone());
 | |
|                     }
 | |
|                     None => {
 | |
|                         // Restore the cached texture atlas.
 | |
|                         if let Some(cached_a) = cached_atlas.take() {
 | |
|                             image.texture_atlas = Some(cached_a);
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn toggle_flip_x(
 | |
|     input: Res<ButtonInput<KeyCode>>,
 | |
|     mut query: Query<&mut CursorIcon, With<Window>>,
 | |
| ) {
 | |
|     if input.just_pressed(KeyCode::KeyX) {
 | |
|         for mut cursor_icon in &mut query {
 | |
|             if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
 | |
|                 image.flip_x = !image.flip_x;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn toggle_flip_y(
 | |
|     input: Res<ButtonInput<KeyCode>>,
 | |
|     mut query: Query<&mut CursorIcon, With<Window>>,
 | |
| ) {
 | |
|     if input.just_pressed(KeyCode::KeyY) {
 | |
|         for mut cursor_icon in &mut query {
 | |
|             if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
 | |
|                 image.flip_y = !image.flip_y;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// This system alternates the [`CursorIcon`]'s `rect` field between `None` and
 | |
| /// 4 sections/rectangles of the cursor's image.
 | |
| fn cycle_rect(input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut CursorIcon, With<Window>>) {
 | |
|     if !input.just_pressed(KeyCode::KeyC) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const RECT_SIZE: u32 = 32; // half the size of a tile in the texture atlas
 | |
| 
 | |
|     const SECTIONS: [Option<URect>; 5] = [
 | |
|         Some(URect {
 | |
|             min: UVec2::ZERO,
 | |
|             max: UVec2::splat(RECT_SIZE),
 | |
|         }),
 | |
|         Some(URect {
 | |
|             min: UVec2::new(RECT_SIZE, 0),
 | |
|             max: UVec2::new(2 * RECT_SIZE, RECT_SIZE),
 | |
|         }),
 | |
|         Some(URect {
 | |
|             min: UVec2::new(0, RECT_SIZE),
 | |
|             max: UVec2::new(RECT_SIZE, 2 * RECT_SIZE),
 | |
|         }),
 | |
|         Some(URect {
 | |
|             min: UVec2::new(RECT_SIZE, RECT_SIZE),
 | |
|             max: UVec2::splat(2 * RECT_SIZE),
 | |
|         }),
 | |
|         None, // reset to None
 | |
|     ];
 | |
| 
 | |
|     for mut cursor_icon in &mut query {
 | |
|         if let CursorIcon::Custom(CustomCursor::Image(ref mut image)) = *cursor_icon {
 | |
|             let next_rect = SECTIONS
 | |
|                 .iter()
 | |
|                 .cycle()
 | |
|                 .skip_while(|&&corner| corner != image.rect)
 | |
|                 .nth(1) // move to the next element
 | |
|                 .unwrap_or(&None);
 | |
| 
 | |
|             image.rect = *next_rect;
 | |
|         }
 | |
|     }
 | |
| }
 |