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
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn area() -> LayoutRect {
302 LayoutRect {
303 x: 0,
304 y: 0,
305 width: 80,
306 height: 24,
307 }
308 }
309
310 #[test]
311 fn resize_handles_count() {
312 let rect = LayoutRect {
313 x: 10,
314 y: 10,
315 width: 20,
316 height: 15,
317 };
318 let handles = resize_handles_for_region(1u8, rect, area());
319 assert_eq!(handles.len(), 8);
320 }
321
322 #[test]
323 fn resize_handles_small_rect() {
324 let rect = LayoutRect {
325 x: 10,
326 y: 10,
327 width: 1,
328 height: 1,
329 };
330 let handles = resize_handles_for_region(1u8, rect, area());
331 assert_eq!(handles.len(), 4);
333 }
334
335 #[test]
336 fn floating_header_normal() {
337 let rect = LayoutRect {
338 x: 10,
339 y: 10,
340 width: 20,
341 height: 20,
342 };
343 let header = floating_header_for_region(1u8, rect, area());
344 assert!(header.is_some());
345 let h = header.unwrap();
346 assert_eq!(h.rect.width, 18);
347 assert_eq!(h.rect.height, 1);
348 assert_eq!(h.rect.y, 11);
349 }
350
351 #[test]
352 fn floating_header_too_small() {
353 let rect = LayoutRect {
354 x: 10,
355 y: 10,
356 width: 2,
357 height: 2,
358 };
359 assert!(floating_header_for_region(1u8, rect, area()).is_none());
360 }
361
362 #[test]
363 fn apply_resize_drag_right_edge() {
364 let result = apply_resize_drag_signed(
365 10,
366 10,
367 20,
368 15,
369 ResizeEdge::Right,
370 50,
371 10,
372 30,
373 10,
374 area(),
375 false,
376 );
377 assert_eq!(result.width, 40); assert_eq!(result.x, 10);
379 }
380
381 #[test]
382 fn apply_resize_drag_left_edge() {
383 let result = apply_resize_drag_signed(
384 20,
385 10,
386 20,
387 15,
388 ResizeEdge::Left,
389 10,
390 10,
391 30,
392 10,
393 area(),
394 false,
395 );
396 assert_eq!(result.x, 0); assert_eq!(result.width, 40); }
399
400 #[test]
401 fn apply_resize_drag_enforces_min_size() {
402 let result = apply_resize_drag_signed(
404 10,
405 10,
406 10,
407 10,
408 ResizeEdge::Left,
409 20,
410 10,
411 5,
412 10,
413 area(),
414 false,
415 );
416 assert_eq!(result.width, FLOATING_MIN_WIDTH);
417 }
418
419 #[test]
420 fn apply_resize_drag_offscreen_allowed() {
421 let result =
422 apply_resize_drag_signed(0, 0, 80, 24, ResizeEdge::Left, 40, 0, 0, 0, area(), true);
423 assert_eq!(result.x, 40);
424 assert_eq!(result.width, 40);
425 }
426}