rush_sync_server/ui/
cursor.rs1use 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 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 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 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 if text.is_empty() || self.position == 0 {
92 return 0;
93 }
94
95 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 if text.is_empty() || self.position <= 1 {
110 return 0;
111 }
112
113 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 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}