rush_sync_server/ui/
viewport.rs

1#[derive(Debug, Clone)]
2pub struct Viewport {
3    terminal_width: u16,
4    terminal_height: u16,
5    output_area: LayoutArea,
6    input_area: LayoutArea,
7    content_height: usize,
8    window_height: usize,
9    scroll_offset: usize,
10    auto_scroll_enabled: bool,
11    min_terminal_height: u16,
12    min_terminal_width: u16,
13}
14
15#[derive(Debug, Clone, Copy)]
16pub struct LayoutArea {
17    pub x: u16,
18    pub y: u16,
19    pub width: u16,
20    pub height: u16,
21}
22
23#[derive(Debug, Clone)]
24pub enum ViewportEvent {
25    TerminalResized {
26        width: u16,
27        height: u16,
28    },
29    ContentChanged {
30        new_height: usize,
31    },
32    ScrollRequest {
33        direction: ScrollDirection,
34        amount: usize,
35    },
36    ForceAutoScroll,
37}
38
39#[derive(Debug, Clone)]
40pub enum ScrollDirection {
41    Up,
42    Down,
43    ToTop,
44    ToBottom,
45    PageUp,
46    PageDown,
47}
48
49impl LayoutArea {
50    pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
51        Self {
52            x,
53            y,
54            width,
55            height,
56        }
57    }
58
59    pub fn is_valid(&self) -> bool {
60        self.width > 0 && self.height > 0
61    }
62
63    pub fn as_rect(&self) -> ratatui::layout::Rect {
64        ratatui::layout::Rect {
65            x: self.x,
66            y: self.y,
67            width: self.width,
68            height: self.height,
69        }
70    }
71}
72
73impl Viewport {
74    pub fn new(terminal_width: u16, terminal_height: u16) -> Self {
75        let mut viewport = Self {
76            terminal_width: terminal_width.max(40),
77            terminal_height: terminal_height.max(10),
78            output_area: LayoutArea::new(0, 0, 0, 0),
79            input_area: LayoutArea::new(0, 0, 0, 0),
80            content_height: 0,
81            window_height: 0,
82            scroll_offset: 0,
83            auto_scroll_enabled: true,
84            min_terminal_height: 10,
85            min_terminal_width: 40,
86        };
87        viewport.calculate_layout();
88        viewport
89    }
90
91    pub fn update_terminal_size(&mut self, width: u16, height: u16) -> bool {
92        let new_width = width.max(self.min_terminal_width);
93        let new_height = height.max(self.min_terminal_height);
94        let changed = self.terminal_width != new_width || self.terminal_height != new_height;
95
96        if changed {
97            self.terminal_width = new_width;
98            self.terminal_height = new_height;
99            self.calculate_layout();
100            self.adjust_scroll_after_resize();
101        }
102        changed
103    }
104
105    fn calculate_layout(&mut self) {
106        // Validate and fix dimensions
107        if self.terminal_width < 10 || self.terminal_height < 5 {
108            self.terminal_width = self.terminal_width.max(10);
109            self.terminal_height = self.terminal_height.max(5);
110        }
111
112        let margin = 1u16;
113        let available_height = self.terminal_height.saturating_sub(margin * 2);
114
115        // Calculate heights with safety checks
116        let input_height = match available_height {
117            h if h >= 5 => 3,
118            h if h >= 3 => 2,
119            _ => 2,
120        }
121        .min(available_height.saturating_sub(1));
122
123        let output_height = available_height.saturating_sub(input_height).max(1);
124
125        // Create layout areas with emergency fallback
126        if input_height < 2 || output_height < 1 {
127            self.create_emergency_layout(margin);
128        } else {
129            self.create_normal_layout(margin, output_height, input_height);
130        }
131
132        self.window_height = output_height.max(1) as usize;
133        self.validate_layout();
134    }
135
136    fn create_emergency_layout(&mut self, margin: u16) {
137        let width = self.terminal_width.saturating_sub(margin * 2).max(1);
138        self.output_area = LayoutArea::new(
139            margin,
140            margin,
141            width,
142            self.terminal_height.saturating_sub(3).max(1),
143        );
144        self.input_area = LayoutArea::new(margin, self.output_area.height + margin, width, 2);
145    }
146
147    fn create_normal_layout(&mut self, margin: u16, output_height: u16, input_height: u16) {
148        let width = self.terminal_width.saturating_sub(margin * 2).max(1);
149        self.output_area = LayoutArea::new(margin, margin, width, output_height);
150        self.input_area = LayoutArea::new(margin, margin + output_height, width, input_height);
151    }
152
153    fn validate_layout(&mut self) {
154        if !self.output_area.is_valid() || !self.input_area.is_valid() {
155            self.output_area = LayoutArea::new(
156                0,
157                0,
158                self.terminal_width.max(1),
159                self.terminal_height.saturating_sub(2).max(1),
160            );
161            self.input_area =
162                LayoutArea::new(0, self.output_area.height, self.terminal_width.max(1), 2);
163            self.window_height = self.output_area.height.max(1) as usize;
164        }
165    }
166
167    // Scroll operations - simplified and consolidated
168    pub fn scroll_up(&mut self, lines: usize) {
169        if lines > 0 {
170            self.disable_auto_scroll();
171        }
172        self.scroll_offset = self.scroll_offset.saturating_sub(lines.max(1));
173    }
174
175    pub fn scroll_down(&mut self, lines: usize) {
176        self.scroll_offset = self.scroll_offset.saturating_add(lines.max(1));
177        self.clamp_scroll_offset();
178        if self.is_at_bottom() {
179            self.enable_auto_scroll();
180        }
181    }
182
183    pub fn scroll_to_top(&mut self) {
184        self.disable_auto_scroll();
185        self.scroll_offset = 0;
186    }
187
188    pub fn scroll_to_bottom(&mut self) {
189        self.scroll_offset = self.max_scroll_offset();
190        self.auto_scroll_enabled = true;
191    }
192
193    pub fn page_up(&mut self) {
194        self.scroll_up(self.window_height.saturating_sub(1).max(1));
195    }
196
197    pub fn page_down(&mut self) {
198        self.scroll_down(self.window_height.saturating_sub(1).max(1));
199    }
200
201    // Content and auto-scroll management
202    pub fn update_content_height(&mut self, new_content_height: usize) {
203        self.content_height = new_content_height;
204        self.clamp_scroll_offset();
205    }
206
207    pub fn update_content_height_silent(&mut self, new_content_height: usize) {
208        self.content_height = new_content_height;
209        self.clamp_scroll_offset();
210    }
211
212    pub fn set_scroll_offset_direct_silent(&mut self, offset: usize) {
213        self.scroll_offset = offset.min(self.max_scroll_offset());
214    }
215
216    pub fn enable_auto_scroll_silent(&mut self) {
217        self.auto_scroll_enabled = true;
218    }
219
220    pub fn force_auto_scroll(&mut self) {
221        self.enable_auto_scroll_silent();
222        self.scroll_to_bottom();
223    }
224
225    pub fn set_scroll_offset_direct(&mut self, offset: usize) {
226        self.scroll_offset = offset;
227        self.clamp_scroll_offset();
228    }
229
230    pub fn enable_auto_scroll(&mut self) {
231        self.auto_scroll_enabled = true;
232    }
233
234    pub fn disable_auto_scroll(&mut self) {
235        self.auto_scroll_enabled = false;
236    }
237
238    // View calculations
239    pub fn get_visible_range(&self) -> (usize, usize) {
240        if self.content_height == 0 || self.window_height == 0 {
241            return (0, 0);
242        }
243        let start = self.scroll_offset;
244        let end = (start + self.window_height).min(self.content_height);
245        (start, end)
246    }
247
248    // Getters - streamlined
249    pub fn output_area(&self) -> LayoutArea {
250        self.output_area
251    }
252    pub fn input_area(&self) -> LayoutArea {
253        self.input_area
254    }
255    pub fn window_height(&self) -> usize {
256        self.window_height
257    }
258    pub fn content_height(&self) -> usize {
259        self.content_height
260    }
261    pub fn scroll_offset(&self) -> usize {
262        self.scroll_offset
263    }
264    pub fn is_auto_scroll_enabled(&self) -> bool {
265        self.auto_scroll_enabled
266    }
267    pub fn terminal_size(&self) -> (u16, u16) {
268        (self.terminal_width, self.terminal_height)
269    }
270
271    pub fn is_usable(&self) -> bool {
272        self.terminal_width >= self.min_terminal_width
273            && self.terminal_height >= self.min_terminal_height
274            && self.output_area.is_valid()
275            && self.input_area.is_valid()
276    }
277
278    pub fn debug_info(&self) -> String {
279        format!("Viewport: {}x{}, output: {}x{}+{}+{}, input: {}x{}+{}+{}, content: {}, window: {}, offset: {}, auto: {}, at_bottom: {}, max_offset: {}",
280            self.terminal_width, self.terminal_height,
281            self.output_area.width, self.output_area.height, self.output_area.x, self.output_area.y,
282            self.input_area.width, self.input_area.height, self.input_area.x, self.input_area.y,
283            self.content_height, self.window_height, self.scroll_offset, self.auto_scroll_enabled,
284            self.is_at_bottom(), self.max_scroll_offset())
285    }
286
287    pub fn short_debug(&self) -> String {
288        format!(
289            "{}x{}, content: {}, offset: {}",
290            self.terminal_width, self.terminal_height, self.content_height, self.scroll_offset
291        )
292    }
293
294    // Event handling - consolidated
295    pub fn handle_event(&mut self, event: ViewportEvent) -> bool {
296        match event {
297            ViewportEvent::TerminalResized { width, height } => {
298                self.update_terminal_size(width, height)
299            }
300            ViewportEvent::ContentChanged { new_height } => {
301                self.update_content_height(new_height);
302                true
303            }
304            ViewportEvent::ScrollRequest { direction, amount } => {
305                match direction {
306                    ScrollDirection::Up => self.scroll_up(amount),
307                    ScrollDirection::Down => self.scroll_down(amount),
308                    ScrollDirection::ToTop => self.scroll_to_top(),
309                    ScrollDirection::ToBottom => self.scroll_to_bottom(),
310                    ScrollDirection::PageUp => self.page_up(),
311                    ScrollDirection::PageDown => self.page_down(),
312                }
313                true
314            }
315            ViewportEvent::ForceAutoScroll => {
316                self.force_auto_scroll();
317                true
318            }
319        }
320    }
321
322    // Private helpers - streamlined
323    fn max_scroll_offset(&self) -> usize {
324        self.content_height.saturating_sub(self.window_height)
325    }
326
327    fn is_at_bottom(&self) -> bool {
328        let max_offset = self.max_scroll_offset();
329        self.scroll_offset >= max_offset || max_offset == 0
330    }
331
332    fn clamp_scroll_offset(&mut self) {
333        self.scroll_offset = self.scroll_offset.min(self.max_scroll_offset());
334    }
335
336    fn adjust_scroll_after_resize(&mut self) {
337        if self.auto_scroll_enabled {
338            self.scroll_to_bottom();
339        } else {
340            self.clamp_scroll_offset();
341        }
342    }
343}