use std::borrow::Cow;
use bevy::prelude::*;
use bevy::window::{CursorIcon, CustomCursor, CustomCursorImage, SystemCursorIcon};
use smallvec::SmallVec;
use crate::prelude::*;
use crate::sickle::*;
#[derive(Resource, Default)]
struct CursorSource
{
primary: Option<LoadableCursor>,
temporary: Option<LoadableCursor>,
}
impl CursorSource
{
fn get_next_cursor(
&mut self,
img_map: &mut ImageMap,
layout_map: &mut TextureAtlasLayoutMap,
asset_server: &AssetServer,
) -> Option<CursorIcon>
{
let cursor = self.temporary.take().or_else(|| self.primary.clone())?;
Some(cursor.into_cursor_icon(img_map, layout_map, asset_server)?)
}
}
fn get_temp_cursor(mut source: ResMut<CursorSource>, temps: Query<(Entity, &TempCursor)>)
{
let mut found: Option<(u8, Entity, &LoadableCursor)> = None;
let mut found_second: Option<(u8, Entity, &LoadableCursor)> = None;
for (entity, temp) in temps
.iter()
.filter(|(_, t)| !matches!(t.cursor, LoadableCursor::None))
{
let Some((prio, _, _)) = &found else {
found = Some((temp.priority, entity, &temp.cursor));
continue;
};
if temp.priority < *prio {
continue;
}
if temp.priority == *prio {
found_second = Some((temp.priority, entity, &temp.cursor));
continue;
}
found = Some((temp.priority, entity, &temp.cursor));
}
if let Some((entity, second)) = found_second.and_then(|(prio, e, s)| {
if prio >= found.unwrap().0 {
return Some((e, s));
}
None
}) {
warn_once!("multiple TempCursor instances detected (first: {:?} {:?}, second: {:?} {:?}); only one can be used at a \
time; this warning only prints once", found.unwrap().1, found.unwrap().2, entity, second);
}
if let Some((_, _, cursor)) = found {
source.temporary = Some(cursor.clone());
}
}
fn refresh_cursor_icon(
asset_server: Res<AssetServer>,
mut c: Commands,
mut source: ResMut<CursorSource>,
windows: Query<(Entity, Option<&CursorIcon>), With<Window>>,
mut img_map: ResMut<ImageMap>,
mut layout_map: ResMut<TextureAtlasLayoutMap>,
)
{
let next_cursor = source.get_next_cursor(&mut img_map, &mut layout_map, &asset_server);
for (window_entity, current_cursor) in windows.iter() {
if current_cursor == next_cursor.as_ref() {
continue;
}
let Some(next_cursor) = next_cursor.clone() else { continue };
c.entity(window_entity).insert(next_cursor);
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum LoadableCursor
{
#[default]
None,
Custom
{
image: Cow<'static, str>,
#[reflect(default)]
texture_atlas: Option<TextureAtlasReference>,
#[reflect(default)]
flip_x: bool,
#[reflect(default)]
flip_y: bool,
#[reflect(default)]
rect: Option<URect>,
hotspot: (u16, u16),
},
Url
{
url: Cow<'static, str>,
hotspot: (u16, u16),
},
System(SystemCursorIcon),
}
impl LoadableCursor
{
pub fn into_cursor_icon(
self,
img_map: &mut ImageMap,
layout_map: &mut TextureAtlasLayoutMap,
asset_server: &AssetServer,
) -> Option<CursorIcon>
{
match self {
Self::None => None,
Self::Custom { image, texture_atlas, flip_x, flip_y, rect, hotspot } => {
let handle = img_map.get_or_load(image.as_ref(), asset_server);
let texture_atlas = texture_atlas.and_then(|a| {
Some(TextureAtlas {
layout: layout_map.get(image.as_ref(), &a.alias),
index: a.index,
})
});
Some(CursorIcon::Custom(CustomCursor::Image(CustomCursorImage {
handle,
texture_atlas,
flip_x,
flip_y,
rect,
hotspot,
})))
}
Self::Url { url, hotspot } => {
if cfg!(not(all(target_family = "wasm", target_os = "unknown")))
{
warn_once!("making cursor icon from URL {url:?}; only WASM targets are supported, but the target \
is not WASM; this warning only prints once");
}
Some(CursorIcon::Custom(CustomCursor::Url(bevy::window::CustomCursorUrl {
url: url.to_string(),
hotspot,
})))
}
Self::System(icon) => Some(CursorIcon::System(icon)),
}
}
}
#[derive(Reflect, Default, Debug, Clone, PartialEq, Deref, DerefMut)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct PrimaryCursor(pub LoadableCursor);
impl Command for PrimaryCursor
{
fn apply(self, world: &mut World)
{
world.resource_mut::<CursorSource>().primary = Some(self.0);
}
}
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct TempCursor
{
pub priority: u8,
pub cursor: LoadableCursor,
}
impl Instruction for TempCursor
{
fn apply(self, entity: Entity, world: &mut World)
{
let _ = world.get_entity_mut(entity).map(|mut emut| {
emut.insert(self);
});
}
fn revert(entity: Entity, world: &mut World)
{
let _ = world.get_entity_mut(entity).map(|mut emut| {
emut.remove::<Self>();
});
}
}
impl StaticAttribute for TempCursor
{
type Value = Self;
fn construct(value: Self::Value) -> Self
{
value
}
}
impl ResponsiveAttribute for TempCursor {}
#[derive(Reflect, Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ResponsiveCursor
{
#[reflect(default)]
pub state: Option<SmallVec<[PseudoState; 3]>>,
#[reflect(default)]
pub hover: Option<LoadableCursor>,
#[reflect(default)]
pub press: Option<LoadableCursor>,
}
impl Instruction for ResponsiveCursor
{
fn apply(self, entity: Entity, world: &mut World)
{
let press = self
.press
.or_else(|| self.hover.clone())
.map(|cursor| TempCursor { priority: 2, cursor });
let hover = self.hover.map(|cursor| TempCursor { priority: 1, cursor });
let respond_to = world.get::<ControlMember>(entity).map(|m| m.id.clone());
let responsive = Responsive::<TempCursor> {
state: self.state,
respond_to,
idle: TempCursor { priority: 0, cursor: LoadableCursor::None },
hover,
press,
..Default::default()
};
responsive.apply(entity, world);
}
fn revert(entity: Entity, world: &mut World)
{
Responsive::<TempCursor>::revert(entity, world);
}
}
pub(crate) struct CursorPlugin;
impl Plugin for CursorPlugin
{
fn build(&self, app: &mut App)
{
app.init_resource::<CursorSource>()
.register_command_type::<PrimaryCursor>()
.register_responsive::<TempCursor>()
.register_instruction_type::<ResponsiveCursor>()
.add_systems(PostUpdate, (get_temp_cursor, refresh_cursor_icon).chain());
}
}