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}
10
11impl InsertPosition {
12 pub fn to_orientation(&self) -> Orientation {
13 match self {
14 InsertPosition::Left | InsertPosition::Right => Orientation::Horizontal,
15 InsertPosition::Top | InsertPosition::Bottom => Orientation::Vertical,
16 }
17 }
18
19 pub fn ratio(&self) -> Ratio {
20 match self {
21 InsertPosition::Left | InsertPosition::Top => Ratio(1, 1),
22 InsertPosition::Right | InsertPosition::Bottom => Ratio(1, 1),
23 }
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SnapTarget<Id: Copy + Eq + Ord> {
29 Edge(InsertPosition),
30 TiledInsert {
31 target: Id,
32 position: InsertPosition,
33 },
34}
35
36#[derive(Debug, Clone)]
37pub struct SnapPreview<Id: Copy + Eq + Ord> {
38 pub target: SnapTarget<Id>,
39 pub preview_rect: LayoutRect,
40}
41
42#[derive(Debug, Clone)]
43pub struct EdgeResistanceConfig {
44 pub magnetic_zone: u16,
45 pub spatial_threshold: u16,
46 pub _temporal_threshold_ns: Option<u64>,
49}
50
51impl EdgeResistanceConfig {
52 pub fn default_tui() -> Self {
53 Self {
54 magnetic_zone: 3,
55 spatial_threshold: 8,
56 _temporal_threshold_ns: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
62pub struct EdgeResistance {
63 pub config: EdgeResistanceConfig,
64 pub prev_x: Option<i32>,
65 pub prev_y: Option<i32>,
66}
67
68impl EdgeResistance {
69 pub fn new(config: EdgeResistanceConfig) -> Self {
70 Self {
71 config,
72 prev_x: None,
73 prev_y: None,
74 }
75 }
76
77 pub fn default_tui() -> Self {
78 Self::new(EdgeResistanceConfig::default_tui())
79 }
80
81 pub fn apply_x(&mut self, new_x: i32, bounds: LayoutRect) -> i32 {
82 let low = bounds.x;
83 let high = bounds
84 .x
85 .saturating_add(i32::from(bounds.width.saturating_sub(1)));
86 let result = snap_axis(
87 new_x,
88 low,
89 high,
90 self.config.magnetic_zone,
91 self.config.spatial_threshold,
92 &self.prev_x,
93 );
94 self.prev_x = Some(new_x);
95 result
96 }
97
98 pub fn apply_y(&mut self, new_y: i32, bounds: LayoutRect) -> i32 {
99 let low = bounds.y;
100 let high = bounds
101 .y
102 .saturating_add(i32::from(bounds.height.saturating_sub(1)));
103 let result = snap_axis(
104 new_y,
105 low,
106 high,
107 self.config.magnetic_zone,
108 self.config.spatial_threshold,
109 &self.prev_y,
110 );
111 self.prev_y = Some(new_y);
112 result
113 }
114
115 pub fn apply(&mut self, new_x: i32, bounds: LayoutRect) -> i32 {
116 self.apply_x(new_x, bounds)
117 }
118}
119
120fn snap_axis(
121 new_val: i32,
122 low: i32,
123 high: i32,
124 magnetic_zone: u16,
125 spatial_threshold: u16,
126 prev: &Option<i32>,
127) -> i32 {
128 let zone = i32::from(magnetic_zone);
129 let hysteresis = i32::from(spatial_threshold);
130
131 let d_low = new_val.saturating_sub(low).unsigned_abs();
132 let d_high = high.saturating_sub(new_val).unsigned_abs();
133
134 let already_snapped = prev
135 .map(|p| {
136 let pd_low = p.saturating_sub(low).unsigned_abs();
137 let pd_high = high.saturating_sub(p).unsigned_abs();
138 pd_low <= zone as u32 || pd_high <= zone as u32
139 })
140 .unwrap_or(false);
141
142 let threshold = if already_snapped { hysteresis } else { zone };
143
144 let snap_low = d_low <= threshold as u32;
145 let snap_high = d_high <= threshold as u32;
146
147 if snap_low && d_low <= d_high {
148 low
149 } else if snap_high {
150 high
151 } else {
152 new_val
153 }
154}
155
156pub fn detect_edge_snap(
157 col: u16,
158 row: u16,
159 managed_area: LayoutRect,
160 sensitivity: u16,
161) -> Option<InsertPosition> {
162 let d_left = col.saturating_sub(managed_area.x as u16);
163 let d_right = (managed_area
164 .x
165 .saturating_add(i32::from(managed_area.width))
166 .saturating_sub(1) as u16)
167 .saturating_sub(col);
168 let d_top = row.saturating_sub(managed_area.y as u16);
169 let d_bottom = (managed_area
170 .y
171 .saturating_add(i32::from(managed_area.height))
172 .saturating_sub(1) as u16)
173 .saturating_sub(row);
174
175 let min_dist = d_left.min(d_right).min(d_top).min(d_bottom);
176
177 if min_dist >= sensitivity {
178 return None;
179 }
180
181 if d_left == min_dist {
182 Some(InsertPosition::Left)
183 } else if d_right == min_dist {
184 Some(InsertPosition::Right)
185 } else if d_top == min_dist {
186 Some(InsertPosition::Top)
187 } else {
188 Some(InsertPosition::Bottom)
189 }
190}
191
192pub fn edge_preview_rect(managed_area: LayoutRect, pos: InsertPosition) -> LayoutRect {
193 match pos {
194 InsertPosition::Left => LayoutRect {
195 width: managed_area.width / 2,
196 ..managed_area
197 },
198 InsertPosition::Right => LayoutRect {
199 x: managed_area
200 .x
201 .saturating_add(i32::from(managed_area.width / 2)),
202 width: managed_area.width / 2,
203 ..managed_area
204 },
205 InsertPosition::Top => LayoutRect {
206 height: managed_area.height / 2,
207 ..managed_area
208 },
209 InsertPosition::Bottom => LayoutRect {
210 y: managed_area
211 .y
212 .saturating_add(i32::from(managed_area.height / 2)),
213 height: managed_area.height / 2,
214 ..managed_area
215 },
216 }
217}
218
219pub fn tiled_preview_rect(target_rect: LayoutRect, position: InsertPosition) -> LayoutRect {
220 match position {
221 InsertPosition::Left => LayoutRect {
222 width: target_rect.width / 2,
223 ..target_rect
224 },
225 InsertPosition::Right => LayoutRect {
226 x: target_rect
227 .x
228 .saturating_add(i32::from(target_rect.width / 2)),
229 width: target_rect.width / 2,
230 ..target_rect
231 },
232 InsertPosition::Top => LayoutRect {
233 height: target_rect.height / 2,
234 ..target_rect
235 },
236 InsertPosition::Bottom => LayoutRect {
237 y: target_rect
238 .y
239 .saturating_add(i32::from(target_rect.height / 2)),
240 height: target_rect.height / 2,
241 ..target_rect
242 },
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 fn area() -> LayoutRect {
251 LayoutRect {
252 x: 0,
253 y: 0,
254 width: 80,
255 height: 24,
256 }
257 }
258
259 #[test]
260 fn edge_snap_left() {
261 assert_eq!(
262 detect_edge_snap(1, 12, area(), 2),
263 Some(InsertPosition::Left)
264 );
265 }
266
267 #[test]
268 fn edge_snap_right() {
269 assert_eq!(
270 detect_edge_snap(79, 12, area(), 2),
271 Some(InsertPosition::Right)
272 );
273 }
274
275 #[test]
276 fn edge_snap_top() {
277 assert_eq!(
278 detect_edge_snap(40, 0, area(), 2),
279 Some(InsertPosition::Top)
280 );
281 }
282
283 #[test]
284 fn edge_snap_bottom() {
285 assert_eq!(
286 detect_edge_snap(40, 23, area(), 2),
287 Some(InsertPosition::Bottom)
288 );
289 }
290
291 #[test]
292 fn no_snap_when_far_from_edge() {
293 assert_eq!(detect_edge_snap(40, 12, area(), 2), None);
294 }
295
296 #[test]
297 fn edge_resistance_snaps_to_left_edge() {
298 let mut er = EdgeResistance::default_tui();
299 let bounds = LayoutRect {
300 x: 0,
301 y: 0,
302 width: 80,
303 height: 24,
304 };
305 assert_eq!(er.apply_x(2, bounds), 0);
306 }
307
308 #[test]
309 fn edge_resistance_snaps_to_right_edge() {
310 let mut er = EdgeResistance::default_tui();
311 let bounds = LayoutRect {
312 x: 0,
313 y: 0,
314 width: 80,
315 height: 24,
316 };
317 assert_eq!(er.apply_x(78, bounds), 79);
318 }
319
320 #[test]
321 fn edge_resistance_passes_through_middle() {
322 let mut er = EdgeResistance::default_tui();
323 let bounds = LayoutRect {
324 x: 0,
325 y: 0,
326 width: 80,
327 height: 24,
328 };
329 assert_eq!(er.apply_x(40, bounds), 40);
330 }
331
332 #[test]
333 fn edge_resistance_snaps_y_to_top() {
334 let mut er = EdgeResistance::default_tui();
335 let bounds = LayoutRect {
336 x: 0,
337 y: 0,
338 width: 80,
339 height: 24,
340 };
341 assert_eq!(er.apply_y(2, bounds), 0);
342 }
343
344 #[test]
345 fn edge_resistance_snaps_y_to_bottom() {
346 let mut er = EdgeResistance::default_tui();
347 let bounds = LayoutRect {
348 x: 0,
349 y: 0,
350 width: 80,
351 height: 24,
352 };
353 assert_eq!(er.apply_y(22, bounds), 23);
354 }
355
356 #[test]
357 fn edge_preview_left() {
358 let p = edge_preview_rect(area(), InsertPosition::Left);
359 assert_eq!(p.width, 40);
360 assert_eq!(p.x, 0);
361 }
362
363 #[test]
364 fn edge_preview_right() {
365 let p = edge_preview_rect(area(), InsertPosition::Right);
366 assert_eq!(p.width, 40);
367 assert_eq!(p.x, 40);
368 }
369
370 #[test]
371 fn edge_preview_top() {
372 let p = edge_preview_rect(area(), InsertPosition::Top);
373 assert_eq!(p.height, 12);
374 assert_eq!(p.y, 0);
375 }
376
377 #[test]
378 fn edge_preview_bottom() {
379 let p = edge_preview_rect(area(), InsertPosition::Bottom);
380 assert_eq!(p.height, 12);
381 assert_eq!(p.y, 12);
382 }
383
384 #[test]
385 fn tiled_preview_rect_left() {
386 let target = LayoutRect {
387 x: 10,
388 y: 10,
389 width: 60,
390 height: 20,
391 };
392 let p = tiled_preview_rect(target, InsertPosition::Left);
393 assert_eq!(p.width, 30);
394 assert_eq!(p.x, 10);
395 }
396
397 #[test]
398 fn inserting_position_to_orientation() {
399 assert_eq!(
400 InsertPosition::Left.to_orientation(),
401 Orientation::Horizontal
402 );
403 assert_eq!(InsertPosition::Top.to_orientation(), Orientation::Vertical);
404 }
405}