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