1use crate::{PxPosition, PxRect};
8
9#[derive(Clone, Copy, Debug, PartialEq)]
11pub enum ClipShape {
12 RoundedRectangle {
15 radii: [f32; 4],
17 exponents: [f32; 4],
19 },
20 Ellipse,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct ClipRegion {
27 pub position: PxPosition,
29 pub size: [f32; 2],
31 pub shape: ClipShape,
33}
34
35impl ClipRegion {
36 pub fn bounds(self) -> PxRect {
38 PxRect::enclosing_f32(self.position, self.size)
39 }
40
41 pub fn contains(self, position: PxPosition) -> bool {
43 let local = position - self.position;
44 if !(local.x >= 0.0 && local.y >= 0.0 && local.x < self.size[0] && local.y < self.size[1]) {
45 return false;
46 }
47 let half = [self.size[0] * 0.5, self.size[1] * 0.5];
48 let point = [local.x - half[0], local.y - half[1]];
49 match self.shape {
50 ClipShape::Ellipse => {
51 (point[0] / half[0]).powi(2) + (point[1] / half[1]).powi(2) <= 1.0
52 }
53 ClipShape::RoundedRectangle { radii, exponents } => {
54 let corner = match (point[0] < 0.0, point[1] < 0.0) {
55 (true, true) => 0,
56 (false, true) => 1,
57 (false, false) => 2,
58 (true, false) => 3,
59 };
60 let radius = finite_radius(radii[corner], half[0].min(half[1]));
61 if radius == 0.0 {
62 return true;
63 }
64 let exponent = finite_exponent(exponents[corner]);
65 let q = [
66 (point[0].abs() - half[0] + radius).max(0.0) / radius,
67 (point[1].abs() - half[1] + radius).max(0.0) / radius,
68 ];
69 q[0].powf(exponent) + q[1].powf(exponent) <= 1.0
70 }
71 }
72 }
73}
74
75pub(crate) fn finite_radius(radius: f32, max: f32) -> f32 {
76 if radius.is_finite() {
77 radius.clamp(0.0, max.max(0.0))
78 } else {
79 0.0
80 }
81}
82
83pub(crate) fn finite_exponent(exponent: f32) -> f32 {
84 if exponent.is_finite() {
85 exponent.clamp(1.0, 32.0)
86 } else {
87 2.0
88 }
89}
90
91pub fn ui_stencil_state() -> Option<wgpu::DepthStencilState> {
97 Some(wgpu::DepthStencilState {
98 format: wgpu::TextureFormat::Stencil8,
99 depth_write_enabled: None,
100 depth_compare: None,
101 stencil: wgpu::StencilState {
102 front: wgpu::StencilFaceState {
103 compare: wgpu::CompareFunction::Equal,
104 ..Default::default()
105 },
106 back: wgpu::StencilFaceState {
107 compare: wgpu::CompareFunction::Equal,
108 ..Default::default()
109 },
110 read_mask: 0xff,
111 write_mask: 0,
112 },
113 bias: Default::default(),
114 })
115}
116
117#[cfg(test)]
118mod tests {
119 use crate::Px;
120
121 use super::*;
122
123 #[test]
124 fn shape_hit_testing_matches_mask_geometry() {
125 let _runtime_session = crate::RuntimeSession::new();
126 let _runtime_scope = _runtime_session.enter();
127 let mut region = ClipRegion {
128 position: PxPosition::from_f32_xy(10.0, 20.0),
129 size: [100.0, 60.0],
130 shape: ClipShape::Ellipse,
131 };
132 assert!(region.contains(PxPosition::from_f32_xy(60.0, 50.0)));
133 assert!(!region.contains(PxPosition::from_f32_xy(11.0, 21.0)));
134 region.shape = ClipShape::RoundedRectangle {
135 radii: [20.0; 4],
136 exponents: [2.0; 4],
137 };
138 assert!(!region.contains(PxPosition::from_f32_xy(11.0, 21.0)));
139 assert!(region.contains(PxPosition::from_f32_xy(30.0, 21.0)));
140 assert!(!region.contains(PxPosition::from_f32_xy(109.0, 79.0)));
141 }
142
143 #[test]
144 fn invalid_radii_and_exponents_are_sanitized() {
145 let _runtime_session = crate::RuntimeSession::new();
146 let _runtime_scope = _runtime_session.enter();
147 assert_eq!(finite_radius(f32::NAN, 10.0), 0.0);
148 assert_eq!(finite_radius(100.0, 10.0), 10.0);
149 assert_eq!(finite_exponent(f32::INFINITY), 2.0);
150 assert_eq!(finite_exponent(-1.0), 1.0);
151 }
152
153 #[test]
154 fn fractional_masks_keep_geometry_and_round_scissors_outwards() {
155 let _runtime_session = crate::RuntimeSession::new();
156 let _runtime_scope = _runtime_session.enter();
157 let region = ClipRegion {
158 position: PxPosition::from_f32_xy(-0.25, 0.75),
159 size: [20.0, 10.0],
160 shape: ClipShape::RoundedRectangle {
161 radii: [0.0; 4],
162 exponents: [2.0; 4],
163 },
164 };
165 assert_eq!(region.bounds(), PxRect::new(Px(-1), Px(0), Px(21), Px(11)));
166 assert!(!region.contains(PxPosition::from_f32_xy(0.0, 0.5)));
167 assert!(region.contains(PxPosition::from_f32_xy(0.0, 0.8)));
168 assert!(!region.contains(PxPosition::from_f32_xy(19.8, 5.0)));
169 }
170}