1use crate::rect::{LayoutRect, rect_contains};
2
3pub const FLOATING_MIN_WIDTH: u16 = 6;
6
7pub const FLOATING_MIN_HEIGHT: u16 = 3;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ResizeEdge {
13 Left,
14 Right,
15 Top,
16 Bottom,
17 TopLeft,
18 TopRight,
19 BottomLeft,
20 BottomRight,
21}
22
23#[derive(Debug, Clone, Copy)]
25pub struct ResizeHandle<R: Copy + Eq + Ord> {
26 pub id: R,
27 pub rect: LayoutRect,
28 pub edge: ResizeEdge,
29}
30
31#[derive(Debug, Clone, Copy)]
33pub struct ResizeDrag<R: Copy + Eq + Ord> {
34 pub id: R,
35 pub edge: ResizeEdge,
36 pub start_col: u16,
37 pub start_row: u16,
38 pub start_x: i32,
39 pub start_y: i32,
40 pub start_width: u16,
41 pub start_height: u16,
42}
43
44#[derive(Debug, Clone, Copy)]
46pub struct HeaderDrag<R: Copy + Eq + Ord> {
47 pub id: R,
48 pub initial_x: i32,
49 pub initial_y: i32,
50 pub start_x: u16,
51 pub start_y: u16,
52}
53
54#[derive(Debug, Clone, Copy)]
56pub struct DragHandle<R: Copy + Eq + Ord> {
57 pub id: R,
58 pub rect: LayoutRect,
59}
60
61pub fn resize_handles_for_region<R: Copy + Eq + Ord>(
63 id: R,
64 rect: LayoutRect,
65 _bounds: LayoutRect,
66) -> Vec<ResizeHandle<R>> {
67 if rect.width == 0 || rect.height == 0 {
68 return Vec::new();
69 }
70
71 let x1 = rect.x;
72 let y1 = rect.y;
73 let x2 = rect
74 .x
75 .saturating_add(i32::from(rect.width.saturating_sub(1)));
76 let y2 = rect
77 .y
78 .saturating_add(i32::from(rect.height.saturating_sub(1)));
79
80 let mut handles = Vec::with_capacity(8);
81
82 handles.push(ResizeHandle {
84 id,
85 rect: LayoutRect {
86 x: x1,
87 y: y1,
88 width: 1,
89 height: 1,
90 },
91 edge: ResizeEdge::TopLeft,
92 });
93 handles.push(ResizeHandle {
94 id,
95 rect: LayoutRect {
96 x: x2,
97 y: y1,
98 width: 1,
99 height: 1,
100 },
101 edge: ResizeEdge::TopRight,
102 });
103 handles.push(ResizeHandle {
104 id,
105 rect: LayoutRect {
106 x: x1,
107 y: y2,
108 width: 1,
109 height: 1,
110 },
111 edge: ResizeEdge::BottomLeft,
112 });
113 handles.push(ResizeHandle {
114 id,
115 rect: LayoutRect {
116 x: x2,
117 y: y2,
118 width: 1,
119 height: 1,
120 },
121 edge: ResizeEdge::BottomRight,
122 });
123
124 if rect.height > 2 {
126 let inner_h = rect.height.saturating_sub(2);
127 handles.push(ResizeHandle {
128 id,
129 rect: LayoutRect {
130 x: x1,
131 y: y1.saturating_add(1),
132 width: 1,
133 height: inner_h,
134 },
135 edge: ResizeEdge::Left,
136 });
137 handles.push(ResizeHandle {
138 id,
139 rect: LayoutRect {
140 x: x2,
141 y: y1.saturating_add(1),
142 width: 1,
143 height: inner_h,
144 },
145 edge: ResizeEdge::Right,
146 });
147 }
148 if rect.width > 2 {
149 let inner_w = rect.width.saturating_sub(2);
150 handles.push(ResizeHandle {
151 id,
152 rect: LayoutRect {
153 x: x1.saturating_add(1),
154 y: y1,
155 width: inner_w,
156 height: 1,
157 },
158 edge: ResizeEdge::Top,
159 });
160 handles.push(ResizeHandle {
161 id,
162 rect: LayoutRect {
163 x: x1.saturating_add(1),
164 y: y2,
165 width: inner_w,
166 height: 1,
167 },
168 edge: ResizeEdge::Bottom,
169 });
170 }
171
172 handles
173}
174
175pub fn floating_header_for_region<R: Copy + Eq + Ord>(
178 id: R,
179 rect: LayoutRect,
180 bounds: LayoutRect,
181) -> Option<DragHandle<R>> {
182 if rect.width < 3 || rect.height < 3 {
183 return None;
184 }
185 let header_rect = LayoutRect {
186 x: rect.x.saturating_add(1),
187 y: rect.y.saturating_add(1),
188 width: rect.width.saturating_sub(2),
189 height: 1,
190 };
191 if !rect_contains(&bounds, header_rect.x as u16, header_rect.y as u16) {
192 return None;
193 }
194 Some(DragHandle {
195 id,
196 rect: header_rect,
197 })
198}
199
200#[allow(clippy::too_many_arguments)]
205pub fn apply_resize_drag_signed(
206 start_x: i32,
207 start_y: i32,
208 start_width: u16,
209 start_height: u16,
210 edge: ResizeEdge,
211 column: u16,
212 row: u16,
213 start_col: u16,
214 start_row: u16,
215 bounds: LayoutRect,
216 allow_offscreen: bool,
217) -> LayoutRect {
218 let dx = i32::from(column).saturating_sub(i32::from(start_col));
219 let dy = i32::from(row).saturating_sub(i32::from(start_row));
220
221 let mut x = start_x;
222 let mut y = start_y;
223 let mut w = i32::from(start_width);
224 let mut h = i32::from(start_height);
225
226 match edge {
228 ResizeEdge::Left | ResizeEdge::TopLeft | ResizeEdge::BottomLeft => {
229 x = x.saturating_add(dx);
230 w = w.saturating_sub(dx);
231 }
232 ResizeEdge::Right | ResizeEdge::TopRight | ResizeEdge::BottomRight => {
233 w = w.saturating_add(dx);
234 }
235 _ => {}
236 }
237 match edge {
238 ResizeEdge::Top | ResizeEdge::TopLeft | ResizeEdge::TopRight => {
239 y = y.saturating_add(dy);
240 h = h.saturating_sub(dy);
241 }
242 ResizeEdge::Bottom | ResizeEdge::BottomLeft | ResizeEdge::BottomRight => {
243 h = h.saturating_add(dy);
244 }
245 _ => {}
246 }
247
248 let min_w = i32::from(FLOATING_MIN_WIDTH);
250 let min_h = i32::from(FLOATING_MIN_HEIGHT);
251
252 if w < min_w {
253 match edge {
254 ResizeEdge::Left | ResizeEdge::TopLeft | ResizeEdge::BottomLeft => {
255 x = x.saturating_sub(min_w.saturating_sub(w));
256 }
257 _ => {}
258 }
259 w = min_w;
260 }
261 if h < min_h {
262 match edge {
263 ResizeEdge::Top | ResizeEdge::TopLeft | ResizeEdge::TopRight => {
264 y = y.saturating_sub(min_h.saturating_sub(h));
265 }
266 _ => {}
267 }
268 h = min_h;
269 }
270
271 let mut width = w.max(1).min(i32::from(u16::MAX)) as u16;
273 let mut height = h.max(1).min(i32::from(u16::MAX)) as u16;
274
275 if !allow_offscreen {
277 width = width.min(bounds.width);
278 height = height.min(bounds.height);
279
280 let bounds_x1 = bounds.x.saturating_add(i32::from(bounds.width));
281 let bounds_y1 = bounds.y.saturating_add(i32::from(bounds.height));
282 let max_x = bounds_x1.saturating_sub(i32::from(width));
283 let max_y = bounds_y1.saturating_sub(i32::from(height));
284
285 x = x.max(bounds.x).min(max_x);
286 y = y.max(bounds.y).min(max_y);
287 }
288
289 LayoutRect {
290 x,
291 y,
292 width,
293 height,
294 }
295}
296
297pub fn clamp_floating_to_bounds(
305 rect: LayoutRect,
306 bounds: LayoutRect,
307 min_visible_margin: u16,
308 allow_offscreen: bool,
309) -> LayoutRect {
310 let min_w = FLOATING_MIN_WIDTH.min(bounds.width.max(1));
311 let min_h = FLOATING_MIN_HEIGHT.min(bounds.height.max(1));
312
313 let width = if allow_offscreen {
314 rect.width.max(min_w)
315 } else {
316 rect.width.max(min_w).min(bounds.width)
317 };
318 let height = if allow_offscreen {
319 rect.height.max(min_h)
320 } else {
321 rect.height.max(min_h).min(bounds.height)
322 };
323
324 let max_x = if allow_offscreen {
325 bounds
326 .x
327 .saturating_add(i32::from(bounds.width))
328 .saturating_sub(i32::from(min_visible_margin.min(width)))
329 } else {
330 bounds
331 .x
332 .saturating_add(i32::from(bounds.width.saturating_sub(width)))
333 };
334
335 let max_y = if allow_offscreen {
336 bounds
337 .y
338 .saturating_add(i32::from(bounds.height))
339 .saturating_sub(i32::from(min_visible_margin.min(height)))
340 } else {
341 bounds
342 .y
343 .saturating_add(i32::from(bounds.height.saturating_sub(height)))
344 };
345
346 let out_x = rect.x.saturating_add(i32::from(rect.width)) <= bounds.x
347 || rect.x >= bounds.x.saturating_add(i32::from(bounds.width));
348 let out_y = rect.y.saturating_add(i32::from(rect.height)) <= bounds.y
349 || rect.y >= bounds.y.saturating_add(i32::from(bounds.height));
350
351 let x = if out_x || !allow_offscreen {
352 rect.x.clamp(bounds.x.min(max_x), max_x)
353 } else {
354 let visible_width = min_visible_margin.min(width);
355 let left_allowed = bounds
356 .x
357 .saturating_sub(i32::from(width.saturating_sub(visible_width)));
358 rect.x.clamp(left_allowed.min(max_x), max_x)
359 };
360
361 let y = if out_y || !allow_offscreen {
362 rect.y.clamp(bounds.y.min(max_y), max_y)
363 } else {
364 let visible_height = min_visible_margin.min(height);
365 let top_allowed = bounds
366 .y
367 .saturating_sub(i32::from(height.saturating_sub(visible_height)));
368 rect.y.clamp(top_allowed.min(max_y), max_y)
369 };
370
371 LayoutRect {
372 x,
373 y,
374 width,
375 height,
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 fn area() -> LayoutRect {
384 LayoutRect {
385 x: 0,
386 y: 0,
387 width: 80,
388 height: 24,
389 }
390 }
391
392 #[test]
393 fn resize_handles_count() {
394 let rect = LayoutRect {
395 x: 10,
396 y: 10,
397 width: 20,
398 height: 15,
399 };
400 let handles = resize_handles_for_region(1u8, rect, area());
401 assert_eq!(handles.len(), 8);
402 }
403
404 #[test]
405 fn resize_handles_small_rect() {
406 let rect = LayoutRect {
407 x: 10,
408 y: 10,
409 width: 1,
410 height: 1,
411 };
412 let handles = resize_handles_for_region(1u8, rect, area());
413 assert_eq!(handles.len(), 4);
415 }
416
417 #[test]
418 fn floating_header_normal() {
419 let rect = LayoutRect {
420 x: 10,
421 y: 10,
422 width: 20,
423 height: 20,
424 };
425 let header = floating_header_for_region(1u8, rect, area());
426 assert!(header.is_some());
427 let h = header.unwrap();
428 assert_eq!(h.rect.width, 18);
429 assert_eq!(h.rect.height, 1);
430 assert_eq!(h.rect.y, 11);
431 }
432
433 #[test]
434 fn floating_header_too_small() {
435 let rect = LayoutRect {
436 x: 10,
437 y: 10,
438 width: 2,
439 height: 2,
440 };
441 assert!(floating_header_for_region(1u8, rect, area()).is_none());
442 }
443
444 #[test]
445 fn apply_resize_drag_right_edge() {
446 let result = apply_resize_drag_signed(
447 10,
448 10,
449 20,
450 15,
451 ResizeEdge::Right,
452 50,
453 10,
454 30,
455 10,
456 area(),
457 false,
458 );
459 assert_eq!(result.width, 40); assert_eq!(result.x, 10);
461 }
462
463 #[test]
464 fn apply_resize_drag_left_edge() {
465 let result = apply_resize_drag_signed(
466 20,
467 10,
468 20,
469 15,
470 ResizeEdge::Left,
471 10,
472 10,
473 30,
474 10,
475 area(),
476 false,
477 );
478 assert_eq!(result.x, 0); assert_eq!(result.width, 40); }
481
482 #[test]
483 fn apply_resize_drag_enforces_min_size() {
484 let result = apply_resize_drag_signed(
486 10,
487 10,
488 10,
489 10,
490 ResizeEdge::Left,
491 20,
492 10,
493 5,
494 10,
495 area(),
496 false,
497 );
498 assert_eq!(result.width, FLOATING_MIN_WIDTH);
499 }
500
501 #[test]
502 fn apply_resize_drag_offscreen_allowed() {
503 let result =
504 apply_resize_drag_signed(0, 0, 80, 24, ResizeEdge::Left, 40, 0, 0, 0, area(), true);
505 assert_eq!(result.x, 40);
506 assert_eq!(result.width, 40);
507 }
508
509 #[test]
510 fn clamp_floating_keeps_min_margin_when_offscreen_allowed() {
511 let bounds = LayoutRect {
514 x: 0,
515 y: 0,
516 width: 10,
517 height: 10,
518 };
519 let rect = LayoutRect {
520 x: -4,
521 y: 0,
522 width: 6,
523 height: 3,
524 };
525 let clamped = clamp_floating_to_bounds(rect, bounds, 4, true);
526 assert_eq!(clamped.x, -2);
528 assert_eq!(clamped.width, 6);
529 }
530
531 #[test]
532 fn clamp_floating_keeps_min_margin_vertically() {
533 let bounds = LayoutRect {
534 x: 0,
535 y: 0,
536 width: 10,
537 height: 10,
538 };
539 let rect = LayoutRect {
540 x: 0,
541 y: -3,
542 width: 6,
543 height: 4,
544 };
545 let clamped = clamp_floating_to_bounds(rect, bounds, 4, true);
546 assert!(
547 clamped.y >= -1,
548 "must keep >= 4 visible rows: y={}",
549 clamped.y
550 );
551 assert!(clamped.y + i32::from(clamped.height) >= 4);
552 }
553
554 #[test]
555 fn clamp_floating_contains_when_offscreen_not_allowed() {
556 let bounds = LayoutRect {
557 x: 0,
558 y: 0,
559 width: 10,
560 height: 10,
561 };
562 let rect = LayoutRect {
563 x: -4,
564 y: 8,
565 width: 6,
566 height: 3,
567 };
568 let clamped = clamp_floating_to_bounds(rect, bounds, 4, false);
569 assert_eq!(clamped.x, 0);
570 assert_eq!(clamped.y, 7); assert_eq!(clamped.width, 6);
572 assert_eq!(clamped.height, 3);
573 }
574
575 #[test]
576 fn clamp_floating_enforces_minimum_size() {
577 let bounds = LayoutRect {
578 x: 0,
579 y: 0,
580 width: 10,
581 height: 10,
582 };
583 let rect = LayoutRect {
584 x: 3,
585 y: 3,
586 width: 1,
587 height: 1,
588 };
589 let clamped = clamp_floating_to_bounds(rect, bounds, 4, true);
590 assert_eq!(clamped.width, FLOATING_MIN_WIDTH);
591 assert_eq!(clamped.height, FLOATING_MIN_HEIGHT);
592 }
593
594 #[test]
595 fn clamp_floating_zero_size_bounds_does_not_panic() {
596 let bounds = LayoutRect::default();
597 let rect = LayoutRect {
598 x: 2,
599 y: 2,
600 width: 6,
601 height: 4,
602 };
603 let clamped = clamp_floating_to_bounds(rect, bounds, 4, true);
604 assert!(clamped.width >= 1);
606 }
607}