rush_sync_server/ui/
cursor.rs

1// ui/cursor.rs - KORRIGIERT: Robuste Bounds-Checks
2use std::time::{Duration, Instant};
3use unicode_segmentation::UnicodeSegmentation;
4
5pub struct CursorState {
6    position: usize,
7    text_length: usize,
8    visible: bool,
9    last_blink: Instant,
10    blink_interval: Duration,
11}
12
13impl CursorState {
14    pub fn new() -> Self {
15        Self {
16            position: 0,
17            text_length: 0,
18            visible: true,
19            last_blink: Instant::now(),
20            blink_interval: Duration::from_millis(530),
21        }
22    }
23
24    pub fn get_position(&self) -> usize {
25        self.position
26    }
27
28    pub fn is_visible(&self) -> bool {
29        self.visible
30    }
31
32    pub fn update_blink(&mut self) {
33        if self.last_blink.elapsed() >= self.blink_interval {
34            self.visible = !self.visible;
35            self.last_blink = Instant::now();
36        }
37    }
38
39    pub fn show_cursor(&mut self) {
40        self.visible = true;
41        self.last_blink = Instant::now();
42    }
43
44    pub fn update_text_length(&mut self, text: &str) {
45        self.text_length = text.graphemes(true).count();
46        // ✅ KRITISCH: Position immer korrekt bounded
47        self.position = self.position.min(self.text_length);
48        self.show_cursor();
49    }
50
51    pub fn move_left(&mut self) {
52        if self.position > 0 {
53            self.position -= 1;
54            self.show_cursor();
55        }
56    }
57
58    pub fn move_right(&mut self) {
59        // ✅ ZUSÄTZLICHER CHECK: Nie über text_length hinaus
60        if self.position < self.text_length {
61            self.position += 1;
62            self.show_cursor();
63        }
64    }
65
66    pub fn move_to_start(&mut self) {
67        self.position = 0;
68        self.show_cursor();
69    }
70
71    pub fn move_to_end(&mut self) {
72        self.position = self.text_length;
73        self.show_cursor();
74    }
75
76    pub fn get_next_byte_position(&self, text: &str) -> usize {
77        // ✅ SAFETY: Leerer Text check
78        if text.is_empty() {
79            return 0;
80        }
81
82        text.grapheme_indices(true)
83            .take(self.position + 1)
84            .last()
85            .map(|(pos, grapheme)| pos + grapheme.len())
86            .unwrap_or(text.len())
87    }
88
89    pub fn get_byte_position(&self, text: &str) -> usize {
90        // ✅ SAFETY: Mehrfache Checks
91        if text.is_empty() || self.position == 0 {
92            return 0;
93        }
94
95        // ✅ SAFETY: Position darf nie größer als text_length sein
96        let safe_position = self.position.min(text.graphemes(true).count());
97        if safe_position == 0 {
98            return 0;
99        }
100
101        text.grapheme_indices(true)
102            .nth(safe_position.saturating_sub(1))
103            .map(|(pos, grapheme)| pos + grapheme.len())
104            .unwrap_or(text.len())
105    }
106
107    pub fn get_prev_byte_position(&self, text: &str) -> usize {
108        // ✅ SAFETY: Umfassende Checks
109        if text.is_empty() || self.position <= 1 {
110            return 0;
111        }
112
113        // ✅ SAFETY: Position validieren
114        let safe_position = self.position.min(text.graphemes(true).count());
115        if safe_position <= 1 {
116            return 0;
117        }
118
119        text.grapheme_indices(true)
120            .nth(safe_position.saturating_sub(2))
121            .map(|(pos, grapheme)| pos + grapheme.len())
122            .unwrap_or(0)
123    }
124
125    // ✅ NEU: Reset für leeren Text
126    pub fn reset_for_empty_text(&mut self) {
127        self.position = 0;
128        self.text_length = 0;
129        self.show_cursor();
130    }
131}
132
133impl Default for CursorState {
134    fn default() -> Self {
135        Self::new()
136    }
137}