Skip to main content

agg_gui/widgets/
text_field_core.rs

1//! Internal helpers for `TextField`: UTF-8 navigation, word boundaries,
2//! hit-test geometry, shared edit state, and the undo command type.
3
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use crate::text::measure_advance;
8use crate::text::Font;
9use crate::undo::UndoRedoCommand;
10
11// ---------------------------------------------------------------------------
12// UTF-8 boundary helpers
13// ---------------------------------------------------------------------------
14
15pub fn prev_char_boundary(s: &str, byte_pos: usize) -> usize {
16    let mut pos = byte_pos;
17    loop {
18        if pos == 0 {
19            return 0;
20        }
21        pos -= 1;
22        if s.is_char_boundary(pos) {
23            return pos;
24        }
25    }
26}
27
28pub fn next_char_boundary(s: &str, byte_pos: usize) -> usize {
29    let mut pos = byte_pos + 1;
30    while pos <= s.len() {
31        if s.is_char_boundary(pos) {
32            return pos;
33        }
34        pos += 1;
35    }
36    s.len()
37}
38
39// ---------------------------------------------------------------------------
40// Word-boundary helpers
41// ---------------------------------------------------------------------------
42
43fn is_word_char(c: char) -> bool {
44    c.is_alphanumeric() || c == '_'
45}
46
47/// Ctrl+Right: advance to end of next token (skip whitespace then non-whitespace).
48pub fn next_word_boundary(s: &str, pos: usize) -> usize {
49    let mut chars = s[pos..].char_indices().peekable();
50    let mut advanced = 0usize;
51    // skip leading whitespace
52    while let Some(&(i, c)) = chars.peek() {
53        if !c.is_whitespace() {
54            break;
55        }
56        advanced = i + c.len_utf8();
57        chars.next();
58    }
59    // skip non-whitespace
60    while let Some(&(i, c)) = chars.peek() {
61        if c.is_whitespace() {
62            break;
63        }
64        advanced = i + c.len_utf8();
65        chars.next();
66    }
67    pos + advanced
68}
69
70/// Ctrl+Left: retreat to start of previous token.
71pub fn prev_word_boundary(s: &str, pos: usize) -> usize {
72    if pos == 0 {
73        return 0;
74    }
75    let chars: Vec<(usize, char)> = s[..pos].char_indices().collect();
76    let mut i = chars.len();
77    while i > 0 && chars[i - 1].1.is_whitespace() {
78        i -= 1;
79    }
80    while i > 0 && !chars[i - 1].1.is_whitespace() {
81        i -= 1;
82    }
83    if i < chars.len() {
84        chars[i].0
85    } else {
86        0
87    }
88}
89
90/// Returns `[start, end)` byte range of the word under `byte_pos`
91/// (used for double-click word selection).
92pub fn word_range_at(s: &str, byte_pos: usize) -> (usize, usize) {
93    let anchor_class = is_word_char(s[byte_pos..].chars().next().unwrap_or(' '));
94    // walk back
95    let start = {
96        let mut p = byte_pos;
97        while p > 0 {
98            let prev = prev_char_boundary(s, p);
99            let c = s[prev..p].chars().next().unwrap_or(' ');
100            if is_word_char(c) != anchor_class {
101                break;
102            }
103            p = prev;
104        }
105        p
106    };
107    // walk forward
108    let end = {
109        let mut p = byte_pos;
110        for (_, c) in s[byte_pos..].char_indices() {
111            if is_word_char(c) != anchor_class {
112                break;
113            }
114            p += c.len_utf8();
115        }
116        p
117    };
118    (start, end)
119}
120
121// ---------------------------------------------------------------------------
122// X-coordinate ↔ byte-offset
123// ---------------------------------------------------------------------------
124
125/// Byte offset of the character boundary closest to `target_x` in rendered text.
126pub fn byte_at_x(font: &Font, text: &str, font_size: f64, target_x: f64) -> usize {
127    if target_x <= 0.0 {
128        return 0;
129    }
130    let mut prev_x = 0.0f64;
131    let mut prev_pos = 0usize;
132    for (i, c) in text.char_indices() {
133        let x = measure_advance(font, &text[..i], font_size);
134        let mid = (prev_x + x) * 0.5;
135        if target_x < mid {
136            return prev_pos;
137        }
138        prev_x = x;
139        prev_pos = i;
140        let _ = c;
141    }
142    let total = measure_advance(font, text, font_size);
143    let mid = (prev_x + total) * 0.5;
144    if target_x < mid {
145        prev_pos
146    } else {
147        text.len()
148    }
149}
150
151// ---------------------------------------------------------------------------
152// Shared edit state
153// ---------------------------------------------------------------------------
154
155/// The mutable editing state shared between `TextField` and its undo commands.
156#[derive(Clone, Default)]
157pub struct TextEditState {
158    pub text: String,
159    pub cursor: usize,
160    pub anchor: usize,
161}
162
163// ---------------------------------------------------------------------------
164// Undo command for text edits
165// ---------------------------------------------------------------------------
166
167/// Stores before/after snapshots of `TextEditState` and a shared reference
168/// to the live state so that undo/redo can restore it.
169pub struct TextEditCommand {
170    pub name: &'static str,
171    pub before: TextEditState,
172    pub after: TextEditState,
173    pub target: Rc<RefCell<TextEditState>>,
174}
175
176impl UndoRedoCommand for TextEditCommand {
177    fn name(&self) -> &str {
178        self.name
179    }
180    fn do_it(&mut self) {
181        *self.target.borrow_mut() = self.after.clone();
182    }
183    fn undo_it(&mut self) {
184        *self.target.borrow_mut() = self.before.clone();
185    }
186}