rush_sync_server/ui/
viewport.rs

1// =====================================================
2// FILE: src/ui/viewport.rs - VERBESSERTES SANFTES SCROLLING
3// =====================================================
4
5/// Zentrale Viewport-Verwaltung für alle Dimensionen
6/// Löst alle Layout-Math-Probleme durch einheitliche Berechnung
7use crate::i18n::get_translation;
8
9#[derive(Debug, Clone)]
10pub struct Viewport {
11    // Terminal-Dimensionen
12    terminal_width: u16,
13    terminal_height: u16,
14
15    // Layout-Bereiche (absolut)
16    output_area: LayoutArea,
17    input_area: LayoutArea,
18
19    // Content-Dimensionen
20    content_height: usize,
21    window_height: usize,
22
23    // Scroll-Position
24    scroll_offset: usize,
25    auto_scroll_enabled: bool,
26
27    // Safety margins
28    min_terminal_height: u16,
29    min_terminal_width: u16,
30}
31
32#[derive(Debug, Clone, Copy)]
33pub struct LayoutArea {
34    pub x: u16,
35    pub y: u16,
36    pub width: u16,
37    pub height: u16,
38}
39
40impl LayoutArea {
41    pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
42        Self {
43            x,
44            y,
45            width,
46            height,
47        }
48    }
49
50    pub fn is_valid(&self) -> bool {
51        self.width > 0 && self.height > 0
52    }
53
54    pub fn as_rect(&self) -> ratatui::layout::Rect {
55        ratatui::layout::Rect {
56            x: self.x,
57            y: self.y,
58            width: self.width,
59            height: self.height,
60        }
61    }
62}
63
64impl Viewport {
65    /// Erstellt neuen Viewport mit sicheren Defaults
66    pub fn new(terminal_width: u16, terminal_height: u16) -> Self {
67        let mut viewport = Self {
68            terminal_width: terminal_width.max(40),   // Minimum 40 Zeichen
69            terminal_height: terminal_height.max(10), // Minimum 10 Zeilen
70            output_area: LayoutArea::new(0, 0, 0, 0),
71            input_area: LayoutArea::new(0, 0, 0, 0),
72            content_height: 0,
73            window_height: 0,
74            scroll_offset: 0,
75            auto_scroll_enabled: true,
76            min_terminal_height: 10,
77            min_terminal_width: 40,
78        };
79
80        viewport.calculate_layout();
81        viewport
82    }
83
84    /// Aktualisiert Terminal-Größe und berechnet Layout neu
85    pub fn update_terminal_size(&mut self, width: u16, height: u16) -> bool {
86        let new_width = width.max(self.min_terminal_width);
87        let new_height = height.max(self.min_terminal_height);
88
89        let changed = self.terminal_width != new_width || self.terminal_height != new_height;
90
91        if changed {
92            log::debug!(
93                "📐 Viewport resize: {}x{} → {}x{}",
94                self.terminal_width,
95                self.terminal_height,
96                new_width,
97                new_height
98            );
99
100            self.terminal_width = new_width;
101            self.terminal_height = new_height;
102            self.calculate_layout();
103
104            // Bei Resize: Scroll-Position anpassen
105            self.adjust_scroll_after_resize();
106        }
107
108        changed
109    }
110
111    /// Berechnet alle Layout-Bereiche (ZENTRAL & ROBUST + PANIC-SAFE)
112    fn calculate_layout(&mut self) {
113        // ✅ PANIC-SAFE: Validiere Input-Dimensionen
114        if self.terminal_width < 10 || self.terminal_height < 5 {
115            log::error!(
116                "{}",
117                get_translation(
118                    "viewport.layout.too_small",
119                    &[
120                        &self.terminal_width.to_string(),
121                        &self.terminal_height.to_string()
122                    ]
123                )
124            );
125            self.terminal_width = self.terminal_width.max(10);
126            self.terminal_height = self.terminal_height.max(5);
127        }
128
129        // Sichere Margin-Berechnung
130        let margin = 1_u16;
131        let available_height = self.terminal_height.saturating_sub(margin * 2);
132
133        // ✅ PANIC-SAFE: Mindest-Größen garantieren
134        let min_input_height = 2_u16;
135        let min_output_height = 1_u16;
136
137        // Input braucht mindestens 2, optimal 3 Zeilen
138        let input_height = if available_height >= 5 {
139            3
140        } else if available_height >= 3 {
141            2
142        } else {
143            min_input_height
144        }
145        .min(available_height.saturating_sub(min_output_height));
146
147        let output_height = available_height
148            .saturating_sub(input_height)
149            .max(min_output_height);
150
151        // ✅ PANIC-SAFE: Final validation
152        if input_height < min_input_height || output_height < min_output_height {
153            log::error!(
154                "{}",
155                get_translation(
156                    "viewport.layout.failed",
157                    &[
158                        &input_height.to_string(),
159                        &output_height.to_string(),
160                        &available_height.to_string()
161                    ]
162                )
163            );
164
165            // Emergency fallback
166            let emergency_input = min_input_height;
167            let emergency_output = available_height.saturating_sub(emergency_input);
168
169            self.output_area = LayoutArea::new(
170                margin,
171                margin,
172                self.terminal_width.saturating_sub(margin * 2).max(1),
173                emergency_output.max(1),
174            );
175            self.input_area = LayoutArea::new(
176                margin,
177                margin + emergency_output,
178                self.terminal_width.saturating_sub(margin * 2).max(1),
179                emergency_input,
180            );
181        } else {
182            // Normal layout
183            self.output_area = LayoutArea::new(
184                margin,
185                margin,
186                self.terminal_width.saturating_sub(margin * 2).max(1),
187                output_height,
188            );
189
190            self.input_area = LayoutArea::new(
191                margin,
192                margin + output_height,
193                self.terminal_width.saturating_sub(margin * 2).max(1),
194                input_height,
195            );
196        }
197
198        // Window-Höhe für Scroll-Berechnungen (panic-safe)
199        self.window_height = output_height.max(1) as usize;
200
201        // ✅ PANIC-SAFE: Validierung mit besserer Fehlerbehandlung
202        let total_used = self.output_area.height + self.input_area.height + margin * 2;
203        if total_used != self.terminal_height {
204            log::warn!(
205                "{}",
206                get_translation(
207                    "viewport.layout.mismatch",
208                    &[
209                        &self.terminal_height.to_string(),
210                        &total_used.to_string(),
211                        &self.output_area.height.to_string(),
212                        &self.input_area.height.to_string(),
213                        &(margin * 2).to_string()
214                    ]
215                )
216            );
217
218            // ✅ NICHT PANIKEN - nur loggen und weiter
219            if total_used > self.terminal_height + 2 {
220                // Toleranz von 2 Zeilen
221                log::error!("{}", get_translation("viewport.layout.broken", &[]));
222
223                // Emergency layout
224                self.output_area = LayoutArea::new(
225                    0,
226                    0,
227                    self.terminal_width,
228                    self.terminal_height.saturating_sub(3),
229                );
230                self.input_area = LayoutArea::new(
231                    0,
232                    self.terminal_height.saturating_sub(3),
233                    self.terminal_width,
234                    3,
235                );
236                self.window_height = self.output_area.height.max(1) as usize;
237            }
238        }
239
240        // ✅ FINAL SAFETY: Bereiche müssen gültig sein
241        if !self.output_area.is_valid() || !self.input_area.is_valid() {
242            log::error!("{}", get_translation("viewport.layout.invalid", &[]));
243
244            self.output_area = LayoutArea::new(
245                0,
246                0,
247                self.terminal_width.max(1),
248                self.terminal_height.saturating_sub(2).max(1),
249            );
250            self.input_area =
251                LayoutArea::new(0, self.output_area.height, self.terminal_width.max(1), 2);
252            self.window_height = self.output_area.height.max(1) as usize;
253        }
254
255        log::trace!(
256            "{}",
257            get_translation(
258                "viewport.layout.calculated",
259                &[
260                    &self.terminal_width.to_string(),
261                    &self.terminal_height.to_string(),
262                    &self.output_area.width.to_string(),
263                    &self.output_area.height.to_string(),
264                    &self.output_area.x.to_string(),
265                    &self.output_area.y.to_string(),
266                    &self.input_area.width.to_string(),
267                    &self.input_area.height.to_string(),
268                    &self.input_area.x.to_string(),
269                    &self.input_area.y.to_string(),
270                    &self.window_height.to_string()
271                ]
272            )
273        );
274    }
275
276    /// ✅ DEBUGGING: Content-Höhe Update mit detailliertem Logging
277    pub fn update_content_height(&mut self, new_content_height: usize) {
278        let old_height = self.content_height;
279        let old_max_offset = self.max_scroll_offset();
280
281        self.content_height = new_content_height;
282
283        let new_max_offset = self.max_scroll_offset();
284
285        // ✅ WICHTIG: Scroll-Bounds sicherstellen
286        self.clamp_scroll_offset();
287
288        let final_offset = self.scroll_offset;
289
290        log::debug!(
291            "📊 Viewport content height updated: {} → {} (window: {}, max_offset: {} → {}, scroll_offset: {})",
292            old_height,
293            new_content_height,
294            self.window_height,
295            old_max_offset,
296            new_max_offset,
297            final_offset
298        );
299
300        // ✅ VERIFICATION: Prüfe Konsistenz
301        if new_content_height > self.window_height && new_max_offset == 0 {
302            log::error!(
303                "🚨 Content height inconsistency! Content: {}, Window: {}, but max_offset is 0",
304                new_content_height,
305                self.window_height
306            );
307        }
308
309        if final_offset > new_max_offset {
310            log::error!(
311                "🚨 Scroll offset too high! Offset: {}, Max: {}",
312                final_offset,
313                new_max_offset
314            );
315        }
316    }
317
318    /// ✅ DIREKTES SCROLL-UP mit besserer Kontrolle
319    pub fn scroll_up(&mut self, lines: usize) {
320        // ✅ AUTO-SCROLL DEAKTIVIEREN beim manuellen Scrollen
321        if lines > 0 {
322            self.disable_auto_scroll();
323        }
324
325        let old_offset = self.scroll_offset;
326        let actual_lines = if lines == 0 { 1 } else { lines }; // Default: 1 Zeile
327        self.scroll_offset = self.scroll_offset.saturating_sub(actual_lines);
328
329        log::trace!(
330            "🔼 Scroll up: {} → {} (-{} lines)",
331            old_offset,
332            self.scroll_offset,
333            actual_lines
334        );
335    }
336
337    /// ✅ DIREKTES SCROLL-DOWN mit Auto-Scroll-Reaktivierung
338    pub fn scroll_down(&mut self, lines: usize) {
339        let old_offset = self.scroll_offset;
340        let actual_lines = if lines == 0 { 1 } else { lines }; // Default: 1 Zeile
341        self.scroll_offset = self.scroll_offset.saturating_add(actual_lines);
342
343        // ✅ WICHTIG: Clamp vor Auto-Scroll-Check
344        self.clamp_scroll_offset();
345
346        // ✅ AUTO-SCROLL reaktivieren wenn am Ende angelangt
347        if self.is_at_bottom() {
348            self.enable_auto_scroll();
349            log::trace!("✅ Auto-scroll re-enabled (reached bottom)");
350        }
351
352        log::trace!(
353            "🔽 Scroll down: {} → {} (+{} lines, auto_scroll: {})",
354            old_offset,
355            self.scroll_offset,
356            actual_lines,
357            self.auto_scroll_enabled
358        );
359    }
360
361    pub fn scroll_to_top(&mut self) {
362        self.disable_auto_scroll();
363        self.scroll_offset = 0;
364        log::trace!("🔝 Scroll to top");
365    }
366
367    /// ✅ DIREKTES Scroll to bottom
368    pub fn scroll_to_bottom(&mut self) {
369        let old_offset = self.scroll_offset;
370        self.scroll_offset = self.max_scroll_offset();
371        self.enable_auto_scroll();
372
373        log::trace!(
374            "🔚 Scroll to bottom: {} → {} (max_offset: {}, content: {}, window: {})",
375            old_offset,
376            self.scroll_offset,
377            self.max_scroll_offset(),
378            self.content_height,
379            self.window_height
380        );
381    }
382
383    /// ✅ SILENT VERSION: Content-Höhe Update ohne Logging (Anti-Loop)
384    pub fn update_content_height_silent(&mut self, new_content_height: usize) {
385        self.content_height = new_content_height;
386        self.clamp_scroll_offset();
387    }
388
389    /// ✅ SILENT VERSION: Direkte Scroll-Offset-Kontrolle ohne Logging (Anti-Loop)
390    pub fn set_scroll_offset_direct_silent(&mut self, offset: usize) {
391        self.scroll_offset = offset;
392        self.clamp_scroll_offset();
393    }
394
395    /// ✅ SILENT VERSION: Auto-Scroll aktivieren ohne Logging (Anti-Loop)
396    pub fn enable_auto_scroll_silent(&mut self) {
397        self.auto_scroll_enabled = true;
398    }
399
400    /// ✅ LEGACY-KOMPATIBILITÄT: Erzwingt Auto-scroll (nutzt jetzt Silent-Methoden)
401    pub fn force_auto_scroll(&mut self) {
402        self.enable_auto_scroll_silent();
403        self.scroll_to_bottom();
404    }
405
406    /// ✅ PAGE-SCROLLING Logik
407    pub fn page_up(&mut self) {
408        let page_size = self.window_height.saturating_sub(1).max(1);
409        log::trace!("📄 Page up: {} lines", page_size);
410        self.scroll_up(page_size);
411    }
412
413    pub fn page_down(&mut self) {
414        let page_size = self.window_height.saturating_sub(1).max(1);
415        log::trace!("📄 Page down: {} lines", page_size);
416        self.scroll_down(page_size);
417    }
418
419    /// ✅ NEUE METHODE: Direkte Scroll-Offset-Kontrolle (bypass Event-System)
420    pub fn set_scroll_offset_direct(&mut self, offset: usize) {
421        let old_offset = self.scroll_offset;
422        self.scroll_offset = offset;
423        self.clamp_scroll_offset();
424
425        log::trace!(
426            "📍 Direct scroll offset set: {} → {} (clamped to {})",
427            old_offset,
428            offset,
429            self.scroll_offset
430        );
431    }
432
433    /// ✅ NEUE METHODE: Auto-Scroll explizit aktivieren
434    pub fn enable_auto_scroll(&mut self) {
435        self.auto_scroll_enabled = true;
436        log::trace!("✅ Auto-scroll enabled");
437    }
438
439    /// ✅ NEUE METHODE: Auto-Scroll explizit deaktivieren
440    pub fn disable_auto_scroll(&mut self) {
441        self.auto_scroll_enabled = false;
442        log::trace!("❌ Auto-scroll disabled");
443    }
444
445    /// Berechnet sichtbaren Bereich für Messages
446    pub fn get_visible_range(&self) -> (usize, usize) {
447        if self.content_height == 0 || self.window_height == 0 {
448            return (0, 0);
449        }
450
451        let start = self.scroll_offset;
452        let end = (start + self.window_height).min(self.content_height);
453
454        log::trace!(
455            "👁️ Visible range: [{}, {}) of {} (window: {}, offset: {})",
456            start,
457            end,
458            self.content_height,
459            self.window_height,
460            self.scroll_offset
461        );
462
463        (start, end)
464    }
465
466    /// Getter für Layout-Bereiche
467    pub fn output_area(&self) -> LayoutArea {
468        self.output_area
469    }
470
471    pub fn input_area(&self) -> LayoutArea {
472        self.input_area
473    }
474
475    pub fn window_height(&self) -> usize {
476        self.window_height
477    }
478
479    pub fn content_height(&self) -> usize {
480        self.content_height
481    }
482
483    pub fn scroll_offset(&self) -> usize {
484        self.scroll_offset
485    }
486
487    pub fn is_auto_scroll_enabled(&self) -> bool {
488        self.auto_scroll_enabled
489    }
490
491    pub fn terminal_size(&self) -> (u16, u16) {
492        (self.terminal_width, self.terminal_height)
493    }
494
495    /// Prüft ob Viewport groß genug ist
496    pub fn is_usable(&self) -> bool {
497        self.terminal_width >= self.min_terminal_width
498            && self.terminal_height >= self.min_terminal_height
499            && self.output_area.is_valid()
500            && self.input_area.is_valid()
501    }
502
503    /// ✅ ERWEITERTE Debug-Informationen
504    pub fn debug_info(&self) -> String {
505        format!(
506            "Viewport: {}x{}, output: {}x{}+{}+{}, input: {}x{}+{}+{}, content: {}, window: {}, offset: {}, auto: {}, at_bottom: {}, max_offset: {}",
507            self.terminal_width, self.terminal_height,
508            self.output_area.width, self.output_area.height, self.output_area.x, self.output_area.y,
509            self.input_area.width, self.input_area.height, self.input_area.x, self.input_area.y,
510            self.content_height, self.window_height, self.scroll_offset, self.auto_scroll_enabled,
511            self.is_at_bottom(), self.max_scroll_offset()
512        )
513    }
514
515    /// ✅ KURZE Debug-Info für Logs (ohne das | Symbol)
516    pub fn short_debug(&self) -> String {
517        format!(
518            "{}x{}, content: {}, offset: {}",
519            self.terminal_width, self.terminal_height, self.content_height, self.scroll_offset
520        )
521    }
522
523    // ==================== PRIVATE HELPERS ====================
524
525    fn max_scroll_offset(&self) -> usize {
526        if self.content_height > self.window_height {
527            self.content_height - self.window_height
528        } else {
529            0
530        }
531    }
532
533    /// ✅ VERBESSERT: Prüft ob am Ende mit Toleranz
534    fn is_at_bottom(&self) -> bool {
535        let max_offset = self.max_scroll_offset();
536        // ✅ KLEINE TOLERANZ für Floating-Point-Fehler
537        self.scroll_offset >= max_offset || max_offset == 0
538    }
539
540    fn clamp_scroll_offset(&mut self) {
541        let max_offset = self.max_scroll_offset();
542        if self.scroll_offset > max_offset {
543            self.scroll_offset = max_offset;
544        }
545    }
546
547    fn adjust_scroll_after_resize(&mut self) {
548        // Bei Resize: Versuche relative Position zu behalten
549        if self.auto_scroll_enabled {
550            self.scroll_to_bottom();
551        } else {
552            self.clamp_scroll_offset();
553        }
554    }
555}
556
557/// Viewport-Events für Koordination
558#[derive(Debug, Clone)]
559pub enum ViewportEvent {
560    TerminalResized {
561        width: u16,
562        height: u16,
563    },
564    ContentChanged {
565        new_height: usize,
566    },
567    ScrollRequest {
568        direction: ScrollDirection,
569        amount: usize,
570    },
571    ForceAutoScroll,
572}
573
574#[derive(Debug, Clone)]
575pub enum ScrollDirection {
576    Up,
577    Down,
578    ToTop,
579    ToBottom,
580    PageUp,
581    PageDown,
582}
583
584impl Viewport {
585    /// ✅ VERBESSERT: Event-Processing mit detailliertem Logging
586    pub fn handle_event(&mut self, event: ViewportEvent) -> bool {
587        match event {
588            ViewportEvent::TerminalResized { width, height } => {
589                self.update_terminal_size(width, height)
590            }
591            ViewportEvent::ContentChanged { new_height } => {
592                self.update_content_height(new_height);
593                true
594            }
595            ViewportEvent::ScrollRequest { direction, amount } => {
596                log::trace!(
597                    "📜 Processing scroll request: {:?} by {}",
598                    direction,
599                    amount
600                );
601
602                match direction {
603                    ScrollDirection::Up => self.scroll_up(amount),
604                    ScrollDirection::Down => self.scroll_down(amount),
605                    ScrollDirection::ToTop => self.scroll_to_top(),
606                    ScrollDirection::ToBottom => self.scroll_to_bottom(),
607                    ScrollDirection::PageUp => self.page_up(),
608                    ScrollDirection::PageDown => self.page_down(),
609                }
610                true
611            }
612            ViewportEvent::ForceAutoScroll => {
613                self.force_auto_scroll();
614                true
615            }
616        }
617    }
618}