Skip to main content

egui/text_selection/
cursor_range.rs

1use epaint::{Galley, text::CharIndex, text::cursor::CCursor};
2
3use crate::{Event, Id, Key, Modifiers, os::OperatingSystem};
4
5use super::text_cursor_state::{ccursor_next_word, ccursor_previous_word, slice_char_range};
6
7/// A selected text range (could be a range of length zero).
8///
9/// The selection is based on character count (NOT byte count!).
10#[derive(Clone, Copy, Debug, Default, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12pub struct CCursorRange {
13    /// When selecting with a mouse, this is where the mouse was released.
14    /// When moving with e.g. shift+arrows, this is what moves.
15    /// Note that the two ends can come in any order, and also be equal (no selection).
16    pub primary: CCursor,
17
18    /// When selecting with a mouse, this is where the mouse was first pressed.
19    /// This part of the cursor does not move when shift is down.
20    pub secondary: CCursor,
21
22    /// Saved horizontal position of the cursor.
23    pub h_pos: Option<f32>,
24}
25
26impl CCursorRange {
27    /// The empty range.
28    #[inline]
29    pub fn one(ccursor: CCursor) -> Self {
30        Self {
31            primary: ccursor,
32            secondary: ccursor,
33            h_pos: None,
34        }
35    }
36
37    #[inline]
38    pub fn two(min: impl Into<CCursor>, max: impl Into<CCursor>) -> Self {
39        Self {
40            primary: max.into(),
41            secondary: min.into(),
42            h_pos: None,
43        }
44    }
45
46    /// Select all the text in a galley
47    pub fn select_all(galley: &Galley) -> Self {
48        Self::two(galley.begin(), galley.end())
49    }
50
51    /// The range of selected character indices.
52    pub fn as_sorted_char_range(&self) -> std::ops::Range<CharIndex> {
53        let [start, end] = self.sorted_cursors();
54        std::ops::Range {
55            start: start.index,
56            end: end.index,
57        }
58    }
59
60    /// True if the selected range contains no characters.
61    #[inline]
62    pub fn is_empty(&self) -> bool {
63        self.primary == self.secondary
64    }
65
66    /// Is `self` a super-set of the other range?
67    pub fn contains(&self, other: Self) -> bool {
68        let [self_min, self_max] = self.sorted_cursors();
69        let [other_min, other_max] = other.sorted_cursors();
70        self_min.index <= other_min.index && other_max.index <= self_max.index
71    }
72
73    /// If there is a selection, None is returned.
74    /// If the two ends are the same, that is returned.
75    pub fn single(&self) -> Option<CCursor> {
76        if self.is_empty() {
77            Some(self.primary)
78        } else {
79            None
80        }
81    }
82
83    #[inline]
84    pub fn is_sorted(&self) -> bool {
85        let p = self.primary;
86        let s = self.secondary;
87        (p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
88    }
89
90    /// returns the two ends ordered
91    #[inline]
92    pub fn sorted_cursors(&self) -> [CCursor; 2] {
93        if self.is_sorted() {
94            [self.primary, self.secondary]
95        } else {
96            [self.secondary, self.primary]
97        }
98    }
99
100    pub fn slice_str<'s>(&self, text: &'s str) -> &'s str {
101        let [min, max] = self.sorted_cursors();
102        slice_char_range(text, min.index..max.index)
103    }
104
105    /// Check for key presses that are moving the cursor.
106    ///
107    /// Returns `true` if we did mutate `self`.
108    pub fn on_key_press(
109        &mut self,
110        os: OperatingSystem,
111        galley: &Galley,
112        modifiers: &Modifiers,
113        key: Key,
114    ) -> bool {
115        match key {
116            Key::A if modifiers.command => {
117                *self = Self::select_all(galley);
118                true
119            }
120
121            Key::ArrowLeft | Key::ArrowRight if modifiers.is_none() && !self.is_empty() => {
122                if key == Key::ArrowLeft {
123                    *self = Self::one(self.sorted_cursors()[0]);
124                } else {
125                    *self = Self::one(self.sorted_cursors()[1]);
126                }
127                true
128            }
129
130            Key::ArrowLeft
131            | Key::ArrowRight
132            | Key::ArrowUp
133            | Key::ArrowDown
134            | Key::Home
135            | Key::End => {
136                move_single_cursor(
137                    os,
138                    &mut self.primary,
139                    &mut self.h_pos,
140                    galley,
141                    key,
142                    modifiers,
143                );
144                if !modifiers.shift {
145                    self.secondary = self.primary;
146                }
147                true
148            }
149
150            Key::P | Key::N | Key::B | Key::F | Key::A | Key::E
151                if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift =>
152            {
153                move_single_cursor(
154                    os,
155                    &mut self.primary,
156                    &mut self.h_pos,
157                    galley,
158                    key,
159                    modifiers,
160                );
161                self.secondary = self.primary;
162                true
163            }
164
165            _ => false,
166        }
167    }
168
169    /// Check for events that modify the cursor range.
170    ///
171    /// Returns `true` if such an event was found and handled.
172    pub fn on_event(
173        &mut self,
174        os: OperatingSystem,
175        event: &Event,
176        galley: &Galley,
177        _widget_id: Id,
178    ) -> bool {
179        match event {
180            Event::Key {
181                modifiers,
182                key,
183                pressed: true,
184                ..
185            } => self.on_key_press(os, galley, modifiers, *key),
186
187            Event::AccessKitActionRequest(accesskit::ActionRequest {
188                action: accesskit::Action::SetTextSelection,
189                target_node,
190                target_tree,
191                data: Some(accesskit::ActionData::SetTextSelection(selection)),
192            }) => {
193                if _widget_id.accesskit_id() == *target_node
194                    && *target_tree == accesskit::TreeId::ROOT
195                {
196                    let primary =
197                        ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus);
198                    let secondary =
199                        ccursor_from_accesskit_text_position(_widget_id, galley, &selection.anchor);
200                    if let (Some(primary), Some(secondary)) = (primary, secondary) {
201                        *self = Self {
202                            primary,
203                            secondary,
204                            h_pos: None,
205                        };
206                        return true;
207                    }
208                }
209                false
210            }
211
212            _ => false,
213        }
214    }
215}
216
217// ----------------------------------------------------------------------------
218
219fn ccursor_from_accesskit_text_position(
220    id: Id,
221    galley: &Galley,
222    position: &accesskit::TextPosition,
223) -> Option<CCursor> {
224    use super::accesskit_text::MAX_CHARS_PER_TEXT_RUN;
225
226    let mut total_length = 0usize;
227    for (i, row) in galley.rows.iter().enumerate() {
228        let row_chars = row.glyphs.len() + (row.ends_with_newline as usize);
229        let num_chunks = if row_chars == 0 {
230            1
231        } else {
232            row_chars.div_ceil(MAX_CHARS_PER_TEXT_RUN)
233        };
234
235        for chunk_idx in 0..num_chunks {
236            let run_id = id.with(i).with(chunk_idx);
237            if run_id.accesskit_id() == position.node {
238                let column = chunk_idx * MAX_CHARS_PER_TEXT_RUN + position.character_index;
239                return Some(CCursor {
240                    index: CharIndex(total_length + column),
241                    prefer_next_row: !(column == row.glyphs.len()
242                        && !row.ends_with_newline
243                        && (i + 1) < galley.rows.len()),
244                });
245            }
246        }
247
248        total_length += row_chars;
249    }
250    None
251}
252
253// ----------------------------------------------------------------------------
254
255/// Move a text cursor based on keyboard
256fn move_single_cursor(
257    os: OperatingSystem,
258    cursor: &mut CCursor,
259    h_pos: &mut Option<f32>,
260    galley: &Galley,
261    key: Key,
262    modifiers: &Modifiers,
263) {
264    let (new_cursor, new_h_pos) =
265        if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift {
266            match key {
267                Key::A => (galley.cursor_begin_of_row(cursor), None),
268                Key::E => (galley.cursor_end_of_row(cursor), None),
269                Key::P => galley.cursor_up_one_row(cursor, *h_pos),
270                Key::N => galley.cursor_down_one_row(cursor, *h_pos),
271                Key::B => (galley.cursor_left_one_character(cursor), None),
272                Key::F => (galley.cursor_right_one_character(cursor), None),
273                _ => return,
274            }
275        } else {
276            match key {
277                Key::ArrowLeft => {
278                    if modifiers.alt || modifiers.ctrl {
279                        // alt on mac, ctrl on windows
280                        (ccursor_previous_word(galley, *cursor), None)
281                    } else if modifiers.mac_cmd {
282                        (galley.cursor_begin_of_row(cursor), None)
283                    } else {
284                        (galley.cursor_left_one_character(cursor), None)
285                    }
286                }
287                Key::ArrowRight => {
288                    if modifiers.alt || modifiers.ctrl {
289                        // alt on mac, ctrl on windows
290                        (ccursor_next_word(galley, *cursor), None)
291                    } else if modifiers.mac_cmd {
292                        (galley.cursor_end_of_row(cursor), None)
293                    } else {
294                        (galley.cursor_right_one_character(cursor), None)
295                    }
296                }
297                Key::ArrowUp => {
298                    if modifiers.command {
299                        // mac and windows behavior
300                        (galley.begin(), None)
301                    } else {
302                        galley.cursor_up_one_row(cursor, *h_pos)
303                    }
304                }
305                Key::ArrowDown => {
306                    if modifiers.command {
307                        // mac and windows behavior
308                        (galley.end(), None)
309                    } else {
310                        galley.cursor_down_one_row(cursor, *h_pos)
311                    }
312                }
313
314                Key::Home => {
315                    if modifiers.ctrl {
316                        // windows behavior
317                        (galley.begin(), None)
318                    } else {
319                        (galley.cursor_begin_of_row(cursor), None)
320                    }
321                }
322                Key::End => {
323                    if modifiers.ctrl {
324                        // windows behavior
325                        (galley.end(), None)
326                    } else {
327                        (galley.cursor_end_of_row(cursor), None)
328                    }
329                }
330
331                _ => unreachable!(),
332            }
333        };
334
335    *cursor = new_cursor;
336    *h_pos = new_h_pos;
337}