reovim_plugin_microscope/microscope/
layout.rs

1//! Helix-style layout calculations for microscope
2//!
3//! Layout is bottom-anchored with horizontal split:
4//! - Results panel (40% width) on the left
5//! - Preview panel (60% width) on the right
6//! - Picker takes 40% of screen height, anchored to bottom
7//!
8//! ```text
9//! +---------------------------------------------------------------+
10//! |                      Editor Content                            |
11//! +------------------ y = screen_height - picker_height -----------+
12//! | Results (40%)                    | Preview (60%)               |
13//! | +-----------------------------+  | +---------------------------+|
14//! | | > query_                [I] |  | | Preview: filename         ||
15//! | | --------------------------- |  | | -------------------------  ||
16//! | | > selected_item.rs          |  | | 1  fn main() {            ||
17//! | |   another_file.rs           |  | | 2      ...                 ||
18//! | |   third_file.rs             |  | | 3  }                       ||
19//! | +-----------------------------+  | +---------------------------+||
20//! +----------------------------------+------------------------------+
21//! | 4/128 files                          <CR> Open | <Esc> Close   |
22//! +---------------------------------------------------------------+
23//! ```
24
25/// Layout configuration for microscope
26#[derive(Debug, Clone, Copy)]
27pub struct LayoutConfig {
28    /// Percentage of screen height for picker (0.0 - 1.0)
29    pub height_ratio: f32,
30    /// Percentage of width for results panel (0.0 - 1.0)
31    pub results_width_ratio: f32,
32    /// Minimum height in rows
33    pub min_height: u16,
34    /// Minimum width in columns
35    pub min_width: u16,
36    /// Whether to show preview panel
37    pub show_preview: bool,
38    /// Padding from screen edges
39    pub padding: u16,
40}
41
42impl Default for LayoutConfig {
43    fn default() -> Self {
44        Self {
45            height_ratio: 0.4,        // 40% of screen height
46            results_width_ratio: 0.4, // 40% for results, 60% for preview
47            min_height: 10,
48            min_width: 40,
49            show_preview: true,
50            padding: 0,
51        }
52    }
53}
54
55/// Calculated layout bounds for microscope panels
56#[derive(Debug, Clone, Copy, Default)]
57pub struct LayoutBounds {
58    /// Results panel bounds
59    pub results: PanelBounds,
60    /// Preview panel bounds (if enabled)
61    pub preview: Option<PanelBounds>,
62    /// Status line bounds (bottom row)
63    pub status: PanelBounds,
64    /// Total picker bounds
65    pub total: PanelBounds,
66}
67
68/// Bounds for a single panel
69#[derive(Debug, Clone, Copy, Default)]
70pub struct PanelBounds {
71    pub x: u16,
72    pub y: u16,
73    pub width: u16,
74    pub height: u16,
75}
76
77impl PanelBounds {
78    /// Create new panel bounds
79    #[must_use]
80    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
81        Self {
82            x,
83            y,
84            width,
85            height,
86        }
87    }
88
89    /// Get the inner area (excluding borders)
90    #[must_use]
91    pub const fn inner(&self) -> Self {
92        Self {
93            x: self.x + 1,
94            y: self.y + 1,
95            width: self.width.saturating_sub(2),
96            height: self.height.saturating_sub(2),
97        }
98    }
99
100    /// Check if a point is within bounds
101    #[must_use]
102    pub const fn contains(&self, x: u16, y: u16) -> bool {
103        x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
104    }
105}
106
107/// Calculate layout bounds for microscope
108#[must_use]
109#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
110pub fn calculate_layout(
111    screen_width: u16,
112    screen_height: u16,
113    config: &LayoutConfig,
114) -> LayoutBounds {
115    // Calculate total picker dimensions
116    let total_height = ((f32::from(screen_height) * config.height_ratio) as u16)
117        .max(config.min_height)
118        .min(screen_height.saturating_sub(config.padding * 2));
119
120    let total_width = screen_width
121        .saturating_sub(config.padding * 2)
122        .max(config.min_width);
123
124    // Bottom-anchored position
125    let x = config.padding;
126    let y = screen_height.saturating_sub(total_height + config.padding);
127
128    let total = PanelBounds::new(x, y, total_width, total_height);
129
130    // Status line takes bottom row
131    let status_height = 1;
132    let content_height = total_height.saturating_sub(status_height);
133
134    let status = PanelBounds::new(x, y + content_height, total_width, status_height);
135
136    // Calculate results and preview panels
137    let (results, preview) = if config.show_preview && total_width > 60 {
138        let results_width = ((f32::from(total_width) * config.results_width_ratio) as u16).max(20);
139        let preview_width = total_width.saturating_sub(results_width);
140
141        let results = PanelBounds::new(x, y, results_width, content_height);
142        let preview = PanelBounds::new(x + results_width, y, preview_width, content_height);
143
144        (results, Some(preview))
145    } else {
146        // No preview - results take full width
147        let results = PanelBounds::new(x, y, total_width, content_height);
148        (results, None)
149    };
150
151    LayoutBounds {
152        results,
153        preview,
154        status,
155        total,
156    }
157}
158
159/// Number of visible items based on panel height
160#[must_use]
161pub const fn visible_item_count(panel: &PanelBounds) -> usize {
162    // Height minus: border top (1) + prompt line (1) + separator (1) + border bottom (1)
163    panel.height.saturating_sub(4) as usize
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_default_config() {
172        let config = LayoutConfig::default();
173        assert!((config.height_ratio - 0.4).abs() < f32::EPSILON);
174        assert!((config.results_width_ratio - 0.4).abs() < f32::EPSILON);
175        assert!(config.show_preview);
176    }
177
178    #[test]
179    fn test_calculate_layout() {
180        let config = LayoutConfig::default();
181        let bounds = calculate_layout(100, 50, &config);
182
183        // Should be bottom-anchored
184        assert_eq!(bounds.total.y + bounds.total.height, 50);
185
186        // Should have preview when width > 60
187        assert!(bounds.preview.is_some());
188
189        // Results should be 40% width
190        assert_eq!(bounds.results.width, 40);
191    }
192
193    #[test]
194    fn test_no_preview_narrow_screen() {
195        let config = LayoutConfig::default();
196        let bounds = calculate_layout(50, 30, &config);
197
198        // Narrow screen should not show preview
199        assert!(bounds.preview.is_none());
200        assert_eq!(bounds.results.width, 50);
201    }
202
203    #[test]
204    fn test_panel_inner() {
205        let panel = PanelBounds::new(10, 20, 30, 15);
206        let inner = panel.inner();
207
208        assert_eq!(inner.x, 11);
209        assert_eq!(inner.y, 21);
210        assert_eq!(inner.width, 28);
211        assert_eq!(inner.height, 13);
212    }
213
214    #[test]
215    fn test_visible_item_count() {
216        let panel = PanelBounds::new(0, 0, 40, 20);
217        assert_eq!(visible_item_count(&panel), 16); // 20 - 4 = 16
218    }
219}