1mod types;
18
19pub use types::{ResizeDirection, ResizeHandle, ResizeStyle};
20
21use crate::event::Key;
22use crate::layout::Rect;
23use crate::style::Color;
24use crate::widget::traits::{RenderContext, View, WidgetProps, WidgetState};
25use crate::{impl_styled_view, impl_view_meta, impl_widget_builders};
26
27pub struct Resizable<F = fn(u16, u16)>
29where
30 F: FnMut(u16, u16),
31{
32 width: u16,
34 height: u16,
36 min_width: u16,
38 min_height: u16,
40 max_width: u16,
42 max_height: u16,
44 handles: Vec<ResizeHandle>,
46 handle_size: u16,
48 style: ResizeStyle,
50 handle_color: Color,
52 active_color: Color,
54 resizing: bool,
56 resize_direction: ResizeDirection,
58 hovered_handle: Option<ResizeHandle>,
60 on_resize: Option<F>,
62 preserve_aspect: bool,
64 aspect_ratio: f32,
66 snap_to_grid: Option<(u16, u16)>,
68 state: WidgetState,
70 props: WidgetProps,
72}
73
74impl Resizable<fn(u16, u16)> {
75 pub fn new(width: u16, height: u16) -> Self {
77 Self {
78 width: width.max(1),
79 height: height.max(1),
80 min_width: 3,
81 min_height: 3,
82 max_width: 0,
83 max_height: 0,
84 handles: ResizeHandle::ALL.to_vec(),
85 handle_size: 1,
86 style: ResizeStyle::default(),
87 handle_color: Color::rgb(100, 100, 100),
88 active_color: Color::CYAN,
89 resizing: false,
90 resize_direction: ResizeDirection::NONE,
91 hovered_handle: None,
92 on_resize: None,
93 preserve_aspect: false,
94 aspect_ratio: width as f32 / height.max(1) as f32,
95 snap_to_grid: None,
96 state: WidgetState::new(),
97 props: WidgetProps::new(),
98 }
99 }
100}
101
102impl<F> Resizable<F>
103where
104 F: FnMut(u16, u16),
105{
106 pub fn min_size(mut self, width: u16, height: u16) -> Self {
108 self.min_width = width.max(1);
109 self.min_height = height.max(1);
110 self
111 }
112
113 pub fn max_size(mut self, width: u16, height: u16) -> Self {
115 self.max_width = width;
116 self.max_height = height;
117 self
118 }
119
120 pub fn handles(mut self, handles: &[ResizeHandle]) -> Self {
122 self.handles = handles.to_vec();
123 self
124 }
125
126 pub fn style(mut self, style: ResizeStyle) -> Self {
128 self.style = style;
129 self
130 }
131
132 pub fn handle_color(mut self, color: Color) -> Self {
134 self.handle_color = color;
135 self
136 }
137
138 pub fn active_color(mut self, color: Color) -> Self {
140 self.active_color = color;
141 self
142 }
143
144 pub fn preserve_aspect_ratio(mut self) -> Self {
146 self.preserve_aspect = true;
147 self.aspect_ratio = self.width as f32 / self.height.max(1) as f32;
148 self
149 }
150
151 pub fn aspect_ratio(mut self, ratio: f32) -> Self {
153 self.preserve_aspect = true;
154 self.aspect_ratio = ratio;
155 self
156 }
157
158 pub fn snap_to_grid(mut self, grid_width: u16, grid_height: u16) -> Self {
160 self.snap_to_grid = Some((grid_width.max(1), grid_height.max(1)));
161 self
162 }
163
164 pub fn on_resize<G>(self, handler: G) -> Resizable<G>
166 where
167 G: FnMut(u16, u16),
168 {
169 Resizable {
170 width: self.width,
171 height: self.height,
172 min_width: self.min_width,
173 min_height: self.min_height,
174 max_width: self.max_width,
175 max_height: self.max_height,
176 handles: self.handles,
177 handle_size: self.handle_size,
178 style: self.style,
179 handle_color: self.handle_color,
180 active_color: self.active_color,
181 resizing: self.resizing,
182 resize_direction: self.resize_direction,
183 hovered_handle: self.hovered_handle,
184 on_resize: Some(handler),
185 preserve_aspect: self.preserve_aspect,
186 aspect_ratio: self.aspect_ratio,
187 snap_to_grid: self.snap_to_grid,
188 state: self.state,
189 props: self.props,
190 }
191 }
192
193 pub fn size(&self) -> (u16, u16) {
195 (self.width, self.height)
196 }
197
198 pub fn set_size(&mut self, width: u16, height: u16) {
200 let (w, h) = self.constrain_size(width, height);
201 self.width = w;
202 self.height = h;
203 }
204
205 pub fn content_area(&self, area: Rect) -> Rect {
207 let border = match self.style {
208 ResizeStyle::Border => 1,
209 _ => 0,
210 };
211 Rect::new(
212 area.x + border,
213 area.y + border,
214 self.width.saturating_sub(border * 2),
215 self.height.saturating_sub(border * 2),
216 )
217 }
218
219 pub fn is_resizing(&self) -> bool {
221 self.resizing
222 }
223
224 pub fn start_resize(&mut self, handle: ResizeHandle) {
226 if self.handles.contains(&handle) {
227 self.resizing = true;
228 self.resize_direction = ResizeDirection::from_handle(handle);
229 }
230 }
231
232 pub fn end_resize(&mut self) {
234 self.resizing = false;
235 self.resize_direction = ResizeDirection::NONE;
236 }
237
238 pub fn apply_delta(&mut self, dx: i16, dy: i16) {
240 if !self.resizing {
241 return;
242 }
243
244 let new_width = if self.resize_direction.horizontal != 0 {
245 let delta = dx * self.resize_direction.horizontal as i16;
246 (self.width as i16 + delta).max(1) as u16
247 } else {
248 self.width
249 };
250
251 let new_height = if self.resize_direction.vertical != 0 {
252 let delta = dy * self.resize_direction.vertical as i16;
253 (self.height as i16 + delta).max(1) as u16
254 } else {
255 self.height
256 };
257
258 let (w, h) = self.constrain_size(new_width, new_height);
259
260 if w != self.width || h != self.height {
261 self.width = w;
262 self.height = h;
263 if let Some(ref mut callback) = self.on_resize {
264 callback(w, h);
265 }
266 }
267 }
268
269 fn constrain_size(&self, mut width: u16, mut height: u16) -> (u16, u16) {
271 if let Some((gw, gh)) = self.snap_to_grid {
273 width = ((width + gw / 2) / gw) * gw;
274 height = ((height + gh / 2) / gh) * gh;
275 }
276
277 width = width.max(self.min_width);
279 height = height.max(self.min_height);
280
281 if self.max_width > 0 {
282 width = width.min(self.max_width);
283 }
284 if self.max_height > 0 {
285 height = height.min(self.max_height);
286 }
287
288 if self.preserve_aspect {
290 let current_ratio = width as f32 / height.max(1) as f32;
291 if (current_ratio - self.aspect_ratio).abs() > 0.01 {
292 let new_height = (width as f32 / self.aspect_ratio)
294 .max(0.0)
295 .min(u16::MAX as f32) as u16;
296 height = new_height.max(self.min_height);
297 if self.max_height > 0 {
298 height = height.min(self.max_height);
299 }
300 }
301 }
302
303 (width.max(1), height.max(1))
304 }
305
306 pub fn handle_at(&self, x: u16, y: u16, area: Rect) -> Option<ResizeHandle> {
308 for handle in &self.handles {
309 if handle.hit_test(x, y, area, self.handle_size) {
310 return Some(*handle);
311 }
312 }
313 None
314 }
315
316 pub fn set_hovered(&mut self, handle: Option<ResizeHandle>) {
318 self.hovered_handle = handle;
319 }
320
321 pub fn handle_key(&mut self, key: &Key) -> bool {
323 if !self.state.focused {
324 return false;
325 }
326
327 let delta = 1i16;
328 match key {
329 Key::Left if self.handles.contains(&ResizeHandle::Right) => {
330 self.resize_direction = ResizeDirection::from_handle(ResizeHandle::Right);
331 self.resizing = true;
332 self.apply_delta(-delta, 0);
333 self.resizing = false;
334 true
335 }
336 Key::Right if self.handles.contains(&ResizeHandle::Right) => {
337 self.resize_direction = ResizeDirection::from_handle(ResizeHandle::Right);
338 self.resizing = true;
339 self.apply_delta(delta, 0);
340 self.resizing = false;
341 true
342 }
343 Key::Up if self.handles.contains(&ResizeHandle::Bottom) => {
344 self.resize_direction = ResizeDirection::from_handle(ResizeHandle::Bottom);
345 self.resizing = true;
346 self.apply_delta(0, -delta);
347 self.resizing = false;
348 true
349 }
350 Key::Down if self.handles.contains(&ResizeHandle::Bottom) => {
351 self.resize_direction = ResizeDirection::from_handle(ResizeHandle::Bottom);
352 self.resizing = true;
353 self.apply_delta(0, delta);
354 self.resizing = false;
355 true
356 }
357 _ => false,
358 }
359 }
360
361 fn draw_handles(&self, ctx: &mut RenderContext) {
363 let area = ctx.area;
364
365 match self.style {
366 ResizeStyle::Border => {
367 self.draw_border(ctx, area);
368 }
369 ResizeStyle::Subtle => {
370 if self.hovered_handle.is_some() || self.resizing {
371 self.draw_border(ctx, area);
372 }
373 }
374 ResizeStyle::Dots => {
375 self.draw_corner_dots(ctx, area);
376 }
377 ResizeStyle::Hidden => {}
378 }
379 }
380
381 fn draw_border(&self, ctx: &mut RenderContext, area: Rect) {
382 let color = if self.resizing {
383 self.active_color
384 } else if self.hovered_handle.is_some() {
385 Color::rgb(150, 150, 150)
386 } else {
387 self.handle_color
388 };
389
390 for x in area.x..area.x + self.width.min(area.width) {
392 if let Some(cell) = ctx.buffer.get_mut(x, area.y) {
393 let ch = if x == area.x {
394 '┌'
395 } else if x == area.x + self.width - 1 {
396 '┐'
397 } else {
398 '─'
399 };
400 cell.symbol = ch;
401 cell.fg = Some(color);
402 }
403 }
404
405 let bottom_y = area.y + self.height.saturating_sub(1);
407 for x in area.x..area.x + self.width.min(area.width) {
408 if let Some(cell) = ctx.buffer.get_mut(x, bottom_y) {
409 let ch = if x == area.x {
410 '└'
411 } else if x == area.x + self.width - 1 {
412 '┘'
413 } else {
414 '─'
415 };
416 cell.symbol = ch;
417 cell.fg = Some(color);
418 }
419 }
420
421 for y in (area.y + 1)..bottom_y {
423 if let Some(cell) = ctx.buffer.get_mut(area.x, y) {
424 cell.symbol = '│';
425 cell.fg = Some(color);
426 }
427 if let Some(cell) = ctx.buffer.get_mut(area.x + self.width - 1, y) {
428 cell.symbol = '│';
429 cell.fg = Some(color);
430 }
431 }
432
433 if let Some(handle) = self.hovered_handle {
435 let active_color = self.active_color;
436 match handle {
437 ResizeHandle::TopLeft => {
438 if let Some(cell) = ctx.buffer.get_mut(area.x, area.y) {
439 cell.fg = Some(active_color);
440 }
441 }
442 ResizeHandle::TopRight => {
443 if let Some(cell) = ctx.buffer.get_mut(area.x + self.width - 1, area.y) {
444 cell.fg = Some(active_color);
445 }
446 }
447 ResizeHandle::BottomLeft => {
448 if let Some(cell) = ctx.buffer.get_mut(area.x, bottom_y) {
449 cell.fg = Some(active_color);
450 }
451 }
452 ResizeHandle::BottomRight => {
453 if let Some(cell) = ctx.buffer.get_mut(area.x + self.width - 1, bottom_y) {
454 cell.fg = Some(active_color);
455 }
456 }
457 _ => {}
458 }
459 }
460 }
461
462 fn draw_corner_dots(&self, ctx: &mut RenderContext, area: Rect) {
463 let color = if self.resizing {
464 self.active_color
465 } else {
466 self.handle_color
467 };
468
469 let corners = [
470 (area.x, area.y),
471 (area.x + self.width - 1, area.y),
472 (area.x, area.y + self.height - 1),
473 (area.x + self.width - 1, area.y + self.height - 1),
474 ];
475
476 for (x, y) in corners {
477 if let Some(cell) = ctx.buffer.get_mut(x, y) {
478 cell.symbol = '●';
479 cell.fg = Some(color);
480 }
481 }
482 }
483}
484
485impl<F> View for Resizable<F>
486where
487 F: FnMut(u16, u16),
488{
489 fn render(&self, ctx: &mut RenderContext) {
490 self.draw_handles(ctx);
491 }
492
493 impl_view_meta!("Resizable");
494}
495
496impl_styled_view!(Resizable);
497impl_widget_builders!(Resizable);
498
499pub fn resizable(width: u16, height: u16) -> Resizable<fn(u16, u16)> {
501 Resizable::new(width, height)
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
509 use crate::layout::Rect;
510 use crate::render::Buffer;
511 use crate::style::Color;
512 use crate::widget::traits::RenderContext;
513 use crate::widget::traits::View;
514
515 #[test]
516 fn test_resizable_new() {
517 let r = Resizable::new(20, 10);
518 assert_eq!(r.size(), (20, 10));
519 }
520
521 #[test]
522 fn test_resizable_constraints() {
523 let mut r = Resizable::new(20, 10).min_size(5, 5).max_size(50, 30);
524
525 r.set_size(3, 3);
526 assert_eq!(r.size(), (5, 5));
527
528 r.set_size(100, 100);
529 assert_eq!(r.size(), (50, 30));
530 }
531
532 #[test]
533 fn test_resizable_aspect_ratio() {
534 let mut r = Resizable::new(20, 10).preserve_aspect_ratio();
535 r.set_size(40, 10);
536 assert_eq!(r.width, 40);
538 assert_eq!(r.height, 20);
539 }
540
541 #[test]
542 fn test_resizable_grid_snap() {
543 let mut r = Resizable::new(20, 10).snap_to_grid(5, 5);
544 r.set_size(23, 12);
545 assert_eq!(r.size(), (25, 10));
546 }
547
548 #[test]
549 fn test_resizable_handles() {
550 let r = Resizable::new(20, 10).handles(ResizeHandle::CORNERS);
551 assert_eq!(r.handles.len(), 4);
552 assert!(r.handles.contains(&ResizeHandle::TopLeft));
553 assert!(!r.handles.contains(&ResizeHandle::Top));
554 }
555
556 #[test]
557 fn test_resize_operation() {
558 let mut r = Resizable::new(20, 10);
559 r.start_resize(ResizeHandle::BottomRight);
560 assert!(r.is_resizing());
561
562 r.apply_delta(5, 3);
563 assert_eq!(r.size(), (25, 13));
564
565 r.end_resize();
566 assert!(!r.is_resizing());
567 }
568
569 #[test]
570 fn test_handle_hit_test() {
571 let area = Rect::new(0, 0, 20, 10);
572
573 assert!(ResizeHandle::BottomRight.hit_test(19, 9, area, 1));
575 assert!(!ResizeHandle::BottomRight.hit_test(10, 5, area, 1));
576
577 assert!(ResizeHandle::Top.hit_test(10, 0, area, 1));
579 assert!(!ResizeHandle::Top.hit_test(0, 0, area, 1)); }
581
582 #[test]
583 fn test_resize_direction() {
584 let dir = ResizeDirection::from_handle(ResizeHandle::BottomRight);
585 assert_eq!(dir.horizontal, 1);
586 assert_eq!(dir.vertical, 1);
587
588 let dir = ResizeDirection::from_handle(ResizeHandle::Left);
589 assert_eq!(dir.horizontal, -1);
590 assert_eq!(dir.vertical, 0);
591 }
592
593 #[test]
594 fn test_content_area() {
595 let r = Resizable::new(20, 10).style(ResizeStyle::Border);
596 let area = Rect::new(5, 5, 20, 10);
597 let content = r.content_area(area);
598
599 assert_eq!(content.x, 6);
600 assert_eq!(content.y, 6);
601 assert_eq!(content.width, 18);
602 assert_eq!(content.height, 8);
603 }
604
605 #[test]
608 fn test_resize_handle_all_constant() {
609 assert_eq!(ResizeHandle::ALL.len(), 8);
610 assert!(ResizeHandle::ALL.contains(&ResizeHandle::Top));
611 assert!(ResizeHandle::ALL.contains(&ResizeHandle::Bottom));
612 assert!(ResizeHandle::ALL.contains(&ResizeHandle::Left));
613 assert!(ResizeHandle::ALL.contains(&ResizeHandle::Right));
614 assert!(ResizeHandle::ALL.contains(&ResizeHandle::TopLeft));
615 assert!(ResizeHandle::ALL.contains(&ResizeHandle::TopRight));
616 assert!(ResizeHandle::ALL.contains(&ResizeHandle::BottomLeft));
617 assert!(ResizeHandle::ALL.contains(&ResizeHandle::BottomRight));
618 }
619
620 #[test]
621 fn test_resize_handle_edges_constant() {
622 assert_eq!(ResizeHandle::EDGES.len(), 4);
623 assert!(ResizeHandle::EDGES.contains(&ResizeHandle::Top));
624 assert!(ResizeHandle::EDGES.contains(&ResizeHandle::Bottom));
625 assert!(ResizeHandle::EDGES.contains(&ResizeHandle::Left));
626 assert!(ResizeHandle::EDGES.contains(&ResizeHandle::Right));
627 assert!(!ResizeHandle::EDGES.contains(&ResizeHandle::TopLeft));
628 }
629
630 #[test]
631 fn test_resize_handle_corners_constant() {
632 assert_eq!(ResizeHandle::CORNERS.len(), 4);
633 assert!(ResizeHandle::CORNERS.contains(&ResizeHandle::TopLeft));
634 assert!(ResizeHandle::CORNERS.contains(&ResizeHandle::TopRight));
635 assert!(ResizeHandle::CORNERS.contains(&ResizeHandle::BottomLeft));
636 assert!(ResizeHandle::CORNERS.contains(&ResizeHandle::BottomRight));
637 assert!(!ResizeHandle::CORNERS.contains(&ResizeHandle::Top));
638 }
639
640 #[test]
641 fn test_resize_handle_debug_clone_eq() {
642 let handle = ResizeHandle::TopLeft;
643 let cloned = handle;
644 assert_eq!(handle, cloned);
645 let _ = format!("{:?}", handle);
646 }
647
648 #[test]
649 fn test_resize_handle_hit_test_top() {
650 let area = Rect::new(0, 0, 20, 10);
651 assert!(ResizeHandle::Top.hit_test(10, 0, area, 1));
653 assert!(ResizeHandle::Top.hit_test(5, 0, area, 1));
654 assert!(!ResizeHandle::Top.hit_test(0, 0, area, 1));
656 assert!(!ResizeHandle::Top.hit_test(19, 0, area, 1));
657 assert!(!ResizeHandle::Top.hit_test(10, 5, area, 1));
659 }
660
661 #[test]
662 fn test_resize_handle_hit_test_bottom() {
663 let area = Rect::new(0, 0, 20, 10);
664 assert!(ResizeHandle::Bottom.hit_test(10, 9, area, 1));
666 assert!(!ResizeHandle::Bottom.hit_test(0, 9, area, 1));
668 assert!(!ResizeHandle::Bottom.hit_test(19, 9, area, 1));
669 }
670
671 #[test]
672 fn test_resize_handle_hit_test_left() {
673 let area = Rect::new(0, 0, 20, 10);
674 assert!(ResizeHandle::Left.hit_test(0, 5, area, 1));
676 assert!(!ResizeHandle::Left.hit_test(0, 0, area, 1));
678 assert!(!ResizeHandle::Left.hit_test(0, 9, area, 1));
679 }
680
681 #[test]
682 fn test_resize_handle_hit_test_right() {
683 let area = Rect::new(0, 0, 20, 10);
684 assert!(ResizeHandle::Right.hit_test(19, 5, area, 1));
686 assert!(!ResizeHandle::Right.hit_test(19, 0, area, 1));
688 assert!(!ResizeHandle::Right.hit_test(19, 9, area, 1));
689 }
690
691 #[test]
692 fn test_resize_handle_hit_test_top_left() {
693 let area = Rect::new(0, 0, 20, 10);
694 assert!(ResizeHandle::TopLeft.hit_test(0, 0, area, 1));
695 assert!(ResizeHandle::TopLeft.hit_test(1, 1, area, 1));
696 assert!(!ResizeHandle::TopLeft.hit_test(10, 5, area, 1));
697 }
698
699 #[test]
700 fn test_resize_handle_hit_test_top_right() {
701 let area = Rect::new(0, 0, 20, 10);
702 assert!(ResizeHandle::TopRight.hit_test(19, 0, area, 1));
703 assert!(ResizeHandle::TopRight.hit_test(18, 1, area, 1));
704 assert!(!ResizeHandle::TopRight.hit_test(10, 5, area, 1));
705 }
706
707 #[test]
708 fn test_resize_handle_hit_test_bottom_left() {
709 let area = Rect::new(0, 0, 20, 10);
710 assert!(ResizeHandle::BottomLeft.hit_test(0, 9, area, 1));
711 assert!(ResizeHandle::BottomLeft.hit_test(1, 8, area, 1));
712 assert!(!ResizeHandle::BottomLeft.hit_test(10, 5, area, 1));
713 }
714
715 #[test]
718 fn test_resize_direction_none() {
719 let dir = ResizeDirection::NONE;
720 assert_eq!(dir.horizontal, 0);
721 assert_eq!(dir.vertical, 0);
722 }
723
724 #[test]
725 fn test_resize_direction_from_handle_all() {
726 let top = ResizeDirection::from_handle(ResizeHandle::Top);
727 assert_eq!(top.horizontal, 0);
728 assert_eq!(top.vertical, -1);
729
730 let bottom = ResizeDirection::from_handle(ResizeHandle::Bottom);
731 assert_eq!(bottom.horizontal, 0);
732 assert_eq!(bottom.vertical, 1);
733
734 let left = ResizeDirection::from_handle(ResizeHandle::Left);
735 assert_eq!(left.horizontal, -1);
736 assert_eq!(left.vertical, 0);
737
738 let right = ResizeDirection::from_handle(ResizeHandle::Right);
739 assert_eq!(right.horizontal, 1);
740 assert_eq!(right.vertical, 0);
741
742 let top_left = ResizeDirection::from_handle(ResizeHandle::TopLeft);
743 assert_eq!(top_left.horizontal, -1);
744 assert_eq!(top_left.vertical, -1);
745
746 let top_right = ResizeDirection::from_handle(ResizeHandle::TopRight);
747 assert_eq!(top_right.horizontal, 1);
748 assert_eq!(top_right.vertical, -1);
749
750 let bottom_left = ResizeDirection::from_handle(ResizeHandle::BottomLeft);
751 assert_eq!(bottom_left.horizontal, -1);
752 assert_eq!(bottom_left.vertical, 1);
753
754 let bottom_right = ResizeDirection::from_handle(ResizeHandle::BottomRight);
755 assert_eq!(bottom_right.horizontal, 1);
756 assert_eq!(bottom_right.vertical, 1);
757 }
758
759 #[test]
760 fn test_resize_direction_debug_clone_eq() {
761 let dir = ResizeDirection::NONE;
762 let cloned = dir;
763 assert_eq!(dir, cloned);
764 let _ = format!("{:?}", dir);
765 }
766
767 #[test]
770 fn test_resize_style_default() {
771 assert_eq!(ResizeStyle::default(), ResizeStyle::Border);
772 }
773
774 #[test]
775 fn test_resize_style_debug_clone_eq() {
776 let style = ResizeStyle::Subtle;
777 let cloned = style;
778 assert_eq!(style, cloned);
779 let _ = format!("{:?}", style);
780 }
781
782 #[test]
783 fn test_resize_style_variants() {
784 let _border = ResizeStyle::Border;
785 let _subtle = ResizeStyle::Subtle;
786 let _hidden = ResizeStyle::Hidden;
787 let _dots = ResizeStyle::Dots;
788 }
789
790 #[test]
793 fn test_resizable_handle_color() {
794 let r = Resizable::new(20, 10).handle_color(Color::RED);
795 assert_eq!(r.handle_color, Color::RED);
796 }
797
798 #[test]
799 fn test_resizable_active_color() {
800 let r = Resizable::new(20, 10).active_color(Color::GREEN);
801 assert_eq!(r.active_color, Color::GREEN);
802 }
803
804 #[test]
805 fn test_resizable_custom_aspect_ratio() {
806 let r = Resizable::new(20, 10).aspect_ratio(4.0);
807 assert!(r.preserve_aspect);
808 assert!((r.aspect_ratio - 4.0).abs() < 0.01);
809 }
810
811 #[test]
812 fn test_resizable_style_subtle() {
813 let r = Resizable::new(20, 10).style(ResizeStyle::Subtle);
814 assert_eq!(r.style, ResizeStyle::Subtle);
815 }
816
817 #[test]
818 fn test_resizable_style_hidden() {
819 let r = Resizable::new(20, 10).style(ResizeStyle::Hidden);
820 assert_eq!(r.style, ResizeStyle::Hidden);
821 }
822
823 #[test]
824 fn test_resizable_style_dots() {
825 let r = Resizable::new(20, 10).style(ResizeStyle::Dots);
826 assert_eq!(r.style, ResizeStyle::Dots);
827 }
828
829 #[test]
832 fn test_resizable_on_resize_callback() {
833 use std::cell::Cell;
834 use std::rc::Rc;
835
836 let called = Rc::new(Cell::new(false));
837 let width_received = Rc::new(Cell::new(0u16));
838 let height_received = Rc::new(Cell::new(0u16));
839
840 let called_clone = called.clone();
841 let width_clone = width_received.clone();
842 let height_clone = height_received.clone();
843
844 let mut r = Resizable::new(20, 10).on_resize(move |w, h| {
845 called_clone.set(true);
846 width_clone.set(w);
847 height_clone.set(h);
848 });
849
850 r.start_resize(ResizeHandle::BottomRight);
851 r.apply_delta(5, 3);
852
853 assert!(called.get());
854 assert_eq!(width_received.get(), 25);
855 assert_eq!(height_received.get(), 13);
856 }
857
858 #[test]
861 fn test_resizable_handle_key_not_focused() {
862 let mut r = Resizable::new(20, 10);
863 let handled = r.handle_key(&Key::Right);
865 assert!(!handled);
866 assert_eq!(r.size(), (20, 10)); }
868
869 #[test]
870 fn test_resizable_handle_key_right() {
871 let mut r = Resizable::new(20, 10);
872 r.state.focused = true;
873
874 let handled = r.handle_key(&Key::Right);
875 assert!(handled);
876 assert_eq!(r.size(), (21, 10));
877 }
878
879 #[test]
880 fn test_resizable_handle_key_left() {
881 let mut r = Resizable::new(20, 10);
882 r.state.focused = true;
883
884 let handled = r.handle_key(&Key::Left);
885 assert!(handled);
886 assert_eq!(r.size(), (19, 10));
887 }
888
889 #[test]
890 fn test_resizable_handle_key_down() {
891 let mut r = Resizable::new(20, 10);
892 r.state.focused = true;
893
894 let handled = r.handle_key(&Key::Down);
895 assert!(handled);
896 assert_eq!(r.size(), (20, 11));
897 }
898
899 #[test]
900 fn test_resizable_handle_key_up() {
901 let mut r = Resizable::new(20, 10);
902 r.state.focused = true;
903
904 let handled = r.handle_key(&Key::Up);
905 assert!(handled);
906 assert_eq!(r.size(), (20, 9));
907 }
908
909 #[test]
910 fn test_resizable_handle_key_unhandled() {
911 let mut r = Resizable::new(20, 10);
912 r.state.focused = true;
913
914 let handled = r.handle_key(&Key::Enter);
915 assert!(!handled);
916 }
917
918 #[test]
919 fn test_resizable_handle_key_without_handle() {
920 let mut r = Resizable::new(20, 10).handles(ResizeHandle::CORNERS);
921 r.state.focused = true;
922
923 let handled = r.handle_key(&Key::Right);
925 assert!(!handled);
926 assert_eq!(r.size(), (20, 10));
927 }
928
929 #[test]
932 fn test_resizable_handle_at() {
933 let r = Resizable::new(20, 10);
934 let area = Rect::new(0, 0, 20, 10);
935
936 assert_eq!(r.handle_at(0, 0, area), Some(ResizeHandle::TopLeft));
938 assert_eq!(r.handle_at(19, 0, area), Some(ResizeHandle::TopRight));
939 assert_eq!(r.handle_at(0, 9, area), Some(ResizeHandle::BottomLeft));
940 assert_eq!(r.handle_at(19, 9, area), Some(ResizeHandle::BottomRight));
941
942 assert_eq!(r.handle_at(10, 5, area), None);
944 }
945
946 #[test]
947 fn test_resizable_set_hovered() {
948 let mut r = Resizable::new(20, 10);
949 assert_eq!(r.hovered_handle, None);
950
951 r.set_hovered(Some(ResizeHandle::TopLeft));
952 assert_eq!(r.hovered_handle, Some(ResizeHandle::TopLeft));
953
954 r.set_hovered(None);
955 assert_eq!(r.hovered_handle, None);
956 }
957
958 #[test]
961 fn test_resizable_min_size_enforced() {
962 let r = Resizable::new(0, 0);
963 assert_eq!(r.size(), (1, 1));
965 }
966
967 #[test]
968 fn test_resizable_min_constraint_enforced() {
969 let mut r = Resizable::new(20, 10).min_size(10, 5);
970 r.set_size(1, 1);
971 assert_eq!(r.size(), (10, 5));
972 }
973
974 #[test]
975 fn test_resizable_max_only() {
976 let mut r = Resizable::new(20, 10).max_size(30, 0);
977 r.set_size(40, 100);
979 assert_eq!(r.size(), (30, 100));
980 }
981
982 #[test]
983 fn test_resizable_start_resize_invalid_handle() {
984 let mut r = Resizable::new(20, 10).handles(ResizeHandle::CORNERS);
985 r.start_resize(ResizeHandle::Top); assert!(!r.is_resizing());
987 }
988
989 #[test]
990 fn test_resizable_apply_delta_not_resizing() {
991 let mut r = Resizable::new(20, 10);
992 r.apply_delta(10, 10);
994 assert_eq!(r.size(), (20, 10));
996 }
997
998 #[test]
999 fn test_resizable_apply_delta_negative() {
1000 let mut r = Resizable::new(20, 10);
1001 r.start_resize(ResizeHandle::Left);
1002 r.apply_delta(-5, 0);
1003 assert_eq!(r.size(), (25, 10));
1005 }
1006
1007 #[test]
1008 fn test_content_area_non_border_style() {
1009 let r = Resizable::new(20, 10).style(ResizeStyle::Dots);
1010 let area = Rect::new(5, 5, 20, 10);
1011 let content = r.content_area(area);
1012
1013 assert_eq!(content.x, 5);
1015 assert_eq!(content.y, 5);
1016 assert_eq!(content.width, 20);
1017 assert_eq!(content.height, 10);
1018 }
1019
1020 #[test]
1023 fn test_resizable_render_border() {
1024 let r = Resizable::new(10, 5).style(ResizeStyle::Border);
1025 let mut buffer = Buffer::new(20, 10);
1026 let rect = Rect::new(0, 0, 20, 10);
1027 let mut ctx = RenderContext::new(&mut buffer, rect);
1028
1029 r.render(&mut ctx);
1030
1031 assert_eq!(buffer.get(0, 0).unwrap().symbol, '┌');
1033 assert_eq!(buffer.get(9, 0).unwrap().symbol, '┐');
1034 assert_eq!(buffer.get(0, 4).unwrap().symbol, '└');
1035 assert_eq!(buffer.get(9, 4).unwrap().symbol, '┘');
1036 }
1037
1038 #[test]
1039 fn test_resizable_render_dots() {
1040 let r = Resizable::new(10, 5).style(ResizeStyle::Dots);
1041 let mut buffer = Buffer::new(20, 10);
1042 let rect = Rect::new(0, 0, 20, 10);
1043 let mut ctx = RenderContext::new(&mut buffer, rect);
1044
1045 r.render(&mut ctx);
1046
1047 assert_eq!(buffer.get(0, 0).unwrap().symbol, '●');
1049 assert_eq!(buffer.get(9, 0).unwrap().symbol, '●');
1050 assert_eq!(buffer.get(0, 4).unwrap().symbol, '●');
1051 assert_eq!(buffer.get(9, 4).unwrap().symbol, '●');
1052 }
1053
1054 #[test]
1055 fn test_resizable_render_hidden() {
1056 let r = Resizable::new(10, 5).style(ResizeStyle::Hidden);
1057 let mut buffer = Buffer::new(20, 10);
1058 let rect = Rect::new(0, 0, 20, 10);
1059 let mut ctx = RenderContext::new(&mut buffer, rect);
1060
1061 r.render(&mut ctx);
1062
1063 assert_eq!(buffer.get(0, 0).unwrap().symbol, ' ');
1065 }
1066
1067 #[test]
1068 fn test_resizable_render_subtle_not_hovered() {
1069 let r = Resizable::new(10, 5).style(ResizeStyle::Subtle);
1070 let mut buffer = Buffer::new(20, 10);
1071 let rect = Rect::new(0, 0, 20, 10);
1072 let mut ctx = RenderContext::new(&mut buffer, rect);
1073
1074 r.render(&mut ctx);
1075
1076 assert_eq!(buffer.get(0, 0).unwrap().symbol, ' ');
1078 }
1079
1080 #[test]
1081 fn test_resizable_render_subtle_hovered() {
1082 let mut r = Resizable::new(10, 5).style(ResizeStyle::Subtle);
1083 r.set_hovered(Some(ResizeHandle::TopLeft));
1084
1085 let mut buffer = Buffer::new(20, 10);
1086 let rect = Rect::new(0, 0, 20, 10);
1087 let mut ctx = RenderContext::new(&mut buffer, rect);
1088
1089 r.render(&mut ctx);
1090
1091 assert_eq!(buffer.get(0, 0).unwrap().symbol, '┌');
1093 }
1094
1095 #[test]
1096 fn test_resizable_render_while_resizing() {
1097 let mut r = Resizable::new(10, 5).style(ResizeStyle::Border);
1098 r.start_resize(ResizeHandle::BottomRight);
1099
1100 let mut buffer = Buffer::new(20, 10);
1101 let rect = Rect::new(0, 0, 20, 10);
1102 let mut ctx = RenderContext::new(&mut buffer, rect);
1103
1104 r.render(&mut ctx);
1105
1106 assert_eq!(buffer.get(0, 0).unwrap().symbol, '┌');
1108 }
1109
1110 #[test]
1111 fn test_resizable_render_with_hovered_corner() {
1112 let mut r = Resizable::new(10, 5).style(ResizeStyle::Border);
1113 r.set_hovered(Some(ResizeHandle::TopRight));
1114
1115 let mut buffer = Buffer::new(20, 10);
1116 let rect = Rect::new(0, 0, 20, 10);
1117 let mut ctx = RenderContext::new(&mut buffer, rect);
1118
1119 r.render(&mut ctx);
1120
1121 assert_eq!(buffer.get(9, 0).unwrap().symbol, '┐');
1123 }
1124
1125 #[test]
1126 fn test_resizable_render_hovered_bottom_corners() {
1127 let mut r = Resizable::new(10, 5).style(ResizeStyle::Border);
1128 r.set_hovered(Some(ResizeHandle::BottomLeft));
1129
1130 let mut buffer = Buffer::new(20, 10);
1131 let rect = Rect::new(0, 0, 20, 10);
1132 let mut ctx = RenderContext::new(&mut buffer, rect);
1133
1134 r.render(&mut ctx);
1135 assert_eq!(buffer.get(0, 4).unwrap().symbol, '└');
1136
1137 r.set_hovered(Some(ResizeHandle::BottomRight));
1139 let mut buffer2 = Buffer::new(20, 10);
1140 let mut ctx2 = RenderContext::new(&mut buffer2, rect);
1141 r.render(&mut ctx2);
1142 assert_eq!(buffer2.get(9, 4).unwrap().symbol, '┘');
1143 }
1144
1145 #[test]
1148 fn test_resizable_helper_function() {
1149 let r = resizable(30, 15);
1150 assert_eq!(r.size(), (30, 15));
1151 }
1152
1153 #[test]
1156 fn test_aspect_ratio_with_max_constraint() {
1157 let mut r = Resizable::new(20, 10)
1158 .preserve_aspect_ratio()
1159 .max_size(50, 20);
1160
1161 r.set_size(60, 10);
1162 assert_eq!(r.width, 50);
1164 assert!(r.height <= 20);
1166 }
1167
1168 #[test]
1169 fn test_grid_snap_rounds() {
1170 let mut r = Resizable::new(20, 10).snap_to_grid(10, 10);
1171
1172 r.set_size(23, 14);
1174 assert_eq!(r.size(), (20, 10));
1175
1176 r.set_size(27, 16);
1178 assert_eq!(r.size(), (30, 20));
1179 }
1180}