Skip to main content

revue/widget/layout/resizable/
mod.rs

1//! Resizable widget wrapper for dynamic sizing
2//!
3//! Wraps any widget and provides resize handles for interactive resizing.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use revue::widget::{Resizable, ResizeHandle, Text};
9//!
10//! Resizable::new(Text::new("Resizable content"))
11//!     .min_size(10, 5)
12//!     .max_size(80, 40)
13//!     .handles(ResizeHandle::ALL)
14//!     .on_resize(|w, h| println!("New size: {}x{}", w, h))
15//! ```
16
17mod 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
27/// Resizable widget wrapper
28pub struct Resizable<F = fn(u16, u16)>
29where
30    F: FnMut(u16, u16),
31{
32    /// Current width
33    width: u16,
34    /// Current height
35    height: u16,
36    /// Minimum width
37    min_width: u16,
38    /// Minimum height
39    min_height: u16,
40    /// Maximum width (0 = unlimited)
41    max_width: u16,
42    /// Maximum height (0 = unlimited)
43    max_height: u16,
44    /// Enabled resize handles
45    handles: Vec<ResizeHandle>,
46    /// Handle size (corner area)
47    handle_size: u16,
48    /// Visual style
49    style: ResizeStyle,
50    /// Handle color
51    handle_color: Color,
52    /// Active handle color
53    active_color: Color,
54    /// Currently resizing
55    resizing: bool,
56    /// Active resize direction
57    resize_direction: ResizeDirection,
58    /// Hovered handle
59    hovered_handle: Option<ResizeHandle>,
60    /// Resize callback
61    on_resize: Option<F>,
62    /// Preserve aspect ratio
63    preserve_aspect: bool,
64    /// Initial aspect ratio (width/height)
65    aspect_ratio: f32,
66    /// Snap to grid
67    snap_to_grid: Option<(u16, u16)>,
68    /// Widget state
69    state: WidgetState,
70    /// Widget props
71    props: WidgetProps,
72}
73
74impl Resizable<fn(u16, u16)> {
75    /// Create a new resizable wrapper with initial dimensions
76    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    /// Set minimum size
107    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    /// Set maximum size
114    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    /// Set allowed handles
121    pub fn handles(mut self, handles: &[ResizeHandle]) -> Self {
122        self.handles = handles.to_vec();
123        self
124    }
125
126    /// Set visual style
127    pub fn style(mut self, style: ResizeStyle) -> Self {
128        self.style = style;
129        self
130    }
131
132    /// Set handle color
133    pub fn handle_color(mut self, color: Color) -> Self {
134        self.handle_color = color;
135        self
136    }
137
138    /// Set active color
139    pub fn active_color(mut self, color: Color) -> Self {
140        self.active_color = color;
141        self
142    }
143
144    /// Preserve aspect ratio during resize
145    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    /// Set custom aspect ratio
152    pub fn aspect_ratio(mut self, ratio: f32) -> Self {
153        self.preserve_aspect = true;
154        self.aspect_ratio = ratio;
155        self
156    }
157
158    /// Snap size to grid
159    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    /// Set resize callback
165    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    /// Get current size
194    pub fn size(&self) -> (u16, u16) {
195        (self.width, self.height)
196    }
197
198    /// Set size directly
199    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    /// Get content area (inside borders)
206    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    /// Check if currently resizing
220    pub fn is_resizing(&self) -> bool {
221        self.resizing
222    }
223
224    /// Start resize operation
225    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    /// End resize operation
233    pub fn end_resize(&mut self) {
234        self.resizing = false;
235        self.resize_direction = ResizeDirection::NONE;
236    }
237
238    /// Apply resize delta
239    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    /// Constrain size within limits
270    fn constrain_size(&self, mut width: u16, mut height: u16) -> (u16, u16) {
271        // Apply grid snapping
272        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        // Apply min/max constraints
278        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        // Apply aspect ratio
289        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                // Adjust height to match aspect ratio
293                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    /// Hit test for handle at position
307    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    /// Set hovered handle
317    pub fn set_hovered(&mut self, handle: Option<ResizeHandle>) {
318        self.hovered_handle = handle;
319    }
320
321    /// Handle keyboard resize
322    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    /// Draw resize handles
362    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        // Top border
391        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        // Bottom border
406        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        // Side borders
422        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        // Highlight active handle
434        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
499/// Create a new resizable wrapper
500pub fn resizable(width: u16, height: u16) -> Resizable<fn(u16, u16)> {
501    Resizable::new(width, height)
502}
503
504#[cfg(test)]
505mod tests {
506    //! Resizable widget tests
507
508    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        // Height should adjust to maintain 2:1 ratio
537        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        // Bottom-right corner
574        assert!(ResizeHandle::BottomRight.hit_test(19, 9, area, 1));
575        assert!(!ResizeHandle::BottomRight.hit_test(10, 5, area, 1));
576
577        // Top edge
578        assert!(ResizeHandle::Top.hit_test(10, 0, area, 1));
579        assert!(!ResizeHandle::Top.hit_test(0, 0, area, 1)); // Corner, not edge
580    }
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    // ==================== ResizeHandle Tests ====================
606
607    #[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        // Top edge (middle section, excluding corners)
652        assert!(ResizeHandle::Top.hit_test(10, 0, area, 1));
653        assert!(ResizeHandle::Top.hit_test(5, 0, area, 1));
654        // Corner positions should not match top edge
655        assert!(!ResizeHandle::Top.hit_test(0, 0, area, 1));
656        assert!(!ResizeHandle::Top.hit_test(19, 0, area, 1));
657        // Wrong row
658        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        // Bottom edge (middle section)
665        assert!(ResizeHandle::Bottom.hit_test(10, 9, area, 1));
666        // Corners should not match
667        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        // Left edge (middle section)
675        assert!(ResizeHandle::Left.hit_test(0, 5, area, 1));
676        // Corners should not match
677        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        // Right edge (middle section)
685        assert!(ResizeHandle::Right.hit_test(19, 5, area, 1));
686        // Corners should not match
687        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    // ==================== ResizeDirection Tests ====================
716
717    #[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    // ==================== ResizeStyle Tests ====================
768
769    #[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    // ==================== Builder Methods Tests ====================
791
792    #[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    // ==================== Callback Tests ====================
830
831    #[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    // ==================== Keyboard Handling Tests ====================
859
860    #[test]
861    fn test_resizable_handle_key_not_focused() {
862        let mut r = Resizable::new(20, 10);
863        // Not focused by default
864        let handled = r.handle_key(&Key::Right);
865        assert!(!handled);
866        assert_eq!(r.size(), (20, 10)); // Size unchanged
867    }
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        // Right handle not in CORNERS
924        let handled = r.handle_key(&Key::Right);
925        assert!(!handled);
926        assert_eq!(r.size(), (20, 10));
927    }
928
929    // ==================== handle_at Tests ====================
930
931    #[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        // Test corner detection
937        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        // Test center (no handle)
943        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    // ==================== Edge Cases ====================
959
960    #[test]
961    fn test_resizable_min_size_enforced() {
962        let r = Resizable::new(0, 0);
963        // Minimum is 1x1 even when created with 0x0
964        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        // max_height = 0 means unlimited
978        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); // Top is not in CORNERS
986        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        // Not in resizing state
993        r.apply_delta(10, 10);
994        // Size should not change
995        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        // Left direction means -1, so delta -5 * -1 = +5
1004        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        // No border padding for Dots style
1014        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    // ==================== Rendering Tests ====================
1021
1022    #[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        // Check border characters
1032        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        // Check corner dots
1048        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        // Hidden style should not draw anything
1064        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        // Subtle style without hover should not draw border
1077        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        // Subtle style with hover should draw border
1092        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        // Should still render border with active color
1107        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        // Border should be rendered with highlighted corner
1122        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        // Test BottomRight
1138        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    // ==================== Helper Function Tests ====================
1146
1147    #[test]
1148    fn test_resizable_helper_function() {
1149        let r = resizable(30, 15);
1150        assert_eq!(r.size(), (30, 15));
1151    }
1152
1153    // ==================== Aspect Ratio Edge Cases ====================
1154
1155    #[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        // Width clamped to 50, height adjusted for 2:1 ratio
1163        assert_eq!(r.width, 50);
1164        // Height should be 25 for 2:1, but clamped to max 20
1165        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        // 23 rounds to 20 (23 + 5 = 28, 28/10 = 2, 2*10 = 20)
1173        r.set_size(23, 14);
1174        assert_eq!(r.size(), (20, 10));
1175
1176        // 27 rounds to 30
1177        r.set_size(27, 16);
1178        assert_eq!(r.size(), (30, 20));
1179    }
1180}