1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
//! Cursor movement and text-mutation primitives for [`TextArea`], split out of
//! `text_area.rs` to keep that file under the project's 800-line cap.
//!
//! These are the low-level funnels the widget's key/mouse handlers call:
//! `insert_str`/`delete` mutate the buffer (and fire `on_change` — see
//! `callbacks.rs`), while `move_cursor_to`/`move_char`/`move_line` reposition
//! the caret without touching the text. They are `pub(super)` so the sibling
//! `widget_impl` and `scroll` modules can drive them.
use super::*;
impl TextArea {
/// Insert a string at the cursor, replacing any active selection.
pub(super) fn insert_str(&mut self, s: &str) {
let mut st = self.edit.borrow_mut();
let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
// Make sure we slice at grapheme boundaries.
let lo = lo.min(st.text.len());
let hi = hi.min(st.text.len());
st.text.replace_range(lo..hi, s);
st.cursor = lo + s.len();
st.anchor = st.cursor;
st.note_text_change();
drop(st);
self.mark_dirty();
self.notify_change();
}
/// Delete the current selection, or (if empty) `dir` chars toward
/// the supplied side. `-1` = backspace, `+1` = delete, `0` = just
/// collapse the selection (cut path).
pub(super) fn delete(&mut self, dir: i32) {
let mut st = self.edit.borrow_mut();
let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
if lo != hi {
st.text.replace_range(lo..hi, "");
st.cursor = lo;
st.anchor = lo;
} else if dir < 0 && st.cursor > 0 {
let cur = st.cursor;
let prev = prev_char_boundary(&st.text, cur);
st.text.replace_range(prev..cur, "");
st.cursor = prev;
st.anchor = prev;
} else if dir > 0 && st.cursor < st.text.len() {
let cur = st.cursor;
let next = next_char_boundary(&st.text, cur);
st.text.replace_range(cur..next, "");
}
st.note_text_change();
drop(st);
self.mark_dirty();
self.notify_change();
}
/// Handle the caret/selection change for a fresh pointer press. `clicks` is
/// the multi-click count (1 = single, 2 = double, 3 = triple). A double
/// selects the word under `off`, a triple the logical line (paragraph);
/// `shift` extends the existing selection to `off`.
pub(super) fn begin_pointer_selection(&mut self, off: usize, clicks: u32, shift: bool) {
if shift {
self.select_granularity = SelectGranularity::Char;
self.select_pivot = (off, off);
self.move_cursor_to(off, /*with_selection=*/ true);
return;
}
let text = self.edit.borrow().text.clone();
match clicks {
n if n >= 3 => {
self.select_granularity = SelectGranularity::Line;
let (start, end) = paragraph_range_at(&text, off);
self.select_pivot = (start, end);
self.set_selection(start, end);
}
2 => {
self.select_granularity = SelectGranularity::Word;
let (start, end) = word_range_at(&text, off);
self.select_pivot = (start, end);
self.set_selection(start, end);
}
_ => {
self.select_granularity = SelectGranularity::Char;
self.select_pivot = (off, off);
self.move_cursor_to(off, /*with_selection=*/ false);
}
}
}
/// Extend the selection during a drag, honouring the granularity the
/// initiating click established. `off` is the caret byte offset under the
/// pointer.
pub(super) fn extend_selection_drag(&mut self, off: usize) {
match self.select_granularity {
SelectGranularity::Char => self.move_cursor_to(off, /*with_selection=*/ true),
SelectGranularity::Word => {
let text = self.edit.borrow().text.clone();
let (pivot_start, pivot_end) = self.select_pivot;
let (cs, ce) = word_range_at(&text, off);
if off >= pivot_end {
self.set_caret_anchor(ce, pivot_start);
} else {
self.set_caret_anchor(cs, pivot_end);
}
}
SelectGranularity::Line => {
let text = self.edit.borrow().text.clone();
let (pivot_start, pivot_end) = self.select_pivot;
let (cs, ce) = paragraph_range_at(&text, off);
if off >= pivot_end {
self.set_caret_anchor(ce, pivot_start);
} else {
self.set_caret_anchor(cs, pivot_end);
}
}
}
}
/// Set anchor and cursor to a sorted `[start, end)` selection, caret at the
/// end.
pub(super) fn set_selection(&mut self, start: usize, end: usize) {
let mut st = self.edit.borrow_mut();
let len = st.text.len();
st.anchor = start.min(len);
st.cursor = end.min(len);
}
/// Place the caret at `cursor` with the selection anchored at `anchor`
/// (both clamped to the text length).
fn set_caret_anchor(&mut self, cursor: usize, anchor: usize) {
let mut st = self.edit.borrow_mut();
let len = st.text.len();
st.cursor = cursor.min(len);
st.anchor = anchor.min(len);
}
/// Move cursor to an absolute byte offset. `with_selection=false`
/// collapses anchor with cursor; `true` leaves the anchor alone
/// so a selection is extended.
pub(super) fn move_cursor_to(&mut self, pos: usize, with_selection: bool) {
let mut st = self.edit.borrow_mut();
let p = pos.min(st.text.len());
st.cursor = p;
if !with_selection {
st.anchor = p;
}
}
/// Cursor one char left / right.
pub(super) fn move_char(&mut self, dir: i32, with_selection: bool) {
let st = self.edit.borrow();
let p = if dir < 0 {
prev_char_boundary(&st.text, st.cursor)
} else {
next_char_boundary(&st.text, st.cursor)
};
drop(st);
self.move_cursor_to(p, with_selection);
}
/// Cursor one visual line up / down. `dir` = −1 for up, +1 for down.
pub(super) fn move_line(&mut self, dir: i32, with_selection: bool) {
self.move_lines(dir as isize, with_selection);
}
/// Cursor `delta` visual lines up (negative) or down (positive), preserving
/// the pixel column. Clamps to the first/last visual line. Shared by arrow
/// navigation ([`move_line`](Self::move_line)) and PageUp/PageDown.
pub(super) fn move_lines(&mut self, delta: isize, with_selection: bool) {
if self.cached_lines.is_empty() {
return;
}
let cursor = self.edit.borrow().cursor;
let cur_line = self.line_for_cursor(cursor) as isize;
let last = self.cached_lines.len() as isize - 1;
let target_line = (cur_line + delta).clamp(0, last);
if target_line == cur_line {
return;
}
let cur_line = cur_line as usize;
let target_line = target_line as usize;
// Preserve horizontal position (pixel column, not byte column),
// measured relative to the current line's aligned start so left /
// center / right alignment all keep the caret in the same column.
let cur_line_x = self
.cached_lines
.get(cur_line)
.map(|l| self.line_x_start(l))
.unwrap_or(self.padding);
let cur_x = self.pos_for_cursor(cursor).x - cur_line_x;
// Find byte offset in target_line closest to `cur_x`.
let line = &self.cached_lines[target_line];
let txt = &line.text;
let mut best_byte = 0usize;
let mut best_delta = f64::INFINITY;
let mut acc = 0.0_f64;
let mut prev_byte = 0usize;
for (i, _) in txt.char_indices().chain(std::iter::once((txt.len(), ' '))) {
let w = if i > prev_byte {
measure_advance(&self.font, &txt[prev_byte..i], self.font_size)
} else {
0.0
};
acc += w;
let d = (acc - cur_x).abs();
if d < best_delta {
best_delta = d;
best_byte = i;
}
prev_byte = i;
}
let target = line.start + best_byte;
self.move_cursor_to(target, with_selection);
}
}