[go: up one dir, main page]

winit-core 0.31.0-beta.2

winit core API.
Documentation
use core::fmt;
use std::error::Error;
use std::hash::Hash;
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;

#[doc(inline)]
pub use cursor_icon::CursorIcon;

use crate::as_any::AsAny;

/// The maximum width and height for a cursor when using [`CustomCursorSource::from_rgba`].
pub const MAX_CURSOR_SIZE: u16 = 2048;

const PIXEL_SIZE: usize = 4;

/// See [`Window::set_cursor()`][crate::window::Window::set_cursor] for more details.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Cursor {
    Icon(CursorIcon),
    Custom(CustomCursor),
}

impl Default for Cursor {
    fn default() -> Self {
        Self::Icon(CursorIcon::default())
    }
}

impl From<CursorIcon> for Cursor {
    fn from(icon: CursorIcon) -> Self {
        Self::Icon(icon)
    }
}

impl From<CustomCursor> for Cursor {
    fn from(custom: CustomCursor) -> Self {
        Self::Custom(custom)
    }
}

/// Use a custom image as a cursor (mouse pointer).
///
/// Is guaranteed to be cheap to clone.
///
/// ## Platform-specific
///
/// **Web**: Some browsers have limits on cursor sizes usually at 128x128.
///
/// # Example
///
/// ```no_run
/// # use winit_core::event_loop::ActiveEventLoop;
/// # use winit_core::window::Window;
/// # fn scope(event_loop: &dyn ActiveEventLoop, window: &dyn Window) {
/// use winit_core::cursor::CustomCursorSource;
///
/// let w = 10;
/// let h = 10;
/// let rgba = vec![255; (w * h * 4) as usize];
///
/// #[cfg(not(target_family = "wasm"))]
/// let source = CustomCursorSource::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
///
/// #[cfg(target_family = "wasm")]
/// let source = CustomCursorSource::Url {
///     url: String::from("http://localhost:3000/cursor.png"),
///     hotspot_x: 0,
///     hotspot_y: 0,
/// };
///
/// if let Ok(custom_cursor) = event_loop.create_custom_cursor(source) {
///     window.set_cursor(custom_cursor.clone().into());
/// }
/// # }
/// ```
#[derive(Clone, Debug)]
pub struct CustomCursor(pub Arc<dyn CustomCursorProvider>);

pub trait CustomCursorProvider: AsAny + fmt::Debug + Send + Sync {
    /// Whether a cursor was backed by animation.
    fn is_animated(&self) -> bool;
}

impl PartialEq for CustomCursor {
    fn eq(&self, other: &Self) -> bool {
        Arc::ptr_eq(&self.0, &other.0)
    }
}

impl Eq for CustomCursor {}

impl Hash for CustomCursor {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        Arc::as_ptr(&self.0).hash(state);
    }
}

impl Deref for CustomCursor {
    type Target = dyn CustomCursorProvider;

    fn deref(&self) -> &Self::Target {
        self.0.deref()
    }
}

impl_dyn_casting!(CustomCursorProvider);

/// Source for [`CustomCursor`].
///
/// See [`CustomCursor`] for more details.
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub enum CustomCursorSource {
    /// Cursor that is backed by RGBA image.
    ///
    /// See [CustomCursorSource::from_rgba] for more.
    ///
    /// ## Platform-specific
    ///
    /// - **iOS / Android / Orbital:** Unsupported
    Image(CursorImage),
    /// Animated cursor.
    ///
    /// See [CustomCursorSource::from_animation] for more.
    ///
    /// ## Platform-specific
    ///
    /// - **iOS / Android / Wayland / Windows / X11 / macOS / Orbital:** Unsupported
    Animation(CursorAnimation),
    /// Creates a new cursor from a URL pointing to an image.
    /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
    /// but browser support for image formats is inconsistent. Using [PNG] is recommended.
    ///
    /// [PNG]: https://en.wikipedia.org/wiki/PNG
    ///
    /// ## Platform-specific
    ///
    /// - **iOS / Android / Wayland / Windows / X11 / macOS / Orbital:** Unsupported
    Url { hotspot_x: u16, hotspot_y: u16, url: String },
}

impl CustomCursorSource {
    /// Creates a new cursor from an rgba buffer.
    ///
    /// The alpha channel is assumed to be **not** premultiplied.
    pub fn from_rgba(
        rgba: Vec<u8>,
        width: u16,
        height: u16,
        hotspot_x: u16,
        hotspot_y: u16,
    ) -> Result<Self, BadImage> {
        CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self::Image)
    }

    /// Crates a new animated cursor from multiple [`CustomCursor`]s
    /// Supplied `cursors` can't be empty or other animations.
    pub fn from_animation(
        duration: Duration,
        cursors: Vec<CustomCursor>,
    ) -> Result<Self, BadAnimation> {
        CursorAnimation::new(duration, cursors).map(Self::Animation)
    }
}

/// An error produced when using [`CustomCursorSource::from_rgba`] with invalid arguments.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BadImage {
    /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't
    /// guarantee that the cursor will work, but should avoid many platform and device specific
    /// limits.
    TooLarge { width: u16, height: u16 },
    /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
    /// safely interpreted as 32bpp RGBA pixels.
    ByteCountNotDivisibleBy4 { byte_count: usize },
    /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
    /// At least one of your arguments is incorrect.
    DimensionsVsPixelCount { width: u16, height: u16, width_x_height: u64, pixel_count: u64 },
    /// Produced when the hotspot is outside the image bounds
    HotspotOutOfBounds { width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 },
}

impl fmt::Display for BadImage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BadImage::TooLarge { width, height } => write!(
                f,
                "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is \
                 {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.",
            ),
            BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(
                f,
                "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \
                 it impossible to interpret as 32bpp RGBA pixels.",
            ),
            BadImage::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => {
                write!(
                    f,
                    "The specified dimensions ({width:?}x{height:?}) don't match the number of \
                     pixels supplied by the `rgba` argument ({pixel_count:?}). For those \
                     dimensions, the expected pixel count is {width_x_height:?}.",
                )
            },
            BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y } => write!(
                f,
                "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds \
                 ({width:?}x{height:?}).",
            ),
        }
    }
}

impl Error for BadImage {}

/// An error produced when using [`CustomCursorSource::from_animation`] with invalid arguments.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BadAnimation {
    /// Produced when no cursors were supplied.
    Empty,
    /// Produced when a supplied cursor is an animation.
    Animation,
}

impl fmt::Display for BadAnimation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => write!(f, "No cursors supplied"),
            Self::Animation => write!(f, "A supplied cursor is an animation"),
        }
    }
}

impl Error for BadAnimation {}

#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct CursorImage {
    pub(crate) rgba: Vec<u8>,
    pub(crate) width: u16,
    pub(crate) height: u16,
    pub(crate) hotspot_x: u16,
    pub(crate) hotspot_y: u16,
}

impl CursorImage {
    pub(crate) fn from_rgba(
        rgba: Vec<u8>,
        width: u16,
        height: u16,
        hotspot_x: u16,
        hotspot_y: u16,
    ) -> Result<Self, BadImage> {
        if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE {
            return Err(BadImage::TooLarge { width, height });
        }

        if rgba.len() % PIXEL_SIZE != 0 {
            return Err(BadImage::ByteCountNotDivisibleBy4 { byte_count: rgba.len() });
        }

        let pixel_count = (rgba.len() / PIXEL_SIZE) as u64;
        let width_x_height = width as u64 * height as u64;
        if pixel_count != width_x_height {
            return Err(BadImage::DimensionsVsPixelCount {
                width,
                height,
                width_x_height,
                pixel_count,
            });
        }

        if hotspot_x >= width || hotspot_y >= height {
            return Err(BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y });
        }

        Ok(CursorImage { rgba, width, height, hotspot_x, hotspot_y })
    }

    pub fn buffer(&self) -> &[u8] {
        self.rgba.as_slice()
    }

    pub fn buffer_mut(&mut self) -> &mut [u8] {
        self.rgba.as_mut_slice()
    }

    pub fn width(&self) -> u16 {
        self.width
    }

    pub fn height(&self) -> u16 {
        self.height
    }

    pub fn hotspot_x(&self) -> u16 {
        self.hotspot_x
    }

    pub fn hotspot_y(&self) -> u16 {
        self.hotspot_y
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CursorAnimation {
    pub(crate) duration: Duration,
    pub(crate) cursors: Vec<CustomCursor>,
}

impl CursorAnimation {
    pub fn new(duration: Duration, cursors: Vec<CustomCursor>) -> Result<Self, BadAnimation> {
        if cursors.is_empty() {
            return Err(BadAnimation::Empty);
        }

        if cursors.iter().any(|cursor| cursor.is_animated()) {
            return Err(BadAnimation::Animation);
        }

        Ok(Self { duration, cursors })
    }

    pub fn duration(&self) -> Duration {
        self.duration
    }

    pub fn cursors(&self) -> &[CustomCursor] {
        self.cursors.as_slice()
    }

    pub fn into_raw(self) -> (Duration, Vec<CustomCursor>) {
        (self.duration, self.cursors)
    }
}