egui/text_selection/
text_cursor_state.rs

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