egui/text_selection/
text_cursor_state.rs

1//! Text cursor changes/interaction, without modifying the text.
2
3use epaint::text::{cursor::*, Galley};
4
5use crate::*;
6
7use super::{CCursorRange, CursorRange};
8
9/// The state of a text cursor selection.
10///
11/// Used for [`crate::TextEdit`] and [`crate::Label`].
12#[derive(Clone, Copy, Debug, Default)]
13#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
14#[cfg_attr(feature = "serde", serde(default))]
15pub struct TextCursorState {
16    cursor_range: Option<CursorRange>,
17
18    /// This is what is easiest to work with when editing text,
19    /// so users are more likely to read/write this.
20    ccursor_range: Option<CCursorRange>,
21}
22
23impl From<CursorRange> for TextCursorState {
24    fn from(cursor_range: CursorRange) -> Self {
25        Self {
26            cursor_range: Some(cursor_range),
27            ccursor_range: Some(CCursorRange {
28                primary: cursor_range.primary.ccursor,
29                secondary: cursor_range.secondary.ccursor,
30            }),
31        }
32    }
33}
34
35impl From<CCursorRange> for TextCursorState {
36    fn from(ccursor_range: CCursorRange) -> Self {
37        Self {
38            cursor_range: None,
39            ccursor_range: Some(ccursor_range),
40        }
41    }
42}
43
44impl TextCursorState {
45    pub fn is_empty(&self) -> bool {
46        self.cursor_range.is_none() && self.ccursor_range.is_none()
47    }
48
49    /// The currently selected range of characters.
50    pub fn char_range(&self) -> Option<CCursorRange> {
51        self.ccursor_range.or_else(|| {
52            self.cursor_range
53                .map(|cursor_range| cursor_range.as_ccursor_range())
54        })
55    }
56
57    pub fn range(&self, galley: &Galley) -> Option<CursorRange> {
58        self.cursor_range
59            .map(|cursor_range| {
60                // We only use the PCursor (paragraph number, and character offset within that paragraph).
61                // This is so that if we resize the [`TextEdit`] region, and text wrapping changes,
62                // we keep the same byte character offset from the beginning of the text,
63                // even though the number of rows changes
64                // (each paragraph can be several rows, due to word wrapping).
65                // The column (character offset) should be able to extend beyond the last word so that we can
66                // go down and still end up on the same column when we return.
67                CursorRange {
68                    primary: galley.from_pcursor(cursor_range.primary.pcursor),
69                    secondary: galley.from_pcursor(cursor_range.secondary.pcursor),
70                }
71            })
72            .or_else(|| {
73                self.ccursor_range.map(|ccursor_range| CursorRange {
74                    primary: galley.from_ccursor(ccursor_range.primary),
75                    secondary: galley.from_ccursor(ccursor_range.secondary),
76                })
77            })
78    }
79
80    /// Sets the currently selected range of characters.
81    pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
82        self.cursor_range = None;
83        self.ccursor_range = ccursor_range;
84    }
85
86    pub fn set_range(&mut self, cursor_range: Option<CursorRange>) {
87        self.cursor_range = cursor_range;
88        self.ccursor_range = None;
89    }
90}
91
92impl TextCursorState {
93    /// Handle clicking and/or dragging text.
94    ///
95    /// Returns `true` if there was interaction.
96    pub fn pointer_interaction(
97        &mut self,
98        ui: &Ui,
99        response: &Response,
100        cursor_at_pointer: Cursor,
101        galley: &Galley,
102        is_being_dragged: bool,
103    ) -> bool {
104        let text = galley.text();
105
106        if response.double_clicked() {
107            // Select word:
108            let ccursor_range = select_word_at(text, cursor_at_pointer.ccursor);
109            self.set_range(Some(CursorRange {
110                primary: galley.from_ccursor(ccursor_range.primary),
111                secondary: galley.from_ccursor(ccursor_range.secondary),
112            }));
113            true
114        } else if response.triple_clicked() {
115            // Select line:
116            let ccursor_range = select_line_at(text, cursor_at_pointer.ccursor);
117            self.set_range(Some(CursorRange {
118                primary: galley.from_ccursor(ccursor_range.primary),
119                secondary: galley.from_ccursor(ccursor_range.secondary),
120            }));
121            true
122        } else if response.sense.drag {
123            if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
124                // The start of a drag (or a click).
125                if ui.input(|i| i.modifiers.shift) {
126                    if let Some(mut cursor_range) = self.range(galley) {
127                        cursor_range.primary = cursor_at_pointer;
128                        self.set_range(Some(cursor_range));
129                    } else {
130                        self.set_range(Some(CursorRange::one(cursor_at_pointer)));
131                    }
132                } else {
133                    self.set_range(Some(CursorRange::one(cursor_at_pointer)));
134                }
135                true
136            } else if is_being_dragged {
137                // Drag to select text:
138                if let Some(mut cursor_range) = self.range(galley) {
139                    cursor_range.primary = cursor_at_pointer;
140                    self.set_range(Some(cursor_range));
141                }
142                true
143            } else {
144                false
145            }
146        } else {
147            false
148        }
149    }
150}
151
152fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
153    if ccursor.index == 0 {
154        CCursorRange::two(ccursor, ccursor_next_word(text, ccursor))
155    } else {
156        let it = text.chars();
157        let mut it = it.skip(ccursor.index - 1);
158        if let Some(char_before_cursor) = it.next() {
159            if let Some(char_after_cursor) = it.next() {
160                if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
161                    let min = ccursor_previous_word(text, ccursor + 1);
162                    let max = ccursor_next_word(text, min);
163                    CCursorRange::two(min, max)
164                } else if is_word_char(char_before_cursor) {
165                    let min = ccursor_previous_word(text, ccursor);
166                    let max = ccursor_next_word(text, min);
167                    CCursorRange::two(min, max)
168                } else if is_word_char(char_after_cursor) {
169                    let max = ccursor_next_word(text, ccursor);
170                    CCursorRange::two(ccursor, max)
171                } else {
172                    let min = ccursor_previous_word(text, ccursor);
173                    let max = ccursor_next_word(text, ccursor);
174                    CCursorRange::two(min, max)
175                }
176            } else {
177                let min = ccursor_previous_word(text, ccursor);
178                CCursorRange::two(min, ccursor)
179            }
180        } else {
181            let max = ccursor_next_word(text, ccursor);
182            CCursorRange::two(ccursor, max)
183        }
184    }
185}
186
187fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
188    if ccursor.index == 0 {
189        CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
190    } else {
191        let it = text.chars();
192        let mut it = it.skip(ccursor.index - 1);
193        if let Some(char_before_cursor) = it.next() {
194            if let Some(char_after_cursor) = it.next() {
195                if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) {
196                    let min = ccursor_previous_line(text, ccursor + 1);
197                    let max = ccursor_next_line(text, min);
198                    CCursorRange::two(min, max)
199                } else if !is_linebreak(char_before_cursor) {
200                    let min = ccursor_previous_line(text, ccursor);
201                    let max = ccursor_next_line(text, min);
202                    CCursorRange::two(min, max)
203                } else if !is_linebreak(char_after_cursor) {
204                    let max = ccursor_next_line(text, ccursor);
205                    CCursorRange::two(ccursor, max)
206                } else {
207                    let min = ccursor_previous_line(text, ccursor);
208                    let max = ccursor_next_line(text, ccursor);
209                    CCursorRange::two(min, max)
210                }
211            } else {
212                let min = ccursor_previous_line(text, ccursor);
213                CCursorRange::two(min, ccursor)
214            }
215        } else {
216            let max = ccursor_next_line(text, ccursor);
217            CCursorRange::two(ccursor, max)
218        }
219    }
220}
221
222pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
223    CCursor {
224        index: next_word_boundary_char_index(text.chars(), ccursor.index),
225        prefer_next_row: false,
226    }
227}
228
229fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
230    CCursor {
231        index: next_line_boundary_char_index(text.chars(), ccursor.index),
232        prefer_next_row: false,
233    }
234}
235
236pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
237    let num_chars = text.chars().count();
238    CCursor {
239        index: num_chars
240            - next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
241        prefer_next_row: true,
242    }
243}
244
245fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
246    let num_chars = text.chars().count();
247    CCursor {
248        index: num_chars
249            - next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
250        prefer_next_row: true,
251    }
252}
253
254fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
255    let mut it = it.skip(index);
256    if let Some(_first) = it.next() {
257        index += 1;
258
259        if let Some(second) = it.next() {
260            index += 1;
261            for next in it {
262                if is_word_char(next) != is_word_char(second) {
263                    break;
264                }
265                index += 1;
266            }
267        }
268    }
269    index
270}
271
272fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
273    let mut it = it.skip(index);
274    if let Some(_first) = it.next() {
275        index += 1;
276
277        if let Some(second) = it.next() {
278            index += 1;
279            for next in it {
280                if is_linebreak(next) != is_linebreak(second) {
281                    break;
282                }
283                index += 1;
284            }
285        }
286    }
287    index
288}
289
290pub fn is_word_char(c: char) -> bool {
291    c.is_ascii_alphanumeric() || c == '_'
292}
293
294fn is_linebreak(c: char) -> bool {
295    c == '\r' || c == '\n'
296}
297
298/// Accepts and returns character offset (NOT byte offset!).
299pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
300    // We know that new lines, '\n', are a single byte char, but we have to
301    // work with char offsets because before the new line there may be any
302    // number of multi byte chars.
303    // We need to know the char index to be able to correctly set the cursor
304    // later.
305    let chars_count = text.chars().count();
306
307    let position = text
308        .chars()
309        .rev()
310        .skip(chars_count - current_index.index)
311        .position(|x| x == '\n');
312
313    match position {
314        Some(pos) => CCursor::new(current_index.index - pos),
315        None => CCursor::new(0),
316    }
317}
318
319pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
320    for (ci, (bi, _)) in s.char_indices().enumerate() {
321        if ci == char_index {
322            return bi;
323        }
324    }
325    s.len()
326}
327
328pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
329    assert!(char_range.start <= char_range.end);
330    let start_byte = byte_index_from_char_index(s, char_range.start);
331    let end_byte = byte_index_from_char_index(s, char_range.end);
332    &s[start_byte..end_byte]
333}
334
335/// The thin rectangle of one end of the selection, e.g. the primary cursor.
336pub fn cursor_rect(galley_pos: Pos2, galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect {
337    let mut cursor_pos = galley
338        .pos_from_cursor(cursor)
339        .translate(galley_pos.to_vec2());
340    cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
341    // Handle completely empty galleys
342    cursor_pos = cursor_pos.expand(1.5);
343    // slightly above/below row
344    cursor_pos
345}