1use crate::rect::{LayoutRect, Orientation, Ratio};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum InsertPosition {
5 Left,
6 Right,
7 Top,
8 Bottom,
9 TopLeft,
10 TopRight,
11 BottomLeft,
12 BottomRight,
13}
14
15impl InsertPosition {
16 pub fn to_orientation(&self) -> Orientation {
17 match self {
18 InsertPosition::Left | InsertPosition::Right => Orientation::Horizontal,
19 InsertPosition::Top | InsertPosition::Bottom => Orientation::Vertical,
20 InsertPosition::TopLeft
22 | InsertPosition::TopRight
23 | InsertPosition::BottomLeft
24 | InsertPosition::BottomRight => Orientation::Horizontal,
25 }
26 }
27
28 pub fn ratio(&self) -> Ratio {
29 match self {
30 InsertPosition::Left
31 | InsertPosition::Top
32 | InsertPosition::Right
33 | InsertPosition::Bottom
34 | InsertPosition::TopLeft
35 | InsertPosition::TopRight
36 | InsertPosition::BottomLeft
37 | InsertPosition::BottomRight => Ratio(1, 1),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum SnapTarget<Id: Copy + Eq + Ord> {
44 Edge(InsertPosition),
45 TiledInsert {
46 target: Id,
47 position: InsertPosition,
48 },
49}
50
51#[derive(Debug, Clone)]
52pub struct SnapPreview<Id: Copy + Eq + Ord> {
53 pub target: SnapTarget<Id>,
54 pub preview_rect: LayoutRect,
55}
56
57#[derive(Debug, Clone)]
58pub struct EdgeResistanceConfig {
59 pub magnetic_zone: u16,
60 pub spatial_threshold: u16,
61 pub temporal_threshold_ns: Option<u64>,
64}
65
66impl EdgeResistanceConfig {
67 pub fn default_tui() -> Self {
68 Self {
69 magnetic_zone: 3,
70 spatial_threshold: 8,
71 temporal_threshold_ns: Some(500_000_000), }
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct EdgeResistance {
78 pub config: EdgeResistanceConfig,
79 pub prev_x: Option<i32>,
80 pub prev_y: Option<i32>,
81 pub entered_magnetic_x_at: Option<u64>,
84 pub entered_magnetic_y_at: Option<u64>,
85}
86
87impl EdgeResistance {
88 pub fn new(config: EdgeResistanceConfig) -> Self {
89 Self {
90 config,
91 prev_x: None,
92 prev_y: None,
93 entered_magnetic_x_at: None,
94 entered_magnetic_y_at: None,
95 }
96 }
97
98 pub fn default_tui() -> Self {
99 Self::new(EdgeResistanceConfig::default_tui())
100 }
101
102 pub fn apply_x(&mut self, new_x: i32, bounds: LayoutRect, now_ns: u64) -> i32 {
108 let low = bounds.x;
109 let high = bounds
110 .x
111 .saturating_add(i32::from(bounds.width.saturating_sub(1)));
112 let result = snap_axis(&SnapAxisParams {
113 new_val: new_x,
114 low,
115 high,
116 magnetic_zone: self.config.magnetic_zone,
117 spatial_threshold: self.config.spatial_threshold,
118 temporal_threshold_ns: self.config.temporal_threshold_ns,
119 prev: self.prev_x,
120 entered_at: self.entered_magnetic_x_at,
121 now_ns,
122 enable_low_snap: true,
123 enable_high_snap: true,
124 });
125 let is_snapped = result == low || result == high;
127 if is_snapped && self.entered_magnetic_x_at.is_none() {
128 self.entered_magnetic_x_at = Some(now_ns);
129 } else if !is_snapped {
130 self.entered_magnetic_x_at = None;
131 }
132 self.prev_x = Some(new_x);
133 result
134 }
135
136 pub fn apply_y(&mut self, new_y: i32, bounds: LayoutRect, now_ns: u64) -> i32 {
138 let low = bounds.y;
139 let high = bounds
140 .y
141 .saturating_add(i32::from(bounds.height.saturating_sub(1)));
142 let result = snap_axis(&SnapAxisParams {
143 new_val: new_y,
144 low,
145 high,
146 magnetic_zone: self.config.magnetic_zone,
147 spatial_threshold: self.config.spatial_threshold,
148 temporal_threshold_ns: self.config.temporal_threshold_ns,
149 prev: self.prev_y,
150 entered_at: self.entered_magnetic_y_at,
151 now_ns,
152 enable_low_snap: false,
153 enable_high_snap: true,
154 });
155 let is_snapped = result == low || result == high;
156 if is_snapped && self.entered_magnetic_y_at.is_none() {
157 self.entered_magnetic_y_at = Some(now_ns);
158 } else if !is_snapped {
159 self.entered_magnetic_y_at = None;
160 }
161 self.prev_y = Some(new_y);
162 result
163 }
164
165 pub fn apply(&mut self, new_x: i32, bounds: LayoutRect, now_ns: u64) -> i32 {
166 self.apply_x(new_x, bounds, now_ns)
167 }
168}
169
170struct SnapAxisParams {
171 new_val: i32,
172 low: i32,
173 high: i32,
174 magnetic_zone: u16,
175 spatial_threshold: u16,
176 temporal_threshold_ns: Option<u64>,
177 prev: Option<i32>,
178 entered_at: Option<u64>,
179 now_ns: u64,
180 enable_low_snap: bool,
181 enable_high_snap: bool,
182}
183
184fn snap_axis(params: &SnapAxisParams) -> i32 {
185 let zone = i32::from(params.magnetic_zone);
186 let hysteresis = i32::from(params.spatial_threshold);
187
188 let d_low = params.new_val.saturating_sub(params.low).unsigned_abs();
189 let d_high = params.high.saturating_sub(params.new_val).unsigned_abs();
190
191 let already_snapped = params
192 .prev
193 .map(|p| {
194 let pd_low = p.saturating_sub(params.low).unsigned_abs();
195 let pd_high = params.high.saturating_sub(p).unsigned_abs();
196 (params.enable_low_snap && pd_low <= zone as u32)
197 || (params.enable_high_snap && pd_high <= zone as u32)
198 })
199 .unwrap_or(false);
200
201 let threshold = if already_snapped { hysteresis } else { zone };
202
203 let snap_low = d_low <= threshold as u32;
204 let snap_high = d_high <= threshold as u32;
205
206 if already_snapped
208 && let (Some(threshold_ns), Some(entry_ns)) =
209 (params.temporal_threshold_ns, params.entered_at)
210 && params.now_ns.saturating_sub(entry_ns) >= threshold_ns
211 {
212 return params.new_val;
213 }
214
215 if params.enable_low_snap && snap_low && d_low <= d_high {
216 params.low
217 } else if params.enable_high_snap && snap_high {
218 params.high
219 } else {
220 params.new_val
221 }
222}
223
224pub fn detect_edge_snap(
225 col: u16,
226 row: u16,
227 managed_area: LayoutRect,
228 sensitivity: u16,
229) -> Option<InsertPosition> {
230 let d_left = col.saturating_sub(managed_area.x as u16);
231 let d_right = (managed_area
232 .x
233 .saturating_add(i32::from(managed_area.width))
234 .saturating_sub(1) as u16)
235 .saturating_sub(col);
236 let d_top = row.saturating_sub(managed_area.y as u16);
237 let d_bottom = (managed_area
238 .y
239 .saturating_add(i32::from(managed_area.height))
240 .saturating_sub(1) as u16)
241 .saturating_sub(row);
242
243 let min_dist = d_left.min(d_right).min(d_top).min(d_bottom);
244
245 if min_dist >= sensitivity {
246 return None;
247 }
248
249 if d_left == min_dist {
250 Some(InsertPosition::Left)
251 } else if d_right == min_dist {
252 Some(InsertPosition::Right)
253 } else if d_top == min_dist {
254 Some(InsertPosition::Top)
255 } else {
256 Some(InsertPosition::Bottom)
257 }
258}
259
260pub fn edge_preview_rect(managed_area: LayoutRect, pos: InsertPosition) -> LayoutRect {
261 match pos {
262 InsertPosition::Left => LayoutRect {
263 width: managed_area.width / 2,
264 ..managed_area
265 },
266 InsertPosition::Right => LayoutRect {
267 x: managed_area
268 .x
269 .saturating_add(i32::from(managed_area.width / 2)),
270 width: managed_area.width / 2,
271 ..managed_area
272 },
273 InsertPosition::Top => LayoutRect {
274 height: managed_area.height / 2,
275 ..managed_area
276 },
277 InsertPosition::Bottom => LayoutRect {
278 y: managed_area
279 .y
280 .saturating_add(i32::from(managed_area.height / 2)),
281 height: managed_area.height / 2,
282 ..managed_area
283 },
284 InsertPosition::TopLeft => LayoutRect {
286 width: managed_area.width / 2,
287 height: managed_area.height / 2,
288 ..managed_area
289 },
290 InsertPosition::TopRight => LayoutRect {
291 x: managed_area
292 .x
293 .saturating_add(i32::from(managed_area.width / 2)),
294 width: managed_area.width / 2,
295 height: managed_area.height / 2,
296 ..managed_area
297 },
298 InsertPosition::BottomLeft => LayoutRect {
299 y: managed_area
300 .y
301 .saturating_add(i32::from(managed_area.height / 2)),
302 width: managed_area.width / 2,
303 height: managed_area.height / 2,
304 ..managed_area
305 },
306 InsertPosition::BottomRight => LayoutRect {
307 x: managed_area
308 .x
309 .saturating_add(i32::from(managed_area.width / 2)),
310 y: managed_area
311 .y
312 .saturating_add(i32::from(managed_area.height / 2)),
313 width: managed_area.width / 2,
314 height: managed_area.height / 2,
315 },
316 }
317}
318
319pub fn tiled_preview_rect(target_rect: LayoutRect, position: InsertPosition) -> LayoutRect {
320 match position {
321 InsertPosition::Left => LayoutRect {
322 width: target_rect.width / 2,
323 ..target_rect
324 },
325 InsertPosition::Right => LayoutRect {
326 x: target_rect
327 .x
328 .saturating_add(i32::from(target_rect.width / 2)),
329 width: target_rect.width / 2,
330 ..target_rect
331 },
332 InsertPosition::Top => LayoutRect {
333 height: target_rect.height / 2,
334 ..target_rect
335 },
336 InsertPosition::Bottom => LayoutRect {
337 y: target_rect
338 .y
339 .saturating_add(i32::from(target_rect.height / 2)),
340 height: target_rect.height / 2,
341 ..target_rect
342 },
343 InsertPosition::TopLeft => LayoutRect {
345 width: target_rect.width / 2,
346 height: target_rect.height / 2,
347 ..target_rect
348 },
349 InsertPosition::TopRight => LayoutRect {
350 x: target_rect
351 .x
352 .saturating_add(i32::from(target_rect.width / 2)),
353 width: target_rect.width / 2,
354 height: target_rect.height / 2,
355 ..target_rect
356 },
357 InsertPosition::BottomLeft => LayoutRect {
358 y: target_rect
359 .y
360 .saturating_add(i32::from(target_rect.height / 2)),
361 width: target_rect.width / 2,
362 height: target_rect.height / 2,
363 ..target_rect
364 },
365 InsertPosition::BottomRight => LayoutRect {
366 x: target_rect
367 .x
368 .saturating_add(i32::from(target_rect.width / 2)),
369 y: target_rect
370 .y
371 .saturating_add(i32::from(target_rect.height / 2)),
372 width: target_rect.width / 2,
373 height: target_rect.height / 2,
374 },
375 }
376}
377
378pub fn corner_preview_rect(managed_area: LayoutRect, pos: InsertPosition) -> LayoutRect {
380 match pos {
381 InsertPosition::TopLeft => LayoutRect {
382 width: managed_area.width / 2,
383 height: managed_area.height / 2,
384 ..managed_area
385 },
386 InsertPosition::TopRight => LayoutRect {
387 x: managed_area
388 .x
389 .saturating_add(i32::from(managed_area.width / 2)),
390 width: managed_area.width / 2,
391 height: managed_area.height / 2,
392 ..managed_area
393 },
394 InsertPosition::BottomLeft => LayoutRect {
395 y: managed_area
396 .y
397 .saturating_add(i32::from(managed_area.height / 2)),
398 width: managed_area.width / 2,
399 height: managed_area.height / 2,
400 ..managed_area
401 },
402 InsertPosition::BottomRight => LayoutRect {
403 x: managed_area
404 .x
405 .saturating_add(i32::from(managed_area.width / 2)),
406 y: managed_area
407 .y
408 .saturating_add(i32::from(managed_area.height / 2)),
409 width: managed_area.width / 2,
410 height: managed_area.height / 2,
411 },
412 _ => edge_preview_rect(managed_area, pos),
414 }
415}
416
417pub fn detect_corner_snap(
420 col: u16,
421 row: u16,
422 managed_area: LayoutRect,
423 sensitivity: u16,
424) -> Option<InsertPosition> {
425 let margin_left = managed_area.x as u16;
426 let margin_right = (managed_area.x as u16)
427 .saturating_add(managed_area.width)
428 .saturating_sub(1);
429 let margin_top = managed_area.y as u16;
430 let margin_bottom = (managed_area.y as u16)
431 .saturating_add(managed_area.height)
432 .saturating_sub(1);
433
434 let near_left = col <= sensitivity.saturating_add(margin_left);
435 let near_right = col >= margin_right.saturating_sub(sensitivity);
436 let near_top = row <= sensitivity.saturating_add(margin_top);
437 let near_bottom = row >= margin_bottom.saturating_sub(sensitivity);
438
439 if near_top && near_left {
440 Some(InsertPosition::TopLeft)
441 } else if near_top && near_right {
442 Some(InsertPosition::TopRight)
443 } else if near_bottom && near_left {
444 Some(InsertPosition::BottomLeft)
445 } else if near_bottom && near_right {
446 Some(InsertPosition::BottomRight)
447 } else {
448 None
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 fn area() -> LayoutRect {
457 LayoutRect {
458 x: 0,
459 y: 0,
460 width: 80,
461 height: 24,
462 }
463 }
464
465 #[test]
466 fn edge_snap_left() {
467 assert_eq!(
468 detect_edge_snap(1, 12, area(), 2),
469 Some(InsertPosition::Left)
470 );
471 }
472
473 #[test]
474 fn edge_snap_right() {
475 assert_eq!(
476 detect_edge_snap(79, 12, area(), 2),
477 Some(InsertPosition::Right)
478 );
479 }
480
481 #[test]
482 fn edge_snap_top() {
483 assert_eq!(
484 detect_edge_snap(40, 0, area(), 2),
485 Some(InsertPosition::Top)
486 );
487 }
488
489 #[test]
490 fn edge_snap_bottom() {
491 assert_eq!(
492 detect_edge_snap(40, 23, area(), 2),
493 Some(InsertPosition::Bottom)
494 );
495 }
496
497 #[test]
498 fn no_snap_when_far_from_edge() {
499 assert_eq!(detect_edge_snap(40, 12, area(), 2), None);
500 }
501
502 #[test]
503 fn edge_resistance_snaps_to_left_edge() {
504 let mut er = EdgeResistance::default_tui();
505 let bounds = LayoutRect {
506 x: 0,
507 y: 0,
508 width: 80,
509 height: 24,
510 };
511 assert_eq!(er.apply_x(2, bounds, 0), 0);
512 }
513
514 #[test]
515 fn edge_resistance_snaps_to_right_edge() {
516 let mut er = EdgeResistance::default_tui();
517 let bounds = LayoutRect {
518 x: 0,
519 y: 0,
520 width: 80,
521 height: 24,
522 };
523 assert_eq!(er.apply_x(78, bounds, 0), 79);
524 }
525
526 #[test]
527 fn edge_resistance_passes_through_middle() {
528 let mut er = EdgeResistance::default_tui();
529 let bounds = LayoutRect {
530 x: 0,
531 y: 0,
532 width: 80,
533 height: 24,
534 };
535 assert_eq!(er.apply_x(40, bounds, 0), 40);
536 }
537
538 #[test]
539 fn edge_resistance_does_not_snap_y_to_top() {
540 let mut er = EdgeResistance::default_tui();
541 let bounds = LayoutRect {
542 x: 0,
543 y: 0,
544 width: 80,
545 height: 24,
546 };
547 assert_eq!(er.apply_y(2, bounds, 0), 2);
549 }
550
551 #[test]
552 fn edge_resistance_snaps_y_to_bottom() {
553 let mut er = EdgeResistance::default_tui();
554 let bounds = LayoutRect {
555 x: 0,
556 y: 0,
557 width: 80,
558 height: 24,
559 };
560 assert_eq!(er.apply_y(22, bounds, 0), 23);
561 }
562
563 #[test]
564 fn temporal_resistance_breaks_after_threshold() {
565 let mut er = EdgeResistance::default_tui();
566 let bounds = LayoutRect {
567 x: 0,
568 y: 0,
569 width: 80,
570 height: 24,
571 };
572 assert_eq!(er.apply_x(2, bounds, 0), 0);
574 assert_eq!(er.apply_x(5, bounds, 100_000_000), 0);
576 assert_eq!(er.apply_x(5, bounds, 600_000_000), 5);
578 }
579
580 #[test]
581 fn corner_snap_top_left() {
582 let a = area();
583 assert_eq!(
584 detect_corner_snap(0, 0, a, 2),
585 Some(InsertPosition::TopLeft)
586 );
587 assert_eq!(
588 detect_corner_snap(1, 0, a, 2),
589 Some(InsertPosition::TopLeft)
590 );
591 assert_eq!(
592 detect_corner_snap(0, 1, a, 2),
593 Some(InsertPosition::TopLeft)
594 );
595 }
596
597 #[test]
598 fn corner_snap_top_right() {
599 let a = area();
600 assert_eq!(
601 detect_corner_snap(79, 0, a, 2),
602 Some(InsertPosition::TopRight)
603 );
604 assert_eq!(
605 detect_corner_snap(78, 0, a, 2),
606 Some(InsertPosition::TopRight)
607 );
608 }
609
610 #[test]
611 fn corner_snap_bottom_left() {
612 let a = area();
613 assert_eq!(
614 detect_corner_snap(0, 23, a, 2),
615 Some(InsertPosition::BottomLeft)
616 );
617 assert_eq!(
618 detect_corner_snap(1, 23, a, 2),
619 Some(InsertPosition::BottomLeft)
620 );
621 }
622
623 #[test]
624 fn corner_snap_bottom_right() {
625 let a = area();
626 assert_eq!(
627 detect_corner_snap(79, 23, a, 2),
628 Some(InsertPosition::BottomRight)
629 );
630 }
631
632 #[test]
633 fn corner_snap_none_when_far() {
634 let a = area();
635 assert_eq!(detect_corner_snap(40, 12, a, 2), None);
636 }
637
638 #[test]
639 fn edge_preview_left() {
640 let p = edge_preview_rect(area(), InsertPosition::Left);
641 assert_eq!(p.width, 40);
642 assert_eq!(p.x, 0);
643 }
644
645 #[test]
646 fn edge_preview_right() {
647 let p = edge_preview_rect(area(), InsertPosition::Right);
648 assert_eq!(p.width, 40);
649 assert_eq!(p.x, 40);
650 }
651
652 #[test]
653 fn edge_preview_top() {
654 let p = edge_preview_rect(area(), InsertPosition::Top);
655 assert_eq!(p.height, 12);
656 assert_eq!(p.y, 0);
657 }
658
659 #[test]
660 fn edge_preview_bottom() {
661 let p = edge_preview_rect(area(), InsertPosition::Bottom);
662 assert_eq!(p.height, 12);
663 assert_eq!(p.y, 12);
664 }
665
666 #[test]
667 fn tiled_preview_rect_left() {
668 let target = LayoutRect {
669 x: 10,
670 y: 10,
671 width: 60,
672 height: 20,
673 };
674 let p = tiled_preview_rect(target, InsertPosition::Left);
675 assert_eq!(p.width, 30);
676 assert_eq!(p.x, 10);
677 }
678
679 #[test]
680 fn inserting_position_to_orientation() {
681 assert_eq!(
682 InsertPosition::Left.to_orientation(),
683 Orientation::Horizontal
684 );
685 assert_eq!(InsertPosition::Top.to_orientation(), Orientation::Vertical);
686 }
687}