Skip to main content

vtcode_design/
layout.rs

1//! Responsive layout mode logic.
2//!
3//! `LayoutMode` provides a single source of truth for layout decisions
4//! based on terminal dimensions. Extracted from `vtcode-tui`.
5
6use ratatui::layout::Rect;
7
8use crate::constants::{COMPACT_MAX_COLS, COMPACT_MAX_ROWS, WIDE_MIN_COLS, WIDE_MIN_ROWS};
9
10/// Responsive layout mode based on terminal dimensions.
11///
12/// This enum provides a single source of truth for layout decisions
13/// across the UI, enabling consistent responsive behavior.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum LayoutMode {
16    /// Minimal chrome for tiny terminals (< 80 cols or < 20 rows)
17    Compact,
18    /// Default layout for standard terminals
19    Standard,
20    /// Enhanced layout with sidebar for wide terminals (>= 120 cols, >= 24 rows)
21    Wide,
22}
23
24impl LayoutMode {
25    /// Determine layout mode from viewport dimensions.
26    pub fn from_area(area: Rect) -> Self {
27        if area.width < COMPACT_MAX_COLS || area.height < COMPACT_MAX_ROWS {
28            LayoutMode::Compact
29        } else if area.width >= WIDE_MIN_COLS && area.height >= WIDE_MIN_ROWS {
30            LayoutMode::Wide
31        } else {
32            LayoutMode::Standard
33        }
34    }
35
36    /// Check if borders should be shown.
37    pub fn show_borders(self) -> bool {
38        !matches!(self, LayoutMode::Compact)
39    }
40
41    /// Check if panel titles should be shown.
42    pub fn show_titles(self) -> bool {
43        !matches!(self, LayoutMode::Compact)
44    }
45
46    /// Check if sidebar can be shown.
47    pub fn allow_sidebar(self) -> bool {
48        matches!(self, LayoutMode::Wide)
49    }
50
51    /// Check if logs panel should be visible.
52    pub fn show_logs_panel(self) -> bool {
53        !matches!(self, LayoutMode::Compact)
54    }
55
56    /// Get the footer height for this mode.
57    /// Footer is disabled in all modes to avoid duplicating header info.
58    pub fn footer_height(self) -> u16 {
59        0
60    }
61
62    /// Check if footer should be shown.
63    /// Footer is disabled to avoid duplicating the header status bar.
64    pub fn show_footer(self) -> bool {
65        false
66    }
67
68    /// Get the maximum header height as percentage of viewport.
69    pub fn max_header_percent(self) -> f32 {
70        match self {
71            LayoutMode::Compact => 0.15,
72            LayoutMode::Standard => 0.25,
73            LayoutMode::Wide => 0.30,
74        }
75    }
76
77    /// Get the sidebar width percentage (only meaningful in Wide mode).
78    pub fn sidebar_width_percent(self) -> u16 {
79        match self {
80            LayoutMode::Wide => 28,
81            _ => 0,
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_compact_mode() {
92        let area = Rect::new(0, 0, 60, 15);
93        assert_eq!(LayoutMode::from_area(area), LayoutMode::Compact);
94    }
95
96    #[test]
97    fn test_standard_mode() {
98        let area = Rect::new(0, 0, 100, 30);
99        assert_eq!(LayoutMode::from_area(area), LayoutMode::Standard);
100    }
101
102    #[test]
103    fn test_wide_mode() {
104        let area = Rect::new(0, 0, 150, 40);
105        assert_eq!(LayoutMode::from_area(area), LayoutMode::Wide);
106    }
107
108    #[test]
109    fn test_mode_properties() {
110        // Compact: no borders, no footer, no sidebar
111        assert!(!LayoutMode::Compact.show_borders());
112        assert!(!LayoutMode::Compact.show_footer());
113        assert!(!LayoutMode::Compact.allow_sidebar());
114        assert_eq!(LayoutMode::Compact.footer_height(), 0);
115
116        // Standard: borders but no footer
117        assert!(LayoutMode::Standard.show_borders());
118        assert!(!LayoutMode::Standard.show_footer());
119        assert!(!LayoutMode::Standard.allow_sidebar());
120        assert_eq!(LayoutMode::Standard.footer_height(), 0);
121
122        // Wide: borders + sidebar, but no footer (header already shows status)
123        assert!(LayoutMode::Wide.show_borders());
124        assert!(!LayoutMode::Wide.show_footer());
125        assert!(LayoutMode::Wide.allow_sidebar());
126        assert_eq!(LayoutMode::Wide.footer_height(), 0);
127    }
128
129    #[test]
130    fn test_boundary_conditions() {
131        // Exactly at 80 cols should be Standard
132        let area_80 = Rect::new(0, 0, 80, 24);
133        assert_eq!(LayoutMode::from_area(area_80), LayoutMode::Standard);
134
135        // Exactly at 120 cols should be Wide
136        let area_120 = Rect::new(0, 0, 120, 24);
137        assert_eq!(LayoutMode::from_area(area_120), LayoutMode::Wide);
138
139        // 79 cols should be Compact
140        let area_79 = Rect::new(0, 0, 79, 24);
141        assert_eq!(LayoutMode::from_area(area_79), LayoutMode::Compact);
142
143        // Wide width but short height should be Standard (not Wide)
144        let area_wide_short = Rect::new(0, 0, 150, 20);
145        assert_eq!(LayoutMode::from_area(area_wide_short), LayoutMode::Standard);
146    }
147}