diff --git a/framework-crates/objc2-core-foundation/src/geometry.rs b/framework-crates/objc2-core-foundation/src/geometry.rs index e93fab3f2..29836ff3a 100644 --- a/framework-crates/objc2-core-foundation/src/geometry.rs +++ b/framework-crates/objc2-core-foundation/src/geometry.rs @@ -246,7 +246,8 @@ impl CGRect { #[doc(alias = "CGRectZero")] pub const ZERO: Self = Self::new(CGPoint::ZERO, CGSize::ZERO); - /// Returns a rectangle with a positive width and height. + /// Returns a rectangle with a nonnegative width and height, moving the + /// origin to preserve the rectangle's extent. /// /// /// # Examples @@ -256,13 +257,39 @@ impl CGRect { /// let origin = CGPoint::new(1.0, 1.0); /// let size = CGSize::new(-5.0, -2.0); /// let rect = CGRect::new(origin, size); - /// assert_eq!(rect.standardize().size, CGSize::new(5.0, 2.0)); + /// assert_eq!( + /// rect.standardize(), + /// CGRect::new(CGPoint::new(-4.0, -1.0), CGSize::new(5.0, 2.0)), + /// ); /// ``` #[inline] #[doc(alias = "CGRectStandardize")] #[cfg(feature = "std")] // `abs` only available in core since Rust 1.85 pub fn standardize(self) -> Self { - Self::new(self.origin, self.size.abs()) + if self.origin.x == CGFloat::INFINITY || self.origin.y == CGFloat::INFINITY { + return Self::new( + CGPoint::new(CGFloat::INFINITY, CGFloat::INFINITY), + CGSize::ZERO, + ); + } + // Always add the offset, including signed zero, as CGRectStandardize does. + Self::new( + CGPoint::new( + self.origin.x + + if self.size.width <= 0.0 { + self.size.width + } else { + 0.0 + }, + self.origin.y + + if self.size.height <= 0.0 { + self.size.height + } else { + 0.0 + }, + ), + self.size.abs(), + ) } /// The smallest coordinate of the rectangle. @@ -301,7 +328,7 @@ impl CGRect { ) } - /// Returns whether a rectangle has zero width or height. + /// Returns whether a rectangle has zero width or height, or is null. /// /// /// # Examples @@ -316,9 +343,10 @@ impl CGRect { #[inline] #[doc(alias = "CGRectIsEmpty")] pub fn is_empty(self) -> bool { - !(self.size.width > 0.0 && self.size.height > 0.0) - // TODO: NaN handling? - // self.size.width <= 0.0 || self.size.height <= 0.0 + self.size.width == 0.0 + || self.size.height == 0.0 + || self.origin.x == CGFloat::INFINITY + || self.origin.y == CGFloat::INFINITY } // TODO: NSContainsRect / CGRectContainsRect diff --git a/framework-crates/objc2-core-graphics/Cargo.modified.toml b/framework-crates/objc2-core-graphics/Cargo.modified.toml new file mode 100644 index 000000000..ef0ef80f1 --- /dev/null +++ b/framework-crates/objc2-core-graphics/Cargo.modified.toml @@ -0,0 +1,2 @@ +[dev-dependencies] +objc2-core-foundation = { workspace = true, features = ["std"] } diff --git a/framework-crates/objc2-core-graphics/Cargo.toml b/framework-crates/objc2-core-graphics/Cargo.toml index 446355490..86fed9876 100644 --- a/framework-crates/objc2-core-graphics/Cargo.toml +++ b/framework-crates/objc2-core-graphics/Cargo.toml @@ -262,3 +262,6 @@ CGWindow = [ "objc2-core-foundation/CFDictionary", ] CGWindowLevel = [] + +[dev-dependencies] +objc2-core-foundation = { workspace = true, features = ["std"] } diff --git a/framework-crates/objc2-core-graphics/tests/geometry.rs b/framework-crates/objc2-core-graphics/tests/geometry.rs new file mode 100644 index 000000000..71584a75b --- /dev/null +++ b/framework-crates/objc2-core-graphics/tests/geometry.rs @@ -0,0 +1,48 @@ +#![cfg(feature = "CGGeometry")] +#![cfg(feature = "std")] +#![cfg(target_vendor = "apple")] + +use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize}; +use objc2_core_graphics::{CGRectIsEmpty, CGRectStandardize}; + +#[test] +fn rectangle_helpers_match_core_graphics() { + let values = [ + CGFloat::NEG_INFINITY, + -CGFloat::MAX, + -5.0, + -CGFloat::MIN_POSITIVE, + -0.0, + 0.0, + CGFloat::MIN_POSITIVE, + 5.0, + CGFloat::MAX, + CGFloat::INFINITY, + CGFloat::NAN, + ]; + for x in values { + for y in values { + for width in values { + for height in values { + let rect = CGRect::new(CGPoint::new(x, y), CGSize::new(width, height)); + assert_eq!(rect.is_empty(), CGRectIsEmpty(rect), "{rect:?}"); + let actual = rect.standardize(); + let expected = CGRectStandardize(rect); + for (actual, expected) in [ + (actual.origin.x, expected.origin.x), + (actual.origin.y, expected.origin.y), + (actual.size.width, expected.size.width), + (actual.size.height, expected.size.height), + ] { + // NaN has no equality relation, but must remain NaN. + assert!( + actual.to_bits() == expected.to_bits() + || (actual.is_nan() && expected.is_nan()), + "{rect:?}: expected {expected:?}, got {actual:?}", + ); + } + } + } + } + } +}