1use crate::{Bounds, Edges, Pixels, Point, ResizeEdge, Tiling};
4
5#[derive(Debug, Copy, Clone, Eq, PartialEq)]
7pub struct ResizeRegion {
8 border_width: Pixels,
9 corner_size: Pixels,
10 enabled_edges: Edges<bool>,
11}
12
13impl ResizeRegion {
14 pub fn new(border_width: Pixels) -> Self {
17 Self {
18 border_width: sane(border_width),
19 corner_size: sane(border_width),
20 enabled_edges: Edges::all(true),
21 }
22 }
23
24 pub fn corner_size(mut self, corner_size: Pixels) -> Self {
26 self.corner_size = sane(corner_size);
27 self
28 }
29
30 pub fn enabled_edges(mut self, enabled_edges: Edges<bool>) -> Self {
32 self.enabled_edges = enabled_edges;
33 self
34 }
35
36 pub fn without_tiled_edges(mut self, tiling: Tiling) -> Self {
38 self.enabled_edges.top &= !tiling.top;
39 self.enabled_edges.right &= !tiling.right;
40 self.enabled_edges.bottom &= !tiling.bottom;
41 self.enabled_edges.left &= !tiling.left;
42 self
43 }
44
45 pub fn hit_test(&self, position: Point<Pixels>, bounds: Bounds<Pixels>) -> Option<ResizeEdge> {
48 let left = bounds.origin.x.0;
49 let top = bounds.origin.y.0;
50 let width = bounds.size.width.0;
51 let height = bounds.size.height.0;
52 let right = left + width;
53 let bottom = top + height;
54 let x = position.x.0;
55 let y = position.y.0;
56 let bw = self.border_width.0;
57 let corner = self.corner_size.0;
58
59 if !(left.is_finite()
60 && top.is_finite()
61 && width.is_finite()
62 && height.is_finite()
63 && x.is_finite()
64 && y.is_finite())
65 || width <= 0.0
66 || height <= 0.0
67 || bw <= 0.0
68 || !right.is_finite()
69 || !bottom.is_finite()
70 {
71 return None;
72 }
73
74 if x < left || x >= right || y < top || y >= bottom {
75 return None;
76 }
77
78 let top_hit = self.enabled_edges.top
79 && y >= top
80 && y < (top + bw).min(bottom)
81 && x >= left
82 && x < right;
83 let right_hit = self.enabled_edges.right
84 && x >= (right - bw).max(left)
85 && x < right
86 && y >= top
87 && y < bottom;
88 let bottom_hit = self.enabled_edges.bottom
89 && y >= (bottom - bw).max(top)
90 && y < bottom
91 && x >= left
92 && x < right;
93 let left_hit = self.enabled_edges.left
94 && x >= left
95 && x < (left + bw).min(right)
96 && y >= top
97 && y < bottom;
98
99 let near_left = x - left < right - x && x - left < corner;
100 let near_right = right - x <= x - left && right - x <= corner;
101 let near_top = y - top < bottom - y && y - top < corner;
102 let near_bottom = bottom - y <= y - top && bottom - y <= corner;
103 let in_left_strip = x - left < bw;
104 let in_right_strip = right - x <= bw;
105 let in_top_strip = y - top < bw;
106 let in_bottom_strip = bottom - y <= bw;
107 let corner_hit = if near_top
108 && near_left
109 && (in_top_strip || in_left_strip)
110 && self.enabled_edges.top
111 && self.enabled_edges.left
112 {
113 Some(ResizeEdge::TopLeft)
114 } else if near_top
115 && near_right
116 && (in_top_strip || in_right_strip)
117 && self.enabled_edges.top
118 && self.enabled_edges.right
119 {
120 Some(ResizeEdge::TopRight)
121 } else if near_bottom
122 && near_left
123 && (in_bottom_strip || in_left_strip)
124 && self.enabled_edges.bottom
125 && self.enabled_edges.left
126 {
127 Some(ResizeEdge::BottomLeft)
128 } else if near_bottom
129 && near_right
130 && (in_bottom_strip || in_right_strip)
131 && self.enabled_edges.bottom
132 && self.enabled_edges.right
133 {
134 Some(ResizeEdge::BottomRight)
135 } else {
136 None
137 };
138 if corner_hit.is_some() {
139 return corner_hit;
140 }
141
142 let mut best: Option<(f32, ResizeEdge)> = None;
144 for (hit, distance, edge) in [
145 (top_hit, y - top, ResizeEdge::Top),
146 (right_hit, right - x, ResizeEdge::Right),
147 (bottom_hit, bottom - y, ResizeEdge::Bottom),
148 (left_hit, x - left, ResizeEdge::Left),
149 ] {
150 if hit && best.map_or(true, |(best_distance, _)| distance < best_distance) {
151 best = Some((distance, edge));
152 }
153 }
154 best.map(|(_, edge)| edge)
155 }
156}
157
158fn sane(value: Pixels) -> Pixels {
159 if value.0.is_finite() && value.0 > 0.0 {
160 value
161 } else {
162 Pixels(0.0)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::{CursorStyle, point, px, size};
170 use proptest::prelude::*;
171
172 fn bounds() -> Bounds<Pixels> {
173 Bounds::new(point(px(10.), px(20.)), size(px(100.), px(80.)))
174 }
175
176 fn region() -> ResizeRegion {
177 ResizeRegion::new(px(4.)).corner_size(px(8.))
178 }
179
180 #[test]
181 fn resize_handles_and_cursor_directions_agree() {
182 for (x, y, expected) in [
184 (60., 20., Some((ResizeEdge::Top, CursorStyle::ResizeUp))),
185 (
186 109.,
187 60.,
188 Some((ResizeEdge::Right, CursorStyle::ResizeRight)),
189 ),
190 (
191 60.,
192 99.,
193 Some((ResizeEdge::Bottom, CursorStyle::ResizeDown)),
194 ),
195 (10., 60., Some((ResizeEdge::Left, CursorStyle::ResizeLeft))),
196 (
197 10.,
198 20.,
199 Some((ResizeEdge::TopLeft, CursorStyle::ResizeUpLeft)),
200 ),
201 (
202 109.,
203 20.,
204 Some((ResizeEdge::TopRight, CursorStyle::ResizeUpRight)),
205 ),
206 (
207 10.,
208 99.,
209 Some((ResizeEdge::BottomLeft, CursorStyle::ResizeDownLeft)),
210 ),
211 (
212 109.,
213 99.,
214 Some((ResizeEdge::BottomRight, CursorStyle::ResizeDownRight)),
215 ),
216 (60., 60., None),
217 (9.75, 60., None),
218 (110., 60., None),
219 (60., 19.75, None),
220 (60., 100., None),
221 ] {
222 let actual = region().hit_test(point(px(x), px(y)), bounds());
223 assert_eq!(actual, expected.map(|(edge, _)| edge), "({x}, {y})");
224 assert_eq!(
225 actual.map(CursorStyle::from),
226 expected.map(|(_, cursor)| cursor)
227 );
228 }
229 }
230
231 #[test]
232 fn strip_and_corner_boundaries_follow_half_open_bounds() {
233 for (x, y, expected) in [
234 (60., 23.75, Some(ResizeEdge::Top)),
235 (60., 24., None),
236 (106., 60., Some(ResizeEdge::Right)),
237 (105.75, 60., None),
238 (60., 96., Some(ResizeEdge::Bottom)),
239 (60., 95.75, None),
240 (13.75, 60., Some(ResizeEdge::Left)),
241 (14., 60., None),
242 (17.75, 21., Some(ResizeEdge::TopLeft)),
243 (18., 21., Some(ResizeEdge::Top)),
244 (102., 21., Some(ResizeEdge::TopRight)),
245 (101.75, 21., Some(ResizeEdge::Top)),
246 (11., 27.75, Some(ResizeEdge::TopLeft)),
247 (11., 28., Some(ResizeEdge::Left)),
248 (11., 92., Some(ResizeEdge::BottomLeft)),
249 (11., 91.75, Some(ResizeEdge::Left)),
250 (17., 27., None), (103., 93., None),
252 ] {
253 assert_eq!(
254 region().hit_test(point(px(x), px(y)), bounds()),
255 expected,
256 "({x}, {y})"
257 );
258 }
259 }
260
261 fn edges(mask: u8) -> Edges<bool> {
262 Edges {
263 top: mask & 1 != 0,
264 right: mask & 2 != 0,
265 bottom: mask & 4 != 0,
266 left: mask & 8 != 0,
267 }
268 }
269
270 #[test]
271 fn every_edge_mask_disables_sides_and_demotes_corners() {
272 for mask in 0..16 {
273 let region = region().enabled_edges(edges(mask));
274 for (x, y, bit, edge) in [
275 (60., 21., 1, ResizeEdge::Top),
276 (109., 60., 2, ResizeEdge::Right),
277 (60., 99., 4, ResizeEdge::Bottom),
278 (11., 60., 8, ResizeEdge::Left),
279 ] {
280 assert_eq!(
281 region.hit_test(point(px(x), px(y)), bounds()),
282 (mask & bit != 0).then_some(edge),
283 "mask={mask}, {edge:?}"
284 );
285 }
286 for (x, y, horizontal, vertical, side, corner) in [
288 (16., 21., 1, 8, ResizeEdge::Top, ResizeEdge::TopLeft),
289 (104., 21., 1, 2, ResizeEdge::Top, ResizeEdge::TopRight),
290 (16., 99., 4, 8, ResizeEdge::Bottom, ResizeEdge::BottomLeft),
291 (104., 99., 4, 2, ResizeEdge::Bottom, ResizeEdge::BottomRight),
292 ] {
293 let expected = match (mask & horizontal != 0, mask & vertical != 0) {
294 (true, true) => Some(corner),
295 (true, false) => Some(side),
296 (false, _) => None,
297 };
298 assert_eq!(
299 region.hit_test(point(px(x), px(y)), bounds()),
300 expected,
301 "mask={mask}, {corner:?}"
302 );
303 }
304 }
305 }
306
307 #[test]
308 fn tiling_intersects_enabled_edges_and_never_reenables_a_side() {
309 for enabled in 0..16 {
310 for tiled in 0..16 {
311 let e = edges(tiled);
312 let tiling = Tiling {
313 top: e.top,
314 right: e.right,
315 bottom: e.bottom,
316 left: e.left,
317 };
318 let actual = region()
319 .enabled_edges(edges(enabled))
320 .without_tiled_edges(tiling);
321 let expected = region().enabled_edges(edges(enabled & !tiled));
322 for (x, y) in [
323 (60., 21.),
324 (109., 60.),
325 (60., 99.),
326 (11., 60.),
327 (11., 21.),
328 (109., 21.),
329 (11., 99.),
330 (109., 99.),
331 ] {
332 let p = point(px(x), px(y));
333 assert_eq!(
334 actual.hit_test(p, bounds()),
335 expected.hit_test(p, bounds()),
336 "enabled={enabled}, tiled={tiled}, {p:?}"
337 );
338 }
339 assert_eq!(actual.without_tiled_edges(tiling), actual);
340 }
341 }
342 }
343
344 #[test]
345 fn overlapping_strips_and_corners_choose_the_nearest_boundaries() {
346 let b = Bounds::new(point(px(10.), px(20.)), size(px(2.), px(2.)));
347 let sides = ResizeRegion::new(px(5.)).corner_size(px(0.));
348 for (x, y, side, corner) in [
349 (10.25, 20.5, ResizeEdge::Left, ResizeEdge::TopLeft),
350 (11.75, 20.5, ResizeEdge::Right, ResizeEdge::TopRight),
351 (10.5, 20.25, ResizeEdge::Top, ResizeEdge::TopLeft),
352 (10.5, 21.75, ResizeEdge::Bottom, ResizeEdge::BottomLeft),
353 (11.75, 21.5, ResizeEdge::Right, ResizeEdge::BottomRight),
354 (10.25, 20.25, ResizeEdge::Top, ResizeEdge::TopLeft),
355 (11., 21., ResizeEdge::Top, ResizeEdge::BottomRight),
356 ] {
357 let p = point(px(x), px(y));
358 assert_eq!(sides.hit_test(p, b), Some(side), "{p:?}");
359 assert_eq!(
360 sides.corner_size(px(5.)).hit_test(p, b),
361 Some(corner),
362 "{p:?}"
363 );
364 }
365 }
366
367 #[test]
368 fn invalid_dimensions_and_nonfinite_coordinates_never_activate_handles() {
369 for invalid in [0., -1., f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
370 assert_eq!(
371 ResizeRegion::new(px(invalid)).hit_test(point(px(10.), px(20.)), bounds()),
372 None
373 );
374 assert_eq!(
376 region()
377 .corner_size(px(invalid))
378 .hit_test(point(px(11.), px(21.)), bounds()),
379 Some(ResizeEdge::Top)
380 );
381 for b in [
382 Bounds::new(bounds().origin, size(px(invalid), px(80.))),
383 Bounds::new(bounds().origin, size(px(100.), px(invalid))),
384 ] {
385 assert_eq!(region().hit_test(bounds().origin, b), None, "{b:?}");
386 }
387 }
388 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
389 for p in [point(px(invalid), px(20.)), point(px(10.), px(invalid))] {
390 assert_eq!(region().hit_test(p, bounds()), None);
391 assert_eq!(
392 region().hit_test(bounds().origin, Bounds::new(p, bounds().size)),
393 None
394 );
395 }
396 }
397 for b in [
398 Bounds::new(point(px(f32::MAX), px(0.)), size(px(f32::MAX), px(80.))),
399 Bounds::new(point(px(0.), px(f32::MAX)), size(px(100.), px(f32::MAX))),
400 ] {
401 assert_eq!(
402 region().hit_test(b.origin, b),
403 None,
404 "overflowed bounds: {b:?}"
405 );
406 }
407 }
408
409 proptest! {
410 #[test]
411 fn translating_a_rectangle_preserves_all_hits(
412 x in -8i32..=108, y in -8i32..=88,
413 dx in -1000i32..=1000, dy in -1000i32..=1000,
414 border in 0u8..=120, corner in 0u8..=120, mask in 0u8..16,
415 ) {
416 let p = point(px(x as f32 / 4.),px(y as f32 / 4.));
418 let offset = point(px(dx as f32),px(dy as f32));
419 let b = Bounds::new(Point::default(),size(px(25.),px(20.)));
420 let r = ResizeRegion::new(px(border as f32 / 4.))
421 .corner_size(px(corner as f32 / 4.))
422 .enabled_edges(edges(mask));
423 prop_assert_eq!(r.hit_test(p,b),r.hit_test(p+offset,Bounds::new(offset,b.size)));
424 }
425 }
426}