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