1use crate::rect::{LayoutRect, rect_contains};
2
3pub const FLOATING_MIN_WIDTH: u16 = 6;
5
6pub const FLOATING_MIN_HEIGHT: u16 = 3;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ResizeEdge {
12 Left,
13 Right,
14 Top,
15 Bottom,
16 TopLeft,
17 TopRight,
18 BottomLeft,
19 BottomRight,
20}
21
22#[derive(Debug, Clone, Copy)]
24pub struct ResizeHandle<R: Copy + Eq + Ord> {
25 pub id: R,
26 pub rect: LayoutRect,
27 pub edge: ResizeEdge,
28}
29
30#[derive(Debug, Clone, Copy)]
32pub struct ResizeDrag<R: Copy + Eq + Ord> {
33 pub id: R,
34 pub edge: ResizeEdge,
35 pub start_col: u16,
36 pub start_row: u16,
37 pub start_x: i32,
38 pub start_y: i32,
39 pub start_width: u16,
40 pub start_height: u16,
41}
42
43#[derive(Debug, Clone, Copy)]
45pub struct HeaderDrag<R: Copy + Eq + Ord> {
46 pub id: R,
47 pub initial_x: i32,
48 pub initial_y: i32,
49 pub start_x: u16,
50 pub start_y: u16,
51}
52
53#[derive(Debug, Clone, Copy)]
55pub struct DragHandle<R: Copy + Eq + Ord> {
56 pub id: R,
57 pub rect: LayoutRect,
58}
59
60pub fn resize_handles_for_region<R: Copy + Eq + Ord>(
62 id: R,
63 rect: LayoutRect,
64 _bounds: LayoutRect,
65) -> Vec<ResizeHandle<R>> {
66 if rect.width == 0 || rect.height == 0 {
67 return Vec::new();
68 }
69
70 let x1 = rect.x;
71 let y1 = rect.y;
72 let x2 = rect
73 .x
74 .saturating_add(i32::from(rect.width.saturating_sub(1)));
75 let y2 = rect
76 .y
77 .saturating_add(i32::from(rect.height.saturating_sub(1)));
78
79 let mut handles = Vec::with_capacity(8);
80
81 handles.push(ResizeHandle {
83 id,
84 rect: LayoutRect {
85 x: x1,
86 y: y1,
87 width: 1,
88 height: 1,
89 },
90 edge: ResizeEdge::TopLeft,
91 });
92 handles.push(ResizeHandle {
93 id,
94 rect: LayoutRect {
95 x: x2,
96 y: y1,
97 width: 1,
98 height: 1,
99 },
100 edge: ResizeEdge::TopRight,
101 });
102 handles.push(ResizeHandle {
103 id,
104 rect: LayoutRect {
105 x: x1,
106 y: y2,
107 width: 1,
108 height: 1,
109 },
110 edge: ResizeEdge::BottomLeft,
111 });
112 handles.push(ResizeHandle {
113 id,
114 rect: LayoutRect {
115 x: x2,
116 y: y2,
117 width: 1,
118 height: 1,
119 },
120 edge: ResizeEdge::BottomRight,
121 });
122
123 if rect.height > 2 {
125 let inner_h = rect.height.saturating_sub(2);
126 handles.push(ResizeHandle {
127 id,
128 rect: LayoutRect {
129 x: x1,
130 y: y1.saturating_add(1),
131 width: 1,
132 height: inner_h,
133 },
134 edge: ResizeEdge::Left,
135 });
136 handles.push(ResizeHandle {
137 id,
138 rect: LayoutRect {
139 x: x2,
140 y: y1.saturating_add(1),
141 width: 1,
142 height: inner_h,
143 },
144 edge: ResizeEdge::Right,
145 });
146 }
147 if rect.width > 2 {
148 let inner_w = rect.width.saturating_sub(2);
149 handles.push(ResizeHandle {
150 id,
151 rect: LayoutRect {
152 x: x1.saturating_add(1),
153 y: y1,
154 width: inner_w,
155 height: 1,
156 },
157 edge: ResizeEdge::Top,
158 });
159 handles.push(ResizeHandle {
160 id,
161 rect: LayoutRect {
162 x: x1.saturating_add(1),
163 y: y2,
164 width: inner_w,
165 height: 1,
166 },
167 edge: ResizeEdge::Bottom,
168 });
169 }
170
171 handles
172}
173
174pub fn floating_header_for_region<R: Copy + Eq + Ord>(
176 id: R,
177 rect: LayoutRect,
178 bounds: LayoutRect,
179) -> Option<DragHandle<R>> {
180 if rect.width < 3 || rect.height < 3 {
181 return None;
182 }
183 let header_rect = LayoutRect {
184 x: rect.x.saturating_add(1),
185 y: rect.y.saturating_add(1),
186 width: rect.width.saturating_sub(2),
187 height: 1,
188 };
189 if !rect_contains(&bounds, header_rect.x as u16, header_rect.y as u16) {
190 return None;
191 }
192 Some(DragHandle {
193 id,
194 rect: header_rect,
195 })
196}
197
198#[allow(clippy::too_many_arguments)]
203pub fn apply_resize_drag_signed(
204 start_x: i32,
205 start_y: i32,
206 start_width: u16,
207 start_height: u16,
208 edge: ResizeEdge,
209 column: u16,
210 row: u16,
211 start_col: u16,
212 start_row: u16,
213 bounds: LayoutRect,
214 allow_offscreen: bool,
215) -> LayoutRect {
216 let dx = i32::from(column).saturating_sub(i32::from(start_col));
217 let dy = i32::from(row).saturating_sub(i32::from(start_row));
218
219 let mut x = start_x;
220 let mut y = start_y;
221 let mut w = i32::from(start_width);
222 let mut h = i32::from(start_height);
223
224 match edge {
226 ResizeEdge::Left | ResizeEdge::TopLeft | ResizeEdge::BottomLeft => {
227 x = x.saturating_add(dx);
228 w = w.saturating_sub(dx);
229 }
230 ResizeEdge::Right | ResizeEdge::TopRight | ResizeEdge::BottomRight => {
231 w = w.saturating_add(dx);
232 }
233 _ => {}
234 }
235 match edge {
236 ResizeEdge::Top | ResizeEdge::TopLeft | ResizeEdge::TopRight => {
237 y = y.saturating_add(dy);
238 h = h.saturating_sub(dy);
239 }
240 ResizeEdge::Bottom | ResizeEdge::BottomLeft | ResizeEdge::BottomRight => {
241 h = h.saturating_add(dy);
242 }
243 _ => {}
244 }
245
246 let min_w = i32::from(FLOATING_MIN_WIDTH);
248 let min_h = i32::from(FLOATING_MIN_HEIGHT);
249
250 if w < min_w {
251 match edge {
252 ResizeEdge::Left | ResizeEdge::TopLeft | ResizeEdge::BottomLeft => {
253 x = x.saturating_sub(min_w.saturating_sub(w));
254 }
255 _ => {}
256 }
257 w = min_w;
258 }
259 if h < min_h {
260 match edge {
261 ResizeEdge::Top | ResizeEdge::TopLeft | ResizeEdge::TopRight => {
262 y = y.saturating_sub(min_h.saturating_sub(h));
263 }
264 _ => {}
265 }
266 h = min_h;
267 }
268
269 let mut width = w.max(1).min(i32::from(u16::MAX)) as u16;
271 let mut height = h.max(1).min(i32::from(u16::MAX)) as u16;
272
273 if !allow_offscreen {
275 width = width.min(bounds.width);
276 height = height.min(bounds.height);
277
278 let bounds_x1 = bounds.x.saturating_add(i32::from(bounds.width));
279 let bounds_y1 = bounds.y.saturating_add(i32::from(bounds.height));
280 let max_x = bounds_x1.saturating_sub(i32::from(width));
281 let max_y = bounds_y1.saturating_sub(i32::from(height));
282
283 x = x.max(bounds.x).min(max_x);
284 y = y.max(bounds.y).min(max_y);
285 }
286
287 LayoutRect {
288 x,
289 y,
290 width,
291 height,
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn area() -> LayoutRect {
300 LayoutRect {
301 x: 0,
302 y: 0,
303 width: 80,
304 height: 24,
305 }
306 }
307
308 #[test]
309 fn resize_handles_count() {
310 let rect = LayoutRect {
311 x: 10,
312 y: 10,
313 width: 20,
314 height: 15,
315 };
316 let handles = resize_handles_for_region(1u8, rect, area());
317 assert_eq!(handles.len(), 8);
318 }
319
320 #[test]
321 fn resize_handles_small_rect() {
322 let rect = LayoutRect {
323 x: 10,
324 y: 10,
325 width: 1,
326 height: 1,
327 };
328 let handles = resize_handles_for_region(1u8, rect, area());
329 assert_eq!(handles.len(), 4);
331 }
332
333 #[test]
334 fn floating_header_normal() {
335 let rect = LayoutRect {
336 x: 10,
337 y: 10,
338 width: 20,
339 height: 20,
340 };
341 let header = floating_header_for_region(1u8, rect, area());
342 assert!(header.is_some());
343 let h = header.unwrap();
344 assert_eq!(h.rect.width, 18);
345 assert_eq!(h.rect.height, 1);
346 assert_eq!(h.rect.y, 11);
347 }
348
349 #[test]
350 fn floating_header_too_small() {
351 let rect = LayoutRect {
352 x: 10,
353 y: 10,
354 width: 2,
355 height: 2,
356 };
357 assert!(floating_header_for_region(1u8, rect, area()).is_none());
358 }
359
360 #[test]
361 fn apply_resize_drag_right_edge() {
362 let result = apply_resize_drag_signed(
363 10,
364 10,
365 20,
366 15,
367 ResizeEdge::Right,
368 50,
369 10,
370 30,
371 10,
372 area(),
373 false,
374 );
375 assert_eq!(result.width, 40); assert_eq!(result.x, 10);
377 }
378
379 #[test]
380 fn apply_resize_drag_left_edge() {
381 let result = apply_resize_drag_signed(
382 20,
383 10,
384 20,
385 15,
386 ResizeEdge::Left,
387 10,
388 10,
389 30,
390 10,
391 area(),
392 false,
393 );
394 assert_eq!(result.x, 0); assert_eq!(result.width, 40); }
397
398 #[test]
399 fn apply_resize_drag_enforces_min_size() {
400 let result = apply_resize_drag_signed(
402 10,
403 10,
404 10,
405 10,
406 ResizeEdge::Left,
407 20,
408 10,
409 5,
410 10,
411 area(),
412 false,
413 );
414 assert_eq!(result.width, FLOATING_MIN_WIDTH);
415 }
416
417 #[test]
418 fn apply_resize_drag_offscreen_allowed() {
419 let result =
420 apply_resize_drag_signed(0, 0, 80, 24, ResizeEdge::Left, 40, 0, 0, 0, area(), true);
421 assert_eq!(result.x, 40);
422 assert_eq!(result.width, 40);
423 }
424}