From ea14c6b104cecf131ab5e7ba320bc52373efa3d1 Mon Sep 17 00:00:00 2001
From: Amr Bashir <amr.bashir2015@gmail.com>
Date: Tue, 11 Jul 2023 16:55:36 +0300
Subject: [PATCH] feat: split min/max size constraints (#759)

* feat: split min/max size constraints, closes #138

* windows impl

* linux impl

* fix linux impl

* cleanup linux impl

* macOS impl

* imports

* ios build

* macos build

* unsafe

* merge `set_min/max_width/height` into a single function

* fix macos build

* macos again

* use macros to generate DPI types

ref: https://github.com/rust-windowing/winit/pull/2148

* fix windows impl

* fmt
---
 examples/min_max_size.rs                  |  57 ++-
 src/dpi.rs                                | 580 +++++++++-------------
 src/platform_impl/android/mod.rs          |   4 +-
 src/platform_impl/ios/window.rs           |  15 +-
 src/platform_impl/linux/event_loop.rs     |  31 +-
 src/platform_impl/linux/util.rs           |  41 +-
 src/platform_impl/linux/window.rs         | 109 ++--
 src/platform_impl/macos/window.rs         |  66 ++-
 src/platform_impl/windows/event_loop.rs   |  72 ++-
 src/platform_impl/windows/system_tray.rs  |   2 +-
 src/platform_impl/windows/window.rs       |  36 +-
 src/platform_impl/windows/window_state.rs |  10 +-
 src/window.rs                             | 162 +++++-
 13 files changed, 624 insertions(+), 561 deletions(-)

diff --git a/examples/min_max_size.rs b/examples/min_max_size.rs
index 03c3004a4..527519727 100644
--- a/examples/min_max_size.rs
+++ b/examples/min_max_size.rs
@@ -3,10 +3,11 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use tao::{
-  dpi::LogicalSize,
-  event::{Event, WindowEvent},
+  dpi::LogicalPixel,
+  event::{ElementState, Event, KeyEvent, WindowEvent},
   event_loop::{ControlFlow, EventLoop},
-  window::WindowBuilder,
+  keyboard::Key,
+  window::{WindowBuilder, WindowSizeConstraints},
 };
 
 #[allow(clippy::single_match)]
@@ -14,20 +15,64 @@ fn main() {
   env_logger::init();
   let event_loop = EventLoop::new();
 
+  let min_width = 400.0;
+  let max_width = 800.0;
+  let min_height = 200.0;
+  let max_height = 400.0;
+  let mut size_constraints = WindowSizeConstraints::default();
+
   let window = WindowBuilder::new().build(&event_loop).unwrap();
 
-  window.set_min_inner_size(Some(LogicalSize::new(400.0, 200.0)));
-  window.set_max_inner_size(Some(LogicalSize::new(800.0, 400.0)));
+  eprintln!("constraint keys:");
+  eprintln!("  (E) Toggle the min width");
+  eprintln!("  (F) Toggle the max width");
+  eprintln!("  (P) Toggle the min height");
+  eprintln!("  (V) Toggle the max height");
 
   event_loop.run(move |event, _, control_flow| {
     *control_flow = ControlFlow::Wait;
-    println!("{:?}", event);
 
     match event {
       Event::WindowEvent {
         event: WindowEvent::CloseRequested,
         ..
       } => *control_flow = ControlFlow::Exit,
+
+      Event::WindowEvent {
+        event:
+          WindowEvent::KeyboardInput {
+            event:
+              KeyEvent {
+                logical_key: Key::Character(key_str),
+                state: ElementState::Released,
+                ..
+              },
+            ..
+          },
+        ..
+      } => match key_str {
+        "e" => {
+          size_constraints.min_width =
+            (!size_constraints.min_width.is_some()).then_some(LogicalPixel::new(min_width).into());
+          window.set_inner_size_constraints(size_constraints);
+        }
+        "f" => {
+          size_constraints.max_width =
+            (!size_constraints.max_width.is_some()).then_some(LogicalPixel::new(max_width).into());
+          window.set_inner_size_constraints(size_constraints);
+        }
+        "p" => {
+          size_constraints.min_height = (!size_constraints.min_height.is_some())
+            .then_some(LogicalPixel::new(min_height).into());
+          window.set_inner_size_constraints(size_constraints);
+        }
+        "v" => {
+          size_constraints.max_height = (!size_constraints.max_height.is_some())
+            .then_some(LogicalPixel::new(max_height).into());
+          window.set_inner_size_constraints(size_constraints);
+        }
+        _ => {}
+      },
       _ => (),
     }
   });
diff --git a/src/dpi.rs b/src/dpi.rs
index f86030433..3b4fb4ae1 100644
--- a/src/dpi.rs
+++ b/src/dpi.rs
@@ -98,47 +98,65 @@ pub trait Pixel: Copy + Into<f64> {
   }
 }
 
-impl Pixel for u8 {
-  fn from_f64(f: f64) -> Self {
-    f.round() as u8
-  }
-}
-impl Pixel for u16 {
-  fn from_f64(f: f64) -> Self {
-    f.round() as u16
-  }
-}
-impl Pixel for u32 {
-  fn from_f64(f: f64) -> Self {
-    f.round() as u32
-  }
-}
-impl Pixel for i8 {
-  fn from_f64(f: f64) -> Self {
-    f.round() as i8
-  }
-}
-impl Pixel for i16 {
-  fn from_f64(f: f64) -> Self {
-    f.round() as i16
-  }
-}
-impl Pixel for i32 {
-  fn from_f64(f: f64) -> Self {
-    f.round() as i32
-  }
+macro_rules! pixel_int_impl {
+  ($($t:ty),*) => {$(
+      impl Pixel for $t {
+          fn from_f64(f: f64) -> Self {
+              f.round() as $t
+          }
+      }
+  )*}
 }
+
+pixel_int_impl!(u8, u16, u32, i8, i16, i32);
+
 impl Pixel for f32 {
   fn from_f64(f: f64) -> Self {
     f as f32
   }
 }
+
 impl Pixel for f64 {
   fn from_f64(f: f64) -> Self {
     f
   }
 }
 
+macro_rules! from_impls {
+  ($t:ident, $a:ident, $(,)? ) => {
+    impl<P: Pixel> From<P> for $t<P> {
+      fn from($a: P) -> Self {
+        Self::new($a.cast())
+      }
+    }
+  };
+  ($t:ident, $a:ident, $b:ident$(,)? ) => {
+    impl<P: Pixel, X: Pixel> From<(X, X)> for $t<P> {
+      fn from(($a, $b): (X, X)) -> Self {
+        Self::new($a.cast(), $b.cast())
+      }
+    }
+
+    impl<P: Pixel, X: Pixel> From<$t<P>> for (X, X) {
+      fn from(p: $t<P>) -> Self {
+        (p.$a.cast(), p.$b.cast())
+      }
+    }
+
+    impl<P: Pixel, X: Pixel> From<[X; 2]> for $t<P> {
+      fn from([$a, $b]: [X; 2]) -> Self {
+        Self::new($a.cast(), $b.cast())
+      }
+    }
+
+    impl<P: Pixel, X: Pixel> From<$t<P>> for [X; 2] {
+      fn from(p: $t<P>) -> Self {
+        [p.$a.cast(), p.$b.cast()]
+      }
+    }
+  };
+}
+
 /// Checks that the scale factor is a normal positive `f64`.
 ///
 /// All functions that take a scale factor assert that this will return `true`. If you're sourcing scale factors from
@@ -149,369 +167,233 @@ pub fn validate_scale_factor(scale_factor: f64) -> bool {
   scale_factor.is_sign_positive() && scale_factor.is_normal()
 }
 
-/// A position represented in logical pixels.
-///
-/// The position is stored as floats, so please be careful. Casting floats to integers truncates the
-/// fractional part, which can cause noticeable issues. To help with that, an `Into<(i32, i32)>`
-/// implementation is provided which does the rounding for you.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct LogicalPosition<P> {
-  pub x: P,
-  pub y: P,
-}
+macro_rules! dpi_type {
+  (
+      $(let $a:ident;)*
 
-impl<P> LogicalPosition<P> {
-  #[inline]
-  pub const fn new(x: P, y: P) -> Self {
-    LogicalPosition { x, y }
-  }
-}
-
-impl<P: Pixel> LogicalPosition<P> {
-  #[inline]
-  pub fn from_physical<T: Into<PhysicalPosition<X>>, X: Pixel>(
-    physical: T,
-    scale_factor: f64,
-  ) -> Self {
-    physical.into().to_logical(scale_factor)
-  }
-
-  #[inline]
-  pub fn to_physical<X: Pixel>(&self, scale_factor: f64) -> PhysicalPosition<X> {
-    assert!(validate_scale_factor(scale_factor));
-    let x = self.x.into() * scale_factor;
-    let y = self.y.into() * scale_factor;
-    PhysicalPosition::new(x, y).cast()
-  }
-
-  #[inline]
-  pub fn cast<X: Pixel>(&self) -> LogicalPosition<X> {
-    LogicalPosition {
-      x: self.x.cast(),
-      y: self.y.cast(),
-    }
-  }
-}
-
-impl<P: Pixel, X: Pixel> From<(X, X)> for LogicalPosition<P> {
-  fn from((x, y): (X, X)) -> LogicalPosition<P> {
-    LogicalPosition::new(x.cast(), y.cast())
-  }
-}
-
-impl<P: Pixel, X: Pixel> Into<(X, X)> for LogicalPosition<P> {
-  fn into(self) -> (X, X) {
-    (self.x.cast(), self.y.cast())
-  }
-}
-
-impl<P: Pixel, X: Pixel> From<[X; 2]> for LogicalPosition<P> {
-  fn from([x, y]: [X; 2]) -> LogicalPosition<P> {
-    LogicalPosition::new(x.cast(), y.cast())
-  }
-}
-
-impl<P: Pixel, X: Pixel> Into<[X; 2]> for LogicalPosition<P> {
-  fn into(self) -> [X; 2] {
-    [self.x.cast(), self.y.cast()]
-  }
-}
-
-/// A position represented in physical pixels.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct PhysicalPosition<P> {
-  pub x: P,
-  pub y: P,
-}
-
-impl<P> PhysicalPosition<P> {
-  #[inline]
-  pub const fn new(x: P, y: P) -> Self {
-    PhysicalPosition { x, y }
-  }
-}
-
-impl<P: Pixel> PhysicalPosition<P> {
-  #[inline]
-  pub fn from_logical<T: Into<LogicalPosition<X>>, X: Pixel>(
-    logical: T,
-    scale_factor: f64,
-  ) -> Self {
-    logical.into().to_physical(scale_factor)
-  }
-
-  #[inline]
-  pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> LogicalPosition<X> {
-    assert!(validate_scale_factor(scale_factor));
-    let x = self.x.into() / scale_factor;
-    let y = self.y.into() / scale_factor;
-    LogicalPosition::new(x, y).cast()
-  }
-
-  #[inline]
-  pub fn cast<X: Pixel>(&self) -> PhysicalPosition<X> {
-    PhysicalPosition {
-      x: self.x.cast(),
-      y: self.y.cast(),
-    }
-  }
-}
+      $(#[$logical_meta:meta])*
+      pub struct $LogicalType:ident;
+      $(#[$physical_meta:meta])*
+      pub struct $PhysicalType:ident;
+      $(#[$unified_meta:meta])*
+      pub enum $UnifiedType:ident {
+          Physical($unified_physical:ty),
+          Logical($unified_logical:ty),
+      }
+  ) => {
+      $(#[$logical_meta])*
+      #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, PartialOrd, Ord)]
+      #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+      pub struct $LogicalType<P> {
+          $(pub $a: P,)*
+      }
 
-impl<P: Pixel, X: Pixel> From<(X, X)> for PhysicalPosition<P> {
-  fn from((x, y): (X, X)) -> PhysicalPosition<P> {
-    PhysicalPosition::new(x.cast(), y.cast())
-  }
-}
+      impl<P> $LogicalType<P> {
+          #[inline]
+          pub const fn new($($a: P,)*) -> Self {
+              $LogicalType { $($a,)* }
+          }
+      }
 
-impl<P: Pixel, X: Pixel> Into<(X, X)> for PhysicalPosition<P> {
-  fn into(self) -> (X, X) {
-    (self.x.cast(), self.y.cast())
-  }
-}
+      impl<P: Pixel> $LogicalType<P> {
+          #[inline]
+          pub fn from_physical<T: Into<$PhysicalType<X>>, X: Pixel>(
+              physical: T,
+              scale_factor: f64,
+          ) -> Self {
+              physical.into().to_logical(scale_factor)
+          }
+
+          #[inline]
+          pub fn to_physical<X: Pixel>(&self, scale_factor: f64) -> $PhysicalType<X> {
+              assert!(validate_scale_factor(scale_factor));
+              $(let $a = self.$a.into() * scale_factor;)*
+              $PhysicalType::new($($a,)*).cast()
+          }
+
+          #[inline]
+          pub fn cast<X: Pixel>(&self) -> $LogicalType<X> {
+              $LogicalType {
+                  $($a: self.$a.cast(),)*
+              }
+          }
+      }
 
-impl<P: Pixel, X: Pixel> From<[X; 2]> for PhysicalPosition<P> {
-  fn from([x, y]: [X; 2]) -> PhysicalPosition<P> {
-    PhysicalPosition::new(x.cast(), y.cast())
-  }
-}
+      from_impls!($LogicalType, $($a,)*);
 
-impl<P: Pixel, X: Pixel> Into<[X; 2]> for PhysicalPosition<P> {
-  fn into(self) -> [X; 2] {
-    [self.x.cast(), self.y.cast()]
-  }
-}
+      $(#[$physical_meta])*
+      #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, PartialOrd, Ord)]
+      #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+      pub struct $PhysicalType<P> {
+        $(pub $a: P,)*
+      }
 
-/// A size represented in logical pixels.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct LogicalSize<P> {
-  pub width: P,
-  pub height: P,
-}
+      impl<P> $PhysicalType<P> {
+          #[inline]
+          pub const fn new($($a: P,)*) -> Self {
+              $PhysicalType { $($a,)* }
+          }
+      }
 
-impl<P> LogicalSize<P> {
-  #[inline]
-  pub const fn new(width: P, height: P) -> Self {
-    LogicalSize { width, height }
-  }
-}
+      impl<P: Pixel> $PhysicalType<P> {
+          #[inline]
+          pub fn from_logical<T: Into<$LogicalType<X>>, X: Pixel>(
+              logical: T,
+              scale_factor: f64,
+          ) -> Self {
+              logical.into().to_physical(scale_factor)
+          }
+
+          #[inline]
+          pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> $LogicalType<X> {
+              assert!(validate_scale_factor(scale_factor));
+              $(let $a = self.$a.into() / scale_factor;)*
+              $LogicalType::new($($a,)*).cast()
+          }
+
+          #[inline]
+          pub fn cast<X: Pixel>(&self) -> $PhysicalType<X> {
+              $PhysicalType {
+                  $($a: self.$a.cast(),)*
+              }
+          }
+      }
 
-impl<P: Pixel> LogicalSize<P> {
-  #[inline]
-  pub fn from_physical<T: Into<PhysicalSize<X>>, X: Pixel>(physical: T, scale_factor: f64) -> Self {
-    physical.into().to_logical(scale_factor)
-  }
+      from_impls!($PhysicalType, $($a,)*);
 
-  #[inline]
-  pub fn to_physical<X: Pixel>(&self, scale_factor: f64) -> PhysicalSize<X> {
-    assert!(validate_scale_factor(scale_factor));
-    let width = self.width.into() * scale_factor;
-    let height = self.height.into() * scale_factor;
-    PhysicalSize::new(width, height).cast()
-  }
+      $(#[$unified_meta])*
+      #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
+      #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+      pub enum $UnifiedType {
+          Physical($unified_physical),
+          Logical($unified_logical),
+      }
 
-  #[inline]
-  pub fn cast<X: Pixel>(&self) -> LogicalSize<X> {
-    LogicalSize {
-      width: self.width.cast(),
-      height: self.height.cast(),
-    }
-  }
-}
+      impl $UnifiedType {
+          pub fn new<S: Into<$UnifiedType>>(val: S) -> $UnifiedType {
+              val.into()
+          }
+
+          pub fn to_logical<P: Pixel>(&self, scale_factor: f64) -> $LogicalType<P> {
+              match *self {
+                  $UnifiedType::Physical(val) => val.to_logical(scale_factor),
+                  $UnifiedType::Logical(val) => val.cast(),
+              }
+          }
+
+          pub fn to_physical<P: Pixel>(&self, scale_factor: f64) -> $PhysicalType<P> {
+              match *self {
+                  $UnifiedType::Physical(val) => val.cast(),
+                  $UnifiedType::Logical(val) => val.to_physical(scale_factor),
+              }
+          }
+
+          $(pub fn $a(&self) -> PixelUnit {
+            match *self {
+              $UnifiedType::Physical(any) => PixelUnit::Physical(any.$a.into()),
+              $UnifiedType::Logical(any) => PixelUnit::Logical(any.$a.into()),
+            }
+          })*
+      }
 
-impl<P: Pixel, X: Pixel> From<(X, X)> for LogicalSize<P> {
-  fn from((x, y): (X, X)) -> LogicalSize<P> {
-    LogicalSize::new(x.cast(), y.cast())
-  }
-}
+      impl<P: Pixel> From<$PhysicalType<P>> for $UnifiedType {
+          #[inline]
+          fn from(val: $PhysicalType<P>) -> $UnifiedType {
+              $UnifiedType::Physical(val.cast())
+          }
+      }
 
-impl<P: Pixel, X: Pixel> Into<(X, X)> for LogicalSize<P> {
-  fn into(self: LogicalSize<P>) -> (X, X) {
-    (self.width.cast(), self.height.cast())
-  }
+      impl<P: Pixel> From<$LogicalType<P>> for $UnifiedType {
+          #[inline]
+          fn from(val: $LogicalType<P>) -> $UnifiedType {
+              $UnifiedType::Logical(val.cast())
+          }
+      }
+  };
 }
 
-impl<P: Pixel, X: Pixel> From<[X; 2]> for LogicalSize<P> {
-  fn from([x, y]: [X; 2]) -> LogicalSize<P> {
-    LogicalSize::new(x.cast(), y.cast())
-  }
-}
+dpi_type! {
+  let value;
 
-impl<P: Pixel, X: Pixel> Into<[X; 2]> for LogicalSize<P> {
-  fn into(self) -> [X; 2] {
-    [self.width.cast(), self.height.cast()]
+  /// A logical pixel.
+  pub struct LogicalPixel;
+  /// A physical pixel.
+  pub struct PhysicalPixel;
+  /// A pixel that's either physical or logical.
+  pub enum PixelUnit {
+      Physical(PhysicalPixel<i32>),
+      Logical(LogicalPixel<f64>),
   }
 }
 
-/// A size represented in physical pixels.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct PhysicalSize<P> {
-  pub width: P,
-  pub height: P,
+impl PixelUnit {
+  /// Represents a minimum logical unit of `0`
+  pub const MIN: PixelUnit = PixelUnit::Logical(LogicalPixel::new(0.0));
+  /// Represents a maximum logical unit that is equal to [`f64::MAX`]
+  pub const MAX: PixelUnit = PixelUnit::Logical(LogicalPixel::new(f64::MAX));
 }
 
-impl<P> PhysicalSize<P> {
-  #[inline]
-  pub const fn new(width: P, height: P) -> Self {
-    PhysicalSize { width, height }
+impl From<u32> for PhysicalPixel<i32> {
+  fn from(value: u32) -> Self {
+    Self::new(value.cast())
   }
 }
 
-impl<P: Pixel> PhysicalSize<P> {
-  #[inline]
-  pub fn from_logical<T: Into<LogicalSize<X>>, X: Pixel>(logical: T, scale_factor: f64) -> Self {
-    logical.into().to_physical(scale_factor)
-  }
+dpi_type! {
+  let x;
+  let y;
 
-  #[inline]
-  pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> LogicalSize<X> {
-    assert!(validate_scale_factor(scale_factor));
-    let width = self.width.into() / scale_factor;
-    let height = self.height.into() / scale_factor;
-    LogicalSize::new(width, height).cast()
-  }
-
-  #[inline]
-  pub fn cast<X: Pixel>(&self) -> PhysicalSize<X> {
-    PhysicalSize {
-      width: self.width.cast(),
-      height: self.height.cast(),
-    }
+  /// A position represented in logical pixels.
+  ///
+  /// The position is stored as floats, so please be careful. Casting floats to integers truncates the
+  /// fractional part, which can cause noticable issues. To help with that, an `Into<(i32, i32)>`
+  /// implementation is provided which does the rounding for you.
+  pub struct LogicalPosition;
+  /// A position represented in physical pixels.
+  pub struct PhysicalPosition;
+  /// A position that's either physical or logical.
+  pub enum Position {
+      Physical(PhysicalPosition<i32>),
+      Logical(LogicalPosition<f64>),
   }
 }
 
-impl<P: Pixel, X: Pixel> From<(X, X)> for PhysicalSize<P> {
-  fn from((x, y): (X, X)) -> PhysicalSize<P> {
-    PhysicalSize::new(x.cast(), y.cast())
-  }
-}
+dpi_type! {
+  let width;
+  let height;
 
-impl<P: Pixel, X: Pixel> Into<(X, X)> for PhysicalSize<P> {
-  fn into(self) -> (X, X) {
-    (self.width.cast(), self.height.cast())
+  /// A size represented in logical pixels.
+  pub struct LogicalSize;
+  /// A size represented in physical pixels.
+  pub struct PhysicalSize;
+  /// A size that's either physical or logical.
+  pub enum Size {
+      Physical(PhysicalSize<u32>),
+      Logical(LogicalSize<f64>),
   }
 }
 
-impl<P: Pixel, X: Pixel> From<[X; 2]> for PhysicalSize<P> {
-  fn from([x, y]: [X; 2]) -> PhysicalSize<P> {
-    PhysicalSize::new(x.cast(), y.cast())
-  }
-}
-
-impl<P: Pixel, X: Pixel> Into<[X; 2]> for PhysicalSize<P> {
-  fn into(self) -> [X; 2] {
-    [self.width.cast(), self.height.cast()]
-  }
-}
-
-/// A size that's either physical or logical.
-#[non_exhaustive]
-#[derive(Debug, Copy, Clone, PartialEq)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub enum Size {
-  Physical(PhysicalSize<u32>),
-  Logical(LogicalSize<f64>),
-}
-
 impl Size {
-  pub fn new<S: Into<Size>>(size: S) -> Size {
-    size.into()
-  }
-
-  pub fn to_logical<P: Pixel>(&self, scale_factor: f64) -> LogicalSize<P> {
-    match *self {
-      Size::Physical(size) => size.to_logical(scale_factor),
-      Size::Logical(size) => size.cast(),
-    }
-  }
-
-  pub fn to_physical<P: Pixel>(&self, scale_factor: f64) -> PhysicalSize<P> {
-    match *self {
-      Size::Physical(size) => size.cast(),
-      Size::Logical(size) => size.to_physical(scale_factor),
-    }
-  }
-
-  pub fn clamp<S: Into<Size>>(input: S, min: S, max: S, scale_factor: f64) -> Size {
-    let (input, min, max) = (
-      input.into().to_physical::<f64>(scale_factor),
+  pub fn clamp<S: Into<Size>>(desired_size: S, min: S, max: S, scale_factor: f64) -> Size {
+    let (desired_size, min, max) = (
+      desired_size.into().to_physical::<f64>(scale_factor),
       min.into().to_physical::<f64>(scale_factor),
       max.into().to_physical::<f64>(scale_factor),
     );
 
-    let clamp = |input: f64, min: f64, max: f64| {
-      if input < min {
+    let clamp = |desired_size: f64, min: f64, max: f64| {
+      if desired_size < min {
         min
-      } else if input > max {
+      } else if desired_size > max {
         max
       } else {
-        input
+        desired_size
       }
     };
 
-    let width = clamp(input.width, min.width, max.width);
-    let height = clamp(input.height, min.height, max.height);
+    let width = clamp(desired_size.width, min.width, max.width);
+    let height = clamp(desired_size.height, min.height, max.height);
 
     PhysicalSize::new(width, height).into()
   }
 }
-
-impl<P: Pixel> From<PhysicalSize<P>> for Size {
-  #[inline]
-  fn from(size: PhysicalSize<P>) -> Size {
-    Size::Physical(size.cast())
-  }
-}
-
-impl<P: Pixel> From<LogicalSize<P>> for Size {
-  #[inline]
-  fn from(size: LogicalSize<P>) -> Size {
-    Size::Logical(size.cast())
-  }
-}
-
-/// A position that's either physical or logical.
-#[non_exhaustive]
-#[derive(Debug, Copy, Clone, PartialEq)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub enum Position {
-  Physical(PhysicalPosition<i32>),
-  Logical(LogicalPosition<f64>),
-}
-
-impl Position {
-  pub fn new<S: Into<Position>>(position: S) -> Position {
-    position.into()
-  }
-
-  pub fn to_logical<P: Pixel>(&self, scale_factor: f64) -> LogicalPosition<P> {
-    match *self {
-      Position::Physical(position) => position.to_logical(scale_factor),
-      Position::Logical(position) => position.cast(),
-    }
-  }
-
-  pub fn to_physical<P: Pixel>(&self, scale_factor: f64) -> PhysicalPosition<P> {
-    match *self {
-      Position::Physical(position) => position.cast(),
-      Position::Logical(position) => position.to_physical(scale_factor),
-    }
-  }
-}
-
-impl<P: Pixel> From<PhysicalPosition<P>> for Position {
-  #[inline]
-  fn from(position: PhysicalPosition<P>) -> Position {
-    Position::Physical(position.cast())
-  }
-}
-
-impl<P: Pixel> From<LogicalPosition<P>> for Position {
-  #[inline]
-  fn from(position: LogicalPosition<P>) -> Position {
-    Position::Logical(position.cast())
-  }
-}
diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs
index fc6b5f849..62fe5a186 100644
--- a/src/platform_impl/android/mod.rs
+++ b/src/platform_impl/android/mod.rs
@@ -12,7 +12,7 @@ use crate::{
   keyboard::{Key, KeyCode, KeyLocation, NativeKeyCode},
   menu::{CustomMenuItem, MenuId, MenuItem, MenuType},
   monitor,
-  window::{self, Theme},
+  window::{self, Theme, WindowSizeConstraints},
 };
 use ndk::{
   configuration::Configuration,
@@ -602,8 +602,8 @@ impl Window {
   }
 
   pub fn set_min_inner_size(&self, _: Option<Size>) {}
-
   pub fn set_max_inner_size(&self, _: Option<Size>) {}
+  pub fn set_inner_size_constraints(&self, _: WindowSizeConstraints) {}
 
   pub fn set_title(&self, _title: &str) {}
   pub fn title(&self) -> String {
diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs
index 736ae592f..ebbf28312 100644
--- a/src/platform_impl/ios/window.rs
+++ b/src/platform_impl/ios/window.rs
@@ -28,6 +28,7 @@ use crate::{
   },
   window::{
     CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes, WindowId as RootWindowId,
+    WindowSizeConstraints,
   },
 };
 
@@ -169,13 +170,15 @@ impl Inner {
     warn!("not clear what `Window::set_inner_size` means on iOS");
   }
 
-  pub fn set_min_inner_size(&self, _dimensions: Option<Size>) {
+  pub fn set_min_inner_size(&self, _: Option<Size>) {
     warn!("`Window::set_min_inner_size` is ignored on iOS")
   }
-
-  pub fn set_max_inner_size(&self, _dimensions: Option<Size>) {
+  pub fn set_max_inner_size(&self, _: Option<Size>) {
     warn!("`Window::set_max_inner_size` is ignored on iOS")
   }
+  pub fn set_inner_size_constraints(&self, _: WindowSizeConstraints) {
+    warn!("`Window::set_inner_size_constraints` is ignored on iOS")
+  }
 
   pub fn set_resizable(&self, _resizable: bool) {
     warn!("`Window::set_resizable` is ignored on iOS")
@@ -461,12 +464,6 @@ impl Window {
     window_attributes: WindowAttributes,
     platform_attributes: PlatformSpecificWindowBuilderAttributes,
   ) -> Result<Window, RootOsError> {
-    if let Some(_) = window_attributes.min_inner_size {
-      warn!("`WindowAttributes::min_inner_size` is ignored on iOS");
-    }
-    if let Some(_) = window_attributes.max_inner_size {
-      warn!("`WindowAttributes::max_inner_size` is ignored on iOS");
-    }
     if window_attributes.always_on_top {
       warn!("`WindowAttributes::always_on_top` is unsupported on iOS");
     }
diff --git a/src/platform_impl/linux/event_loop.rs b/src/platform_impl/linux/event_loop.rs
index 7af4cc9f4..f141b8112 100644
--- a/src/platform_impl/linux/event_loop.rs
+++ b/src/platform_impl/linux/event_loop.rs
@@ -223,35 +223,8 @@ impl<T: 'static> EventLoop<T> {
           WindowRequest::Title(title) => window.set_title(&title),
           WindowRequest::Position((x, y)) => window.move_(x, y),
           WindowRequest::Size((w, h)) => window.resize(w, h),
-          WindowRequest::SizeConstraint { min, max } => {
-            let geom_mask = min
-              .map(|_| gdk::WindowHints::MIN_SIZE)
-              .unwrap_or(gdk::WindowHints::empty())
-              | max
-                .map(|_| gdk::WindowHints::MAX_SIZE)
-                .unwrap_or(gdk::WindowHints::empty());
-
-            let (min_width, min_height) = min.map(Into::into).unwrap_or_default();
-            let (max_width, max_height) = max.map(Into::into).unwrap_or_default();
-
-            let picky_none: Option<&gtk::Window> = None;
-            window.set_geometry_hints(
-              picky_none,
-              Some(&gdk::Geometry::new(
-                min_width,
-                min_height,
-                max_width,
-                max_height,
-                0,
-                0,
-                0,
-                0,
-                0f64,
-                0f64,
-                gdk::Gravity::Center,
-              )),
-              geom_mask,
-            )
+          WindowRequest::SizeConstraints(constraints) => {
+            util::set_size_constraints(&window, constraints);
           }
           WindowRequest::Visible(visible) => {
             if visible {
diff --git a/src/platform_impl/linux/util.rs b/src/platform_impl/linux/util.rs
index 180b130d7..a221e609b 100644
--- a/src/platform_impl/linux/util.rs
+++ b/src/platform_impl/linux/util.rs
@@ -1,8 +1,10 @@
 use gdk::Display;
+use gtk::traits::{GtkWindowExt, WidgetExt};
 
 use crate::{
-  dpi::{LogicalPosition, PhysicalPosition},
+  dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
   error::ExternalError,
+  window::WindowSizeConstraints,
 };
 
 #[inline]
@@ -28,6 +30,43 @@ pub fn cursor_position(is_wayland: bool) -> Result<PhysicalPosition<f64>, Extern
   }
 }
 
+pub fn set_size_constraints<W: GtkWindowExt + WidgetExt>(
+  window: &W,
+  constraints: WindowSizeConstraints,
+) {
+  let mut geom_mask = gdk::WindowHints::empty();
+  if constraints.has_min() {
+    geom_mask |= gdk::WindowHints::MIN_SIZE;
+  }
+  if constraints.has_max() {
+    geom_mask |= gdk::WindowHints::MAX_SIZE;
+  }
+
+  let scale_factor = window.scale_factor() as f64;
+
+  let min_size: LogicalSize<i32> = constraints.min_size_logical(scale_factor);
+  let max_size: LogicalSize<i32> = constraints.max_size_logical(scale_factor);
+
+  let picky_none: Option<&gtk::Window> = None;
+  window.set_geometry_hints(
+    picky_none,
+    Some(&gdk::Geometry::new(
+      min_size.width,
+      min_size.height,
+      max_size.width,
+      max_size.height,
+      0,
+      0,
+      0,
+      0,
+      0f64,
+      0f64,
+      gdk::Gravity::Center,
+    )),
+    geom_mask,
+  )
+}
+
 pub fn is_unity() -> bool {
   std::env::var("XDG_CURRENT_DESKTOP")
     .map(|d| {
diff --git a/src/platform_impl/linux/window.rs b/src/platform_impl/linux/window.rs
index 002d8a5c7..1c9b66ca0 100644
--- a/src/platform_impl/linux/window.rs
+++ b/src/platform_impl/linux/window.rs
@@ -28,7 +28,7 @@ use crate::{
   monitor::MonitorHandle as RootMonitorHandle,
   window::{
     CursorIcon, Fullscreen, ProgressBarState, Theme, UserAttentionType, WindowAttributes,
-    BORDERLESS_RESIZE_INSET,
+    WindowSizeConstraints, BORDERLESS_RESIZE_INSET,
   },
 };
 
@@ -69,8 +69,7 @@ pub struct Window {
   maximized: Rc<AtomicBool>,
   minimized: Rc<AtomicBool>,
   fullscreen: RefCell<Option<Fullscreen>>,
-  min_inner_size: RefCell<Option<Size>>,
-  max_inner_size: RefCell<Option<Size>>,
+  inner_size_constraints: RefCell<WindowSizeConstraints>,
   /// Draw event Sender
   draw_tx: crossbeam_channel::Sender<WindowId>,
 }
@@ -114,40 +113,7 @@ impl Window {
     window.set_deletable(attributes.closable);
 
     // Set Min/Max Size
-    let geom_mask = attributes
-      .min_inner_size
-      .map(|_| gdk::WindowHints::MIN_SIZE)
-      .unwrap_or(gdk::WindowHints::empty())
-      | attributes
-        .max_inner_size
-        .map(|_| gdk::WindowHints::MAX_SIZE)
-        .unwrap_or(gdk::WindowHints::empty());
-    let (min_width, min_height) = attributes
-      .min_inner_size
-      .map(|size| size.to_logical::<f64>(win_scale_factor as f64).into())
-      .unwrap_or_default();
-    let (max_width, max_height) = attributes
-      .max_inner_size
-      .map(|size| size.to_logical::<f64>(win_scale_factor as f64).into())
-      .unwrap_or_default();
-    let picky_none: Option<&gtk::Window> = None;
-    window.set_geometry_hints(
-      picky_none,
-      Some(&gdk::Geometry::new(
-        min_width,
-        min_height,
-        max_width,
-        max_height,
-        0,
-        0,
-        0,
-        0,
-        0f64,
-        0f64,
-        gdk::Gravity::Center,
-      )),
-      geom_mask,
-    );
+    util::set_size_constraints(&window, attributes.inner_size_constraints);
 
     // Set Position
     if let Some(position) = attributes.position {
@@ -313,12 +279,12 @@ impl Window {
     let maximized: Rc<AtomicBool> = Rc::new(w_max.into());
     let max_clone = maximized.clone();
     let minimized = Rc::new(AtomicBool::new(false));
-    let min_clone = minimized.clone();
+    let minimized_clone = minimized.clone();
 
     window.connect_window_state_event(move |_, event| {
       let state = event.new_window_state();
       max_clone.store(state.contains(WindowState::MAXIMIZED), Ordering::Release);
-      min_clone.store(state.contains(WindowState::ICONIFIED), Ordering::Release);
+      minimized_clone.store(state.contains(WindowState::ICONIFIED), Ordering::Release);
       Inhibit(false)
     });
 
@@ -361,8 +327,7 @@ impl Window {
       maximized,
       minimized,
       fullscreen: RefCell::new(attributes.fullscreen),
-      min_inner_size: RefCell::new(attributes.min_inner_size),
-      max_inner_size: RefCell::new(attributes.max_inner_size),
+      inner_size_constraints: RefCell::new(attributes.inner_size_constraints),
     };
 
     win.set_skip_taskbar(pl_attribs.skip_taskbar);
@@ -445,47 +410,32 @@ impl Window {
     .to_physical(self.scale_factor.load(Ordering::Acquire) as f64)
   }
 
-  pub fn set_min_inner_size<S: Into<Size>>(&self, min_size: Option<S>) {
-    *self.min_inner_size.borrow_mut() = min_size.map(Into::into);
-
-    let scale_factor = self.scale_factor();
-
-    let min = self
-      .min_inner_size
-      .borrow()
-      .map(|s| s.to_logical::<i32>(scale_factor));
-    let max = self
-      .max_inner_size
-      .borrow()
-      .map(|s| s.to_logical::<i32>(scale_factor));
-
-    if let Err(e) = self
-      .window_requests_tx
-      .send((self.window_id, WindowRequest::SizeConstraint { min, max }))
-    {
-      log::warn!("Fail to send min size request: {}", e);
+  fn set_size_constraints(&self) {
+    if let Err(e) = self.window_requests_tx.send((
+      self.window_id,
+      WindowRequest::SizeConstraints(*self.inner_size_constraints.borrow()),
+    )) {
+      log::warn!("Fail to send size constraint request: {}", e);
     }
   }
-  pub fn set_max_inner_size<S: Into<Size>>(&self, max_size: Option<S>) {
-    *self.max_inner_size.borrow_mut() = max_size.map(Into::into);
 
-    let scale_factor = self.scale_factor();
+  pub fn set_min_inner_size(&self, size: Option<Size>) {
+    let mut size_constraints = self.inner_size_constraints.borrow_mut();
+    size_constraints.min_width = size.map(|s| s.width());
+    size_constraints.min_height = size.map(|s| s.height());
+    self.set_size_constraints()
+  }
 
-    let min = self
-      .min_inner_size
-      .borrow()
-      .map(|s| s.to_logical::<i32>(scale_factor));
-    let max = self
-      .max_inner_size
-      .borrow()
-      .map(|s| s.to_logical::<i32>(scale_factor));
+  pub fn set_max_inner_size(&self, size: Option<Size>) {
+    let mut size_constraints = self.inner_size_constraints.borrow_mut();
+    size_constraints.max_width = size.map(|s| s.width());
+    size_constraints.max_height = size.map(|s| s.height());
+    self.set_size_constraints()
+  }
 
-    if let Err(e) = self
-      .window_requests_tx
-      .send((self.window_id, WindowRequest::SizeConstraint { min, max }))
-    {
-      log::warn!("Fail to send max size request: {}", e);
-    }
+  pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) {
+    *self.inner_size_constraints.borrow_mut() = constraints;
+    self.set_size_constraints()
   }
 
   pub fn set_title(&self, title: &str) {
@@ -892,10 +842,7 @@ pub enum WindowRequest {
   Title(String),
   Position((i32, i32)),
   Size((i32, i32)),
-  SizeConstraint {
-    min: Option<LogicalSize<i32>>,
-    max: Option<LogicalSize<i32>>,
-  },
+  SizeConstraints(WindowSizeConstraints),
   Visible(bool),
   Focus,
   Resizable(bool),
diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs
index 3db5044fd..8c000c16d 100644
--- a/src/platform_impl/macos/window.rs
+++ b/src/platform_impl/macos/window.rs
@@ -36,6 +36,7 @@ use crate::{
   },
   window::{
     CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes, WindowId as RootWindowId,
+    WindowSizeConstraints,
   },
 };
 use cocoa::{
@@ -162,13 +163,14 @@ fn create_window(
       None => {
         let screen = NSScreen::mainScreen(nil);
         let scale_factor = NSScreen::backingScaleFactor(screen) as f64;
-        let (width, height) = match attrs.inner_size {
-          Some(size) => {
-            let logical = size.to_logical(scale_factor);
-            (logical.width, logical.height)
-          }
-          None => (800.0, 600.0),
-        };
+        let desired_size = attrs
+          .inner_size
+          .unwrap_or_else(|| PhysicalSize::new(800, 600).into());
+        let (width, height): (f64, f64) = attrs
+          .inner_size_constraints
+          .clamp(desired_size, scale_factor)
+          .to_logical::<f64>(scale_factor)
+          .into();
         let (left, bottom) = match attrs.position {
           Some(position) => {
             let logical = util::window_position(position.to_logical(scale_factor));
@@ -495,14 +497,18 @@ impl UnownedWindow {
         ns_window.setBackgroundColor_(NSColor::clearColor(nil));
       }
 
-      win_attribs.min_inner_size.map(|dim| {
-        let logical_dim = dim.to_logical(scale_factor);
-        set_min_inner_size(*ns_window, logical_dim)
-      });
-      win_attribs.max_inner_size.map(|dim| {
-        let logical_dim = dim.to_logical(scale_factor);
-        set_max_inner_size(*ns_window, logical_dim)
-      });
+      if win_attribs.inner_size_constraints.has_min() {
+        let min_size = win_attribs
+          .inner_size_constraints
+          .min_size_logical(scale_factor);
+        set_min_inner_size(*ns_window, min_size);
+      }
+      if win_attribs.inner_size_constraints.has_max() {
+        let max_size = win_attribs
+          .inner_size_constraints
+          .max_size_logical(scale_factor);
+        set_max_inner_size(*ns_window, max_size);
+      }
 
       // register for drag and drop operations.
       let () = msg_send![
@@ -697,27 +703,37 @@ impl UnownedWindow {
   }
 
   pub fn set_min_inner_size(&self, dimensions: Option<Size>) {
+    let dimensions = dimensions.unwrap_or(Logical(LogicalSize {
+      width: 0.0,
+      height: 0.0,
+    }));
+    let scale_factor = self.scale_factor();
     unsafe {
-      let dimensions = dimensions.unwrap_or(Logical(LogicalSize {
-        width: 0.0,
-        height: 0.0,
-      }));
-      let scale_factor = self.scale_factor();
       set_min_inner_size(*self.ns_window, dimensions.to_logical(scale_factor));
     }
   }
 
   pub fn set_max_inner_size(&self, dimensions: Option<Size>) {
+    let dimensions = dimensions.unwrap_or(Logical(LogicalSize {
+      width: std::f32::MAX as f64,
+      height: std::f32::MAX as f64,
+    }));
+    let scale_factor = self.scale_factor();
     unsafe {
-      let dimensions = dimensions.unwrap_or(Logical(LogicalSize {
-        width: std::f32::MAX as f64,
-        height: std::f32::MAX as f64,
-      }));
-      let scale_factor = self.scale_factor();
       set_max_inner_size(*self.ns_window, dimensions.to_logical(scale_factor));
     }
   }
 
+  pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) {
+    let scale_factor = self.scale_factor();
+    unsafe {
+      let min_size = constraints.min_size_logical(scale_factor);
+      set_min_inner_size(*self.ns_window, min_size);
+      let max_size = constraints.max_size_logical(scale_factor);
+      set_max_inner_size(*self.ns_window, max_size);
+    }
+  }
+
   #[inline]
   pub fn set_resizable(&self, resizable: bool) {
     let fullscreen = {
diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs
index 62217683a..596613c92 100644
--- a/src/platform_impl/windows/event_loop.rs
+++ b/src/platform_impl/windows/event_loop.rs
@@ -44,7 +44,7 @@ use windows::{
 
 use crate::{
   accelerator::AcceleratorId,
-  dpi::{PhysicalPosition, PhysicalSize},
+  dpi::{PhysicalPosition, PhysicalSize, PixelUnit},
   error::ExternalError,
   event::{DeviceEvent, Event, Force, RawKeyEvent, Touch, TouchPhase, WindowEvent},
   event_loop::{ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW},
@@ -1773,29 +1773,49 @@ unsafe fn public_window_callback_inner<T: 'static>(
       let mmi = lparam.0 as *mut MINMAXINFO;
 
       let window_state = subclass_input.window_state.lock();
-
-      if window_state.min_size.is_some() || window_state.max_size.is_some() {
-        let is_decorated = window_state
-          .window_flags()
-          .contains(WindowFlags::MARKER_DECORATIONS);
-        if let Some(min_size) = window_state.min_size {
-          let min_size = min_size.to_physical(window_state.scale_factor);
-          let (width, height): (u32, u32) =
-            util::adjust_size(window, min_size, is_decorated).into();
-          (*mmi).ptMinTrackSize = POINT {
-            x: width as i32,
-            y: height as i32,
-          };
-        }
-        if let Some(max_size) = window_state.max_size {
-          let max_size = max_size.to_physical(window_state.scale_factor);
-          let (width, height): (u32, u32) =
-            util::adjust_size(window, max_size, is_decorated).into();
-          (*mmi).ptMaxTrackSize = POINT {
-            x: width as i32,
-            y: height as i32,
-          };
-        }
+      let is_decorated = window_state
+        .window_flags()
+        .contains(WindowFlags::MARKER_DECORATIONS);
+
+      let size_constraints = window_state.size_constraints;
+
+      if size_constraints.has_min() {
+        let min_size = PhysicalSize::new(
+          size_constraints
+            .min_width
+            .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CXMINTRACK).into()))
+            .to_physical(window_state.scale_factor)
+            .value,
+          size_constraints
+            .min_height
+            .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CYMINTRACK).into()))
+            .to_physical(window_state.scale_factor)
+            .value,
+        );
+        let (width, height): (u32, u32) = util::adjust_size(window, min_size, is_decorated).into();
+        (*mmi).ptMinTrackSize = POINT {
+          x: width as i32,
+          y: height as i32,
+        };
+      }
+      if size_constraints.has_max() {
+        let max_size = PhysicalSize::new(
+          size_constraints
+            .max_width
+            .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CXMAXTRACK).into()))
+            .to_physical(window_state.scale_factor)
+            .value,
+          size_constraints
+            .max_height
+            .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CYMAXTRACK).into()))
+            .to_physical(window_state.scale_factor)
+            .value,
+        );
+        let (width, height): (u32, u32) = util::adjust_size(window, max_size, is_decorated).into();
+        (*mmi).ptMaxTrackSize = POINT {
+          x: width as i32,
+          y: height as i32,
+        };
       }
 
       result = ProcResult::Value(LRESULT(0));
@@ -1891,7 +1911,7 @@ unsafe fn public_window_callback_inner<T: 'static>(
         false => old_physical_inner_size,
       };
 
-      let _ = subclass_input.send_event(Event::WindowEvent {
+      subclass_input.send_event(Event::WindowEvent {
         window_id: RootWindowId(WindowId(window.0)),
         event: ScaleFactorChanged {
           scale_factor: new_scale_factor,
@@ -2028,7 +2048,7 @@ unsafe fn public_window_callback_inner<T: 'static>(
 
       let preferred_theme = subclass_input.window_state.lock().preferred_theme;
 
-      if preferred_theme == None {
+      if preferred_theme.is_none() {
         let new_theme = try_theme(window, preferred_theme);
         let mut window_state = subclass_input.window_state.lock();
 
diff --git a/src/platform_impl/windows/system_tray.rs b/src/platform_impl/windows/system_tray.rs
index f44c5184c..aa2a43d77 100644
--- a/src/platform_impl/windows/system_tray.rs
+++ b/src/platform_impl/windows/system_tray.rs
@@ -114,7 +114,7 @@ impl SystemTrayBuilder {
         )));
       }
 
-      let system_tray = SystemTray { hwnd: hwnd.clone() };
+      let system_tray = SystemTray { hwnd };
 
       // system_tray event handler
       let event_loop_runner = window_target.p.runner_shared.clone();
diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs
index a56eafa2f..87b3e6b0c 100644
--- a/src/platform_impl/windows/window.rs
+++ b/src/platform_impl/windows/window.rs
@@ -54,7 +54,7 @@ use crate::{
   },
   window::{
     CursorIcon, Fullscreen, ProgressBarState, ProgressState, Theme, UserAttentionType,
-    WindowAttributes, WindowId as RootWindowId, BORDERLESS_RESIZE_INSET,
+    WindowAttributes, WindowId as RootWindowId, WindowSizeConstraints, BORDERLESS_RESIZE_INSET,
   },
 };
 
@@ -285,7 +285,11 @@ impl Window {
 
   #[inline]
   pub fn set_min_inner_size(&self, size: Option<Size>) {
-    self.window_state.lock().min_size = size;
+    {
+      let mut window_state = self.window_state.lock();
+      window_state.size_constraints.min_width = size.map(|s| s.width());
+      window_state.size_constraints.min_height = size.map(|s| s.height());
+    }
     // Make windows re-check the window size bounds.
     let size = self.inner_size();
     self.set_inner_size(size.into());
@@ -293,7 +297,19 @@ impl Window {
 
   #[inline]
   pub fn set_max_inner_size(&self, size: Option<Size>) {
-    self.window_state.lock().max_size = size;
+    {
+      let mut window_state = self.window_state.lock();
+      window_state.size_constraints.max_width = size.map(|s| s.width());
+      window_state.size_constraints.max_height = size.map(|s| s.height());
+    }
+    // Make windows re-check the window size bounds.
+    let size = self.inner_size();
+    self.set_inner_size(size.into());
+  }
+
+  #[inline]
+  pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) {
+    self.window_state.lock().size_constraints = constraints;
     // Make windows re-check the window size bounds.
     let size = self.inner_size();
     self.set_inner_size(size.into());
@@ -1102,17 +1118,13 @@ unsafe fn init<T: 'static>(
     win.set_fullscreen(attributes.fullscreen);
     force_window_active(win.window.0);
   } else {
-    let size = attributes
+    let desired_size = attributes
       .inner_size
       .unwrap_or_else(|| PhysicalSize::new(800, 600).into());
-    let max_size = attributes
-      .max_inner_size
-      .unwrap_or_else(|| PhysicalSize::new(f64::MAX, f64::MAX).into());
-    let min_size = attributes
-      .min_inner_size
-      .unwrap_or_else(|| PhysicalSize::new(0, 0).into());
-    let clamped_size = Size::clamp(size, min_size, max_size, win.scale_factor());
-    win.set_inner_size(clamped_size);
+    let size = attributes
+      .inner_size_constraints
+      .clamp(desired_size, win.scale_factor());
+    win.set_inner_size(size);
 
     if attributes.maximized {
       // Need to set MAXIMIZED after setting `inner_size` as
diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs
index d42433368..579add95e 100644
--- a/src/platform_impl/windows/window_state.rs
+++ b/src/platform_impl/windows/window_state.rs
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use crate::{
-  dpi::{PhysicalPosition, Size},
+  dpi::PhysicalPosition,
   icon::Icon,
   keyboard::ModifiersState,
   platform_impl::platform::{event_loop, minimal_ime::MinimalIme, util},
-  window::{CursorIcon, Fullscreen, Theme, WindowAttributes},
+  window::{CursorIcon, Fullscreen, Theme, WindowAttributes, WindowSizeConstraints},
 };
 use parking_lot::MutexGuard;
 use std::io;
@@ -22,8 +22,7 @@ pub struct WindowState {
   pub mouse: MouseProperties,
 
   /// Used by `WM_GETMINMAXINFO`.
-  pub min_size: Option<Size>,
-  pub max_size: Option<Size>,
+  pub size_constraints: WindowSizeConstraints,
 
   pub window_icon: Option<Icon>,
   pub taskbar_icon: Option<Icon>,
@@ -128,8 +127,7 @@ impl WindowState {
         last_position: None,
       },
 
-      min_size: attributes.min_inner_size,
-      max_size: attributes.max_inner_size,
+      size_constraints: attributes.inner_size_constraints,
 
       window_icon: attributes.window_icon.clone(),
       taskbar_icon,
diff --git a/src/window.rs b/src/window.rs
index b44d33587..5b67da73e 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -8,7 +8,7 @@ use std::fmt;
 use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle};
 
 use crate::{
-  dpi::{PhysicalPosition, PhysicalSize, Position, Size},
+  dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Pixel, PixelUnit, Position, Size},
   error::{ExternalError, NotSupportedError, OsError},
   event_loop::EventLoopWindowTarget,
   menu::MenuBar,
@@ -136,15 +136,8 @@ pub struct WindowAttributes {
   /// The default is `None`.
   pub inner_size: Option<Size>,
 
-  /// The minimum dimensions a window can be, If this is `None`, the window will have no minimum dimensions (aside from reserved).
-  ///
-  /// The default is `None`.
-  pub min_inner_size: Option<Size>,
-
-  /// The maximum dimensions a window can be, If this is `None`, the maximum will have no maximum or will be set to the primary monitor's dimensions by the platform.
-  ///
-  /// The default is `None`.
-  pub max_inner_size: Option<Size>,
+  /// The window size constraints
+  pub inner_size_constraints: WindowSizeConstraints,
 
   /// The desired position of the window. If this is `None`, some platform-specific position
   /// will be chosen.
@@ -278,8 +271,7 @@ impl Default for WindowAttributes {
   fn default() -> WindowAttributes {
     WindowAttributes {
       inner_size: None,
-      min_inner_size: None,
-      max_inner_size: None,
+      inner_size_constraints: Default::default(),
       position: None,
       resizable: true,
       minimizable: true,
@@ -328,7 +320,9 @@ impl WindowBuilder {
   /// [`Window::set_min_inner_size`]: crate::window::Window::set_min_inner_size
   #[inline]
   pub fn with_min_inner_size<S: Into<Size>>(mut self, min_size: S) -> Self {
-    self.window.min_inner_size = Some(min_size.into());
+    let size = min_size.into();
+    self.window.inner_size_constraints.min_width = Some(size.width());
+    self.window.inner_size_constraints.min_height = Some(size.height());
     self
   }
 
@@ -339,7 +333,20 @@ impl WindowBuilder {
   /// [`Window::set_max_inner_size`]: crate::window::Window::set_max_inner_size
   #[inline]
   pub fn with_max_inner_size<S: Into<Size>>(mut self, max_size: S) -> Self {
-    self.window.max_inner_size = Some(max_size.into());
+    let size = max_size.into();
+    self.window.inner_size_constraints.max_width = Some(size.width());
+    self.window.inner_size_constraints.max_height = Some(size.height());
+    self
+  }
+
+  /// Sets inner size constraints for the window.
+  ///
+  /// See [`Window::set_inner_size_constraints`] for details.
+  ///
+  /// [`Window::set_inner_size_constraints`]: crate::window::Window::set_inner_size_constraints
+  #[inline]
+  pub fn with_inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self {
+    self.window.inner_size_constraints = constraints;
     self
   }
 
@@ -742,6 +749,16 @@ impl Window {
   pub fn set_max_inner_size<S: Into<Size>>(&self, max_size: Option<S>) {
     self.window.set_max_inner_size(max_size.map(|s| s.into()))
   }
+
+  /// Sets inner size constraints for the window.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **iOS / Android:** Unsupported.
+  #[inline]
+  pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) {
+    self.window.set_inner_size_constraints(constraints)
+  }
 }
 
 /// Misc. attribute functions.
@@ -1436,6 +1453,123 @@ impl Default for UserAttentionType {
   }
 }
 
+/// Window size constraints
+#[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Default)]
+pub struct WindowSizeConstraints {
+  /// The minimum width a window can be, If this is `None`, the window will have no minimum width (aside from reserved).
+  ///
+  /// The default is `None`.
+  pub min_width: Option<PixelUnit>,
+  /// The minimum height a window can be, If this is `None`, the window will have no minimum height (aside from reserved).
+  ///
+  /// The default is `None`.
+  pub min_height: Option<PixelUnit>,
+  /// The maximum width a window can be, If this is `None`, the window will have no maximum width (aside from reserved).
+  ///
+  /// The default is `None`.
+  pub max_width: Option<PixelUnit>,
+  /// The maximum height a window can be, If this is `None`, the window will have no maximum height (aside from reserved).
+  ///
+  /// The default is `None`.
+  pub max_height: Option<PixelUnit>,
+}
+
+impl WindowSizeConstraints {
+  pub fn new(
+    min_width: Option<PixelUnit>,
+    min_height: Option<PixelUnit>,
+    max_width: Option<PixelUnit>,
+    max_height: Option<PixelUnit>,
+  ) -> Self {
+    Self {
+      min_width,
+      min_height,
+      max_width,
+      max_height,
+    }
+  }
+
+  /// Returns true if `min_width` or `min_height` is set.
+  pub fn has_min(&self) -> bool {
+    self.min_width.is_some() || self.min_height.is_some()
+  }
+  /// Returns true if `max_width` or `max_height` is set.
+  pub fn has_max(&self) -> bool {
+    self.max_width.is_some() || self.max_height.is_some()
+  }
+
+  /// Returns a physical size that represents the minimum constraints set and fallbacks to [`PixelUnit::MIN`] for unset values
+  pub fn min_size_physical<T: Pixel>(&self, scale_factor: f64) -> PhysicalSize<T> {
+    PhysicalSize::new(
+      self
+        .min_width
+        .unwrap_or(PixelUnit::MIN)
+        .to_physical(scale_factor)
+        .value,
+      self
+        .min_height
+        .unwrap_or(PixelUnit::MIN)
+        .to_physical(scale_factor)
+        .value,
+    )
+  }
+
+  /// Returns a logical size that represents the minimum constraints set and fallbacks to [`PixelUnit::MIN`] for unset values
+  pub fn min_size_logical<T: Pixel>(&self, scale_factor: f64) -> LogicalSize<T> {
+    LogicalSize::new(
+      self
+        .min_width
+        .unwrap_or(PixelUnit::MIN)
+        .to_logical(scale_factor)
+        .value,
+      self
+        .min_height
+        .unwrap_or(PixelUnit::MIN)
+        .to_logical(scale_factor)
+        .value,
+    )
+  }
+
+  /// Returns a physical size that represents the maximum constraints set and fallbacks to [`PixelUnit::MAX`] for unset values
+  pub fn max_size_physical<T: Pixel>(&self, scale_factor: f64) -> PhysicalSize<T> {
+    PhysicalSize::new(
+      self
+        .max_width
+        .unwrap_or(PixelUnit::MAX)
+        .to_physical(scale_factor)
+        .value,
+      self
+        .max_height
+        .unwrap_or(PixelUnit::MAX)
+        .to_physical(scale_factor)
+        .value,
+    )
+  }
+
+  /// Returns a logical size that represents the maximum constraints set and fallbacks to [`PixelUnit::MAX`] for unset values
+  pub fn max_size_logical<T: Pixel>(&self, scale_factor: f64) -> LogicalSize<T> {
+    LogicalSize::new(
+      self
+        .max_width
+        .unwrap_or(PixelUnit::MAX)
+        .to_logical(scale_factor)
+        .value,
+      self
+        .max_height
+        .unwrap_or(PixelUnit::MAX)
+        .to_logical(scale_factor)
+        .value,
+    )
+  }
+
+  /// Clamps the desired size based on the constraints set
+  pub fn clamp(&self, desired_size: Size, scale_factor: f64) -> Size {
+    let min_size: PhysicalSize<f64> = self.min_size_physical(scale_factor);
+    let max_size: PhysicalSize<f64> = self.max_size_physical(scale_factor);
+    Size::clamp(desired_size, min_size.into(), max_size.into(), scale_factor)
+  }
+}
+
 /// A constant used to determine how much inside the window, the resize handler should appear (only used in Linux(gtk) and Windows).
 /// You probably need to scale it by the scale_factor of the window.
 pub const BORDERLESS_RESIZE_INSET: i32 = 5;