use std::marker::PhantomData;
use std::ops::{Add, Mul, Sub};
pub trait CoordinateSpace: Copy + Clone + private::Sealed {}
mod private {
pub trait Sealed {}
impl Sealed for super::Logical {}
impl Sealed for super::Physical {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Logical;
impl CoordinateSpace for Logical {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Physical;
impl CoordinateSpace for Physical {}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct ScaleFactor(pub f64);
impl ScaleFactor {
#[inline]
pub const fn new(scale: f64) -> Self {
Self(scale)
}
#[inline]
pub fn try_new(scale: f64) -> Option<Self> {
if scale.is_finite() && scale > 0.0 {
Some(Self(scale))
} else {
None
}
}
#[inline]
pub fn is_valid(self) -> bool {
self.0.is_finite() && self.0 > 0.0
}
#[inline]
pub fn inverse(self) -> Self {
debug_assert!(
self.is_valid(),
"ScaleFactor::inverse() called on invalid scale factor: {}",
self.0
);
Self(1.0 / self.0)
}
#[inline]
pub fn as_f32(self) -> f32 {
self.0 as f32
}
#[inline]
pub fn as_f64(self) -> f64 {
self.0
}
}
impl Default for ScaleFactor {
fn default() -> Self {
Self(1.0)
}
}
impl From<f64> for ScaleFactor {
fn from(scale: f64) -> Self {
Self(scale)
}
}
impl From<f32> for ScaleFactor {
fn from(scale: f32) -> Self {
Self(scale as f64)
}
}
impl From<ScaleFactor> for f64 {
fn from(scale: ScaleFactor) -> Self {
scale.0
}
}
impl From<ScaleFactor> for f32 {
fn from(scale: ScaleFactor) -> Self {
scale.0 as f32
}
}
#[derive(Debug, Clone, Copy)]
pub struct Size2D<T, S: CoordinateSpace> {
pub width: T,
pub height: T,
_marker: PhantomData<S>,
}
impl<T: PartialEq, S: CoordinateSpace> PartialEq for Size2D<T, S> {
fn eq(&self, other: &Self) -> bool {
self.width == other.width && self.height == other.height
}
}
impl<T: Eq, S: CoordinateSpace> Eq for Size2D<T, S> {}
impl<T: Default, S: CoordinateSpace> Default for Size2D<T, S> {
fn default() -> Self {
Self {
width: T::default(),
height: T::default(),
_marker: PhantomData,
}
}
}
impl<T: std::hash::Hash, S: CoordinateSpace> std::hash::Hash for Size2D<T, S> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.width.hash(state);
self.height.hash(state);
}
}
impl<T, S: CoordinateSpace> Size2D<T, S> {
#[inline]
pub const fn new(width: T, height: T) -> Self {
Self {
width,
height,
_marker: PhantomData,
}
}
#[inline]
pub fn into_tuple(self) -> (T, T) {
(self.width, self.height)
}
#[inline]
pub fn from_tuple(tuple: (T, T)) -> Self {
Self::new(tuple.0, tuple.1)
}
}
impl<T: Copy, S: CoordinateSpace> Size2D<T, S> {
#[inline]
pub fn width(&self) -> T {
self.width
}
#[inline]
pub fn height(&self) -> T {
self.height
}
}
impl<T: Copy, S: CoordinateSpace> Size2D<T, S> {
#[inline]
pub fn cast<U>(self) -> Size2D<U, S>
where
T: Into<U>,
{
Size2D::new(self.width.into(), self.height.into())
}
}
impl<T, S: CoordinateSpace> Size2D<T, S>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_f32(self) -> Size2D<f32, S> {
Size2D::new(self.width.into() as f32, self.height.into() as f32)
}
#[inline]
pub fn to_f64(self) -> Size2D<f64, S> {
Size2D::new(self.width.into(), self.height.into())
}
}
impl<T: Mul<Output = T> + Copy, S: CoordinateSpace> Mul<T> for Size2D<T, S> {
type Output = Self;
fn mul(self, rhs: T) -> Self::Output {
Self::new(self.width * rhs, self.height * rhs)
}
}
impl<T> Size2D<T, Logical>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_physical(self, scale: ScaleFactor) -> PhysicalSize<u32> {
PhysicalSize::new(
(self.width.into() * scale.0).round() as u32,
(self.height.into() * scale.0).round() as u32,
)
}
#[inline]
pub fn to_physical_f32(self, scale: ScaleFactor) -> PhysicalSize<f32> {
PhysicalSize::new(
(self.width.into() * scale.0) as f32,
(self.height.into() * scale.0) as f32,
)
}
#[inline]
pub fn to_physical_f64(self, scale: ScaleFactor) -> PhysicalSize<f64> {
PhysicalSize::new(self.width.into() * scale.0, self.height.into() * scale.0)
}
}
impl<T> Size2D<T, Physical>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_logical(self, scale: ScaleFactor) -> LogicalSize<f32> {
LogicalSize::new(
(self.width.into() / scale.0) as f32,
(self.height.into() / scale.0) as f32,
)
}
#[inline]
pub fn to_logical_f64(self, scale: ScaleFactor) -> LogicalSize<f64> {
LogicalSize::new(self.width.into() / scale.0, self.height.into() / scale.0)
}
}
pub type LogicalSize<T> = Size2D<T, Logical>;
pub type PhysicalSize<T> = Size2D<T, Physical>;
#[derive(Debug, Clone, Copy)]
pub struct Position2D<T, S: CoordinateSpace> {
pub x: T,
pub y: T,
_marker: PhantomData<S>,
}
impl<T: PartialEq, S: CoordinateSpace> PartialEq for Position2D<T, S> {
fn eq(&self, other: &Self) -> bool {
self.x == other.x && self.y == other.y
}
}
impl<T: Eq, S: CoordinateSpace> Eq for Position2D<T, S> {}
impl<T: Default, S: CoordinateSpace> Default for Position2D<T, S> {
fn default() -> Self {
Self {
x: T::default(),
y: T::default(),
_marker: PhantomData,
}
}
}
impl<T: std::hash::Hash, S: CoordinateSpace> std::hash::Hash for Position2D<T, S> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.x.hash(state);
self.y.hash(state);
}
}
impl<T, S: CoordinateSpace> Position2D<T, S> {
#[inline]
pub const fn new(x: T, y: T) -> Self {
Self {
x,
y,
_marker: PhantomData,
}
}
#[inline]
pub fn origin() -> Self
where
T: Default,
{
Self::default()
}
#[inline]
pub fn into_tuple(self) -> (T, T) {
(self.x, self.y)
}
#[inline]
pub fn from_tuple(tuple: (T, T)) -> Self {
Self::new(tuple.0, tuple.1)
}
}
impl<T: Copy, S: CoordinateSpace> Position2D<T, S> {
#[inline]
pub fn x(&self) -> T {
self.x
}
#[inline]
pub fn y(&self) -> T {
self.y
}
}
impl<T, S: CoordinateSpace> Position2D<T, S>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_f32(self) -> Position2D<f32, S> {
Position2D::new(self.x.into() as f32, self.y.into() as f32)
}
#[inline]
pub fn to_f64(self) -> Position2D<f64, S> {
Position2D::new(self.x.into(), self.y.into())
}
}
impl<T: Mul<Output = T> + Copy, S: CoordinateSpace> Mul<T> for Position2D<T, S> {
type Output = Self;
fn mul(self, rhs: T) -> Self::Output {
Self::new(self.x * rhs, self.y * rhs)
}
}
impl<T: Add<Output = T>, S: CoordinateSpace> Add for Position2D<T, S> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.x + rhs.x, self.y + rhs.y)
}
}
impl<T: Sub<Output = T>, S: CoordinateSpace> Sub for Position2D<T, S> {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self::new(self.x - rhs.x, self.y - rhs.y)
}
}
impl<T> Position2D<T, Logical>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_physical(self, scale: ScaleFactor) -> PhysicalPosition<i32> {
PhysicalPosition::new(
(self.x.into() * scale.0).round() as i32,
(self.y.into() * scale.0).round() as i32,
)
}
#[inline]
pub fn to_physical_f32(self, scale: ScaleFactor) -> PhysicalPosition<f32> {
PhysicalPosition::new(
(self.x.into() * scale.0) as f32,
(self.y.into() * scale.0) as f32,
)
}
#[inline]
pub fn to_physical_f64(self, scale: ScaleFactor) -> PhysicalPosition<f64> {
PhysicalPosition::new(self.x.into() * scale.0, self.y.into() * scale.0)
}
}
impl<T> Position2D<T, Physical>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_logical(self, scale: ScaleFactor) -> LogicalPosition<f32> {
LogicalPosition::new(
(self.x.into() / scale.0) as f32,
(self.y.into() / scale.0) as f32,
)
}
#[inline]
pub fn to_logical_f64(self, scale: ScaleFactor) -> LogicalPosition<f64> {
LogicalPosition::new(self.x.into() / scale.0, self.y.into() / scale.0)
}
}
pub type LogicalPosition<T> = Position2D<T, Logical>;
pub type PhysicalPosition<T> = Position2D<T, Physical>;
#[derive(Debug, Clone, Copy)]
pub struct Rect2D<T, S: CoordinateSpace> {
pub x: T,
pub y: T,
pub width: T,
pub height: T,
_marker: PhantomData<S>,
}
impl<T: PartialEq, S: CoordinateSpace> PartialEq for Rect2D<T, S> {
fn eq(&self, other: &Self) -> bool {
self.x == other.x
&& self.y == other.y
&& self.width == other.width
&& self.height == other.height
}
}
impl<T: Eq, S: CoordinateSpace> Eq for Rect2D<T, S> {}
impl<T: Default, S: CoordinateSpace> Default for Rect2D<T, S> {
fn default() -> Self {
Self {
x: T::default(),
y: T::default(),
width: T::default(),
height: T::default(),
_marker: PhantomData,
}
}
}
impl<T: std::hash::Hash, S: CoordinateSpace> std::hash::Hash for Rect2D<T, S> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.x.hash(state);
self.y.hash(state);
self.width.hash(state);
self.height.hash(state);
}
}
impl<T, S: CoordinateSpace> Rect2D<T, S> {
#[inline]
pub const fn new(x: T, y: T, width: T, height: T) -> Self {
Self {
x,
y,
width,
height,
_marker: PhantomData,
}
}
#[inline]
pub fn from_position_size(position: Position2D<T, S>, size: Size2D<T, S>) -> Self {
Self::new(position.x, position.y, size.width, size.height)
}
}
impl<T: Copy, S: CoordinateSpace> Rect2D<T, S> {
#[inline]
pub fn position(&self) -> Position2D<T, S> {
Position2D::new(self.x, self.y)
}
#[inline]
pub fn size(&self) -> Size2D<T, S> {
Size2D::new(self.width, self.height)
}
#[inline]
pub fn x(&self) -> T {
self.x
}
#[inline]
pub fn y(&self) -> T {
self.y
}
#[inline]
pub fn width(&self) -> T {
self.width
}
#[inline]
pub fn height(&self) -> T {
self.height
}
}
impl<T, S: CoordinateSpace> Rect2D<T, S>
where
T: Copy + Add<Output = T>,
{
#[inline]
pub fn right(&self) -> T {
self.x + self.width
}
#[inline]
pub fn bottom(&self) -> T {
self.y + self.height
}
}
impl<T, S: CoordinateSpace> Rect2D<T, S>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_f32(self) -> Rect2D<f32, S> {
Rect2D::new(
self.x.into() as f32,
self.y.into() as f32,
self.width.into() as f32,
self.height.into() as f32,
)
}
#[inline]
pub fn to_f64(self) -> Rect2D<f64, S> {
Rect2D::new(
self.x.into(),
self.y.into(),
self.width.into(),
self.height.into(),
)
}
}
impl<T, S: CoordinateSpace> Rect2D<T, S>
where
T: Copy + PartialOrd + Add<Output = T>,
{
#[inline]
pub fn contains(&self, point: Position2D<T, S>) -> bool {
point.x >= self.x
&& point.x < self.x + self.width
&& point.y >= self.y
&& point.y < self.y + self.height
}
}
impl<T> Rect2D<T, Logical>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_physical(self, scale: ScaleFactor) -> PhysicalRect<u32> {
PhysicalRect::new(
(self.x.into() * scale.0).round() as u32,
(self.y.into() * scale.0).round() as u32,
(self.width.into() * scale.0).round() as u32,
(self.height.into() * scale.0).round() as u32,
)
}
#[inline]
pub fn to_physical_f32(self, scale: ScaleFactor) -> PhysicalRect<f32> {
PhysicalRect::new(
(self.x.into() * scale.0) as f32,
(self.y.into() * scale.0) as f32,
(self.width.into() * scale.0) as f32,
(self.height.into() * scale.0) as f32,
)
}
}
impl<T> Rect2D<T, Physical>
where
T: Copy + Into<f64>,
{
#[inline]
pub fn to_logical(self, scale: ScaleFactor) -> LogicalRect<f32> {
LogicalRect::new(
(self.x.into() / scale.0) as f32,
(self.y.into() / scale.0) as f32,
(self.width.into() / scale.0) as f32,
(self.height.into() / scale.0) as f32,
)
}
#[inline]
pub fn to_logical_f64(self, scale: ScaleFactor) -> LogicalRect<f64> {
LogicalRect::new(
self.x.into() / scale.0,
self.y.into() / scale.0,
self.width.into() / scale.0,
self.height.into() / scale.0,
)
}
}
pub type LogicalRect<T> = Rect2D<T, Logical>;
pub type PhysicalRect<T> = Rect2D<T, Physical>;
#[cfg(feature = "winit")]
mod winit_interop {
use super::*;
impl From<winit::dpi::PhysicalSize<u32>> for PhysicalSize<u32> {
fn from(size: winit::dpi::PhysicalSize<u32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<PhysicalSize<u32>> for winit::dpi::PhysicalSize<u32> {
fn from(size: PhysicalSize<u32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<winit::dpi::PhysicalSize<f32>> for PhysicalSize<f32> {
fn from(size: winit::dpi::PhysicalSize<f32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<PhysicalSize<f32>> for winit::dpi::PhysicalSize<f32> {
fn from(size: PhysicalSize<f32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<winit::dpi::LogicalSize<u32>> for LogicalSize<u32> {
fn from(size: winit::dpi::LogicalSize<u32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<LogicalSize<u32>> for winit::dpi::LogicalSize<u32> {
fn from(size: LogicalSize<u32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<winit::dpi::LogicalSize<f32>> for LogicalSize<f32> {
fn from(size: winit::dpi::LogicalSize<f32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<LogicalSize<f32>> for winit::dpi::LogicalSize<f32> {
fn from(size: LogicalSize<f32>) -> Self {
Self::new(size.width, size.height)
}
}
impl From<winit::dpi::PhysicalPosition<i32>> for PhysicalPosition<i32> {
fn from(pos: winit::dpi::PhysicalPosition<i32>) -> Self {
Self::new(pos.x, pos.y)
}
}
impl From<PhysicalPosition<i32>> for winit::dpi::PhysicalPosition<i32> {
fn from(pos: PhysicalPosition<i32>) -> Self {
Self::new(pos.x, pos.y)
}
}
impl From<winit::dpi::PhysicalPosition<f64>> for PhysicalPosition<f64> {
fn from(pos: winit::dpi::PhysicalPosition<f64>) -> Self {
Self::new(pos.x, pos.y)
}
}
impl From<PhysicalPosition<f64>> for winit::dpi::PhysicalPosition<f64> {
fn from(pos: PhysicalPosition<f64>) -> Self {
Self::new(pos.x, pos.y)
}
}
impl From<winit::dpi::LogicalPosition<f64>> for LogicalPosition<f64> {
fn from(pos: winit::dpi::LogicalPosition<f64>) -> Self {
Self::new(pos.x, pos.y)
}
}
impl From<LogicalPosition<f64>> for winit::dpi::LogicalPosition<f64> {
fn from(pos: LogicalPosition<f64>) -> Self {
Self::new(pos.x, pos.y)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Size<T> {
pub width: T,
pub height: T,
}
impl<T> Size<T> {
#[inline]
pub const fn new(width: T, height: T) -> Self {
Self { width, height }
}
}
impl<T: Copy> Size<T> {
#[inline]
pub fn into_tuple(self) -> (T, T) {
(self.width, self.height)
}
}
impl<T: Mul<Output = T> + Copy> Mul<T> for Size<T> {
type Output = Self;
fn mul(self, rhs: T) -> Self::Output {
Self::new(self.width * rhs, self.height * rhs)
}
}
impl<T: Add<Output = T>> Add for Size<T> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.width + rhs.width, self.height + rhs.height)
}
}
impl<T: Sub<Output = T>> Sub for Size<T> {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self::new(self.width - rhs.width, self.height - rhs.height)
}
}
impl<T, S: CoordinateSpace> From<Size2D<T, S>> for Size<T> {
fn from(size: Size2D<T, S>) -> Self {
Self::new(size.width, size.height)
}
}
impl<T, S: CoordinateSpace> From<Position2D<T, S>> for Pos<T> {
fn from(pos: Position2D<T, S>) -> Self {
Self::new(pos.x, pos.y)
}
}
impl<T, S: CoordinateSpace> From<Rect2D<T, S>> for Rect<T> {
fn from(rect: Rect2D<T, S>) -> Self {
Self::new(rect.x, rect.y, rect.width, rect.height)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Pos<T> {
pub x: T,
pub y: T,
}
impl<T> Pos<T> {
#[inline]
pub const fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Copy> Pos<T> {
#[inline]
pub fn into_tuple(self) -> (T, T) {
(self.x, self.y)
}
}
impl<T: Mul<Output = T> + Copy> Mul<T> for Pos<T> {
type Output = Self;
fn mul(self, rhs: T) -> Self::Output {
Self::new(self.x * rhs, self.y * rhs)
}
}
impl<T: Add<Output = T>> Add for Pos<T> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::new(self.x + rhs.x, self.y + rhs.y)
}
}
impl<T: Sub<Output = T>> Sub for Pos<T> {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self::new(self.x - rhs.x, self.y - rhs.y)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Rect<T> {
pub x: T,
pub y: T,
pub width: T,
pub height: T,
}
impl<T> Rect<T> {
#[inline]
pub const fn new(x: T, y: T, width: T, height: T) -> Self {
Self {
x,
y,
width,
height,
}
}
#[inline]
pub fn from_position_size(pos: Pos<T>, size: Size<T>) -> Self {
Self::new(pos.x, pos.y, size.width, size.height)
}
#[inline]
pub fn position(&self) -> Pos<T>
where
T: Copy,
{
Pos::new(self.x, self.y)
}
#[inline]
pub fn size(&self) -> Size<T>
where
T: Copy,
{
Size::new(self.width, self.height)
}
}
impl<T> Rect<T>
where
T: Copy + PartialOrd + Add<Output = T>,
{
#[inline]
pub fn contains(&self, point: Pos<T>) -> bool {
point.x >= self.x
&& point.x < self.x + self.width
&& point.y >= self.y
&& point.y < self.y + self.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logical_to_physical_size() {
let logical = LogicalSize::new(100.0_f64, 50.0);
let scale = ScaleFactor(2.0);
let physical = logical.to_physical(scale);
assert_eq!(physical.width, 200);
assert_eq!(physical.height, 100);
}
#[test]
fn test_physical_to_logical_size() {
let physical = PhysicalSize::new(200_u32, 100);
let scale = ScaleFactor(2.0);
let logical = physical.to_logical(scale);
assert_eq!(logical.width, 100.0);
assert_eq!(logical.height, 50.0);
}
#[test]
fn test_logical_to_physical_position() {
let logical = LogicalPosition::new(50.0_f64, 25.0);
let scale = ScaleFactor(2.0);
let physical = logical.to_physical(scale);
assert_eq!(physical.x, 100);
assert_eq!(physical.y, 50);
}
#[test]
fn test_rect_contains() {
let rect = LogicalRect::new(10.0_f64, 10.0, 100.0, 50.0);
assert!(rect.contains(LogicalPosition::new(50.0, 30.0)));
assert!(!rect.contains(LogicalPosition::new(5.0, 30.0)));
assert!(!rect.contains(LogicalPosition::new(50.0, 5.0)));
}
#[test]
fn test_scale_factor_inverse() {
let scale = ScaleFactor(2.0);
let inv = scale.inverse();
assert_eq!(inv.0, 0.5);
}
#[test]
fn test_size_multiplication() {
let size = LogicalSize::new(10.0_f32, 20.0);
let scaled = size * 2.0;
assert_eq!(scaled.width, 20.0);
assert_eq!(scaled.height, 40.0);
}
#[test]
fn test_position_arithmetic() {
let a = LogicalPosition::new(10.0_f32, 20.0);
let b = LogicalPosition::new(5.0, 10.0);
let sum = a + b;
let diff = a - b;
assert_eq!(sum.x, 15.0);
assert_eq!(sum.y, 30.0);
assert_eq!(diff.x, 5.0);
assert_eq!(diff.y, 10.0);
}
#[test]
fn test_rect_from_position_size() {
let pos = LogicalPosition::new(10.0_f32, 20.0);
let size = LogicalSize::new(100.0_f32, 50.0);
let rect = LogicalRect::from_position_size(pos, size);
assert_eq!(rect.x, 10.0);
assert_eq!(rect.y, 20.0);
assert_eq!(rect.width, 100.0);
assert_eq!(rect.height, 50.0);
}
}