kas_core/text/
selection.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Tools for text selection
7
8use crate::geom::{Rect, Vec2};
9use crate::theme::Text;
10use cast::CastFloat;
11use kas_text::{TextDisplay, format::FormattableText};
12use std::ops::Range;
13use unicode_segmentation::UnicodeSegmentation;
14
15/// Action used by [`crate::event::components::TextInput`]
16#[derive(Default)]
17pub struct SelectionAction {
18    pub anchor: bool,
19    pub clear: bool,
20    pub repeats: u32,
21}
22
23impl SelectionAction {
24    /// Construct
25    pub fn new(anchor: bool, clear: bool, repeats: u32) -> Self {
26        SelectionAction {
27            anchor,
28            clear,
29            repeats,
30        }
31    }
32}
33
34/// Text-selection logic
35///
36/// This struct holds the index of the edit cursor and selection position, which
37/// together form a range. There is no requirement on the order of these two
38/// positions. Each may be adjusted independently.
39///
40/// Additionally, this struct holds the selection anchor index. This usually
41/// equals the selection index, but when using double-click or triple-click
42/// selection, the anchor represents the initially-clicked position while the
43/// selection index represents the expanded position.
44#[derive(Clone, Debug, Default)]
45pub struct SelectionHelper {
46    edit: usize,
47    sel: usize,
48    anchor: usize,
49}
50
51impl SelectionHelper {
52    /// Construct from `(edit, selection)` positions
53    ///
54    /// The anchor position is set to the selection position.
55    pub fn new(edit: usize, selection: usize) -> Self {
56        SelectionHelper {
57            edit,
58            sel: selection,
59            anchor: selection,
60        }
61    }
62
63    /// Reset to the default state
64    ///
65    /// All positions are set to 0.
66    pub fn clear(&mut self) {
67        *self = Self::default();
68    }
69
70    /// True if the selection index equals the cursor index
71    pub fn is_empty(&self) -> bool {
72        self.edit == self.sel
73    }
74    /// Clear selection without changing the edit index
75    pub fn set_empty(&mut self) {
76        self.sel = self.edit;
77        self.anchor = self.edit;
78    }
79
80    /// Set the cursor index and clear the selection
81    pub fn set_all(&mut self, index: usize) {
82        self.edit = index;
83        self.sel = index;
84        self.anchor = index;
85    }
86
87    /// Get the cursor index
88    pub fn edit_index(&self) -> usize {
89        self.edit
90    }
91    /// Set the cursor index without adjusting the selection index
92    pub fn set_edit_index(&mut self, index: usize) {
93        self.edit = index;
94    }
95
96    /// Get the selection index
97    pub fn sel_index(&self) -> usize {
98        self.sel
99    }
100    /// Set the selection index without adjusting the edit index
101    ///
102    /// The anchor index is also set to the selection index.
103    pub fn set_sel_index(&mut self, index: usize) {
104        self.sel = index;
105        self.anchor = index;
106    }
107    /// Set the selection index only
108    ///
109    /// Prefer [`Self::set_sel_index`] unless you know you don't want to set the anchor.
110    pub fn set_sel_index_only(&mut self, index: usize) {
111        self.sel = index;
112    }
113
114    /// Apply new limit to the maximum length
115    ///
116    /// Call this method if the string changes under the selection to ensure
117    /// that the selection does not exceed the length of the new string.
118    pub fn set_max_len(&mut self, len: usize) {
119        self.edit = self.edit.min(len);
120        self.sel = self.sel.min(len);
121        self.anchor = self.anchor.min(len);
122    }
123
124    /// Get the selection range
125    ///
126    /// This range is from the edit index to the selection index or reversed,
127    /// whichever is increasing.
128    pub fn range(&self) -> Range<usize> {
129        let mut range = self.edit..self.sel;
130        if range.start > range.end {
131            std::mem::swap(&mut range.start, &mut range.end);
132        }
133        range
134    }
135
136    /// Set the anchor position to the start of the selection range
137    pub fn set_anchor_to_range_start(&mut self) {
138        self.anchor = self.range().start;
139    }
140
141    /// Get the range from the anchor position to the edit position
142    ///
143    /// This is used following [`Self::set_anchor_to_range_start`] to get the
144    /// IME pre-edit range.
145    pub fn anchor_to_edit_range(&self) -> Range<usize> {
146        debug_assert!(self.anchor <= self.edit);
147        self.anchor..self.edit
148    }
149
150    /// Expand the selection from the range between edit index and anchor index
151    ///
152    /// This moves both edit index and selection index. To obtain repeatable behaviour,
153    /// first set `self.anchor_pos`.
154    /// then before each time this method is called set the edit position.
155    ///
156    /// If `repeats <= 2`, the selection is expanded by words, otherwise it is
157    /// expanded by lines. Line expansion only works if text is line-wrapped
158    /// (layout has been solved).
159    fn expand<T: FormattableText>(&mut self, text: &Text<T>, repeats: u32) {
160        let string = text.as_str();
161        let mut range = self.edit..self.anchor;
162        if range.start > range.end {
163            std::mem::swap(&mut range.start, &mut range.end);
164        }
165        let (mut start, mut end);
166        if repeats <= 2 {
167            end = string[range.start..]
168                .char_indices()
169                .nth(1)
170                .map(|(i, _)| range.start + i)
171                .unwrap_or(string.len());
172            start = string[0..end]
173                .split_word_bound_indices()
174                .next_back()
175                .map(|(index, _)| index)
176                .unwrap_or(0);
177            end = string[start..]
178                .split_word_bound_indices()
179                .find_map(|(index, _)| {
180                    let pos = start + index;
181                    (pos >= range.end).then_some(pos)
182                })
183                .unwrap_or(string.len());
184        } else {
185            start = match text.find_line(range.start) {
186                Ok(Some(r)) => r.1.start,
187                _ => 0,
188            };
189            end = match text.find_line(range.end) {
190                Ok(Some(r)) => r.1.end,
191                _ => string.len(),
192            };
193        }
194
195        if self.edit < self.sel {
196            std::mem::swap(&mut start, &mut end);
197        }
198        self.sel = start;
199        self.edit = end;
200    }
201
202    /// Handle an action
203    pub fn action<T: FormattableText>(&mut self, text: &Text<T>, action: SelectionAction) {
204        if action.anchor {
205            self.anchor = self.edit;
206        }
207        if action.clear {
208            self.set_empty();
209        }
210        if action.repeats > 1 {
211            self.expand(text, action.repeats);
212        }
213    }
214
215    /// Return a [`Rect`] encompassing the cursor(s) and selection
216    pub fn cursor_rect(&self, text: &TextDisplay) -> Option<Rect> {
217        let (m1, m2);
218        if self.sel == self.edit {
219            let mut iter = text.text_glyph_pos(self.edit);
220            m1 = iter.next();
221            m2 = iter.next();
222        } else if self.sel < self.edit {
223            m1 = text.text_glyph_pos(self.sel).next_back();
224            m2 = text.text_glyph_pos(self.edit).next();
225        } else {
226            m1 = text.text_glyph_pos(self.edit).next_back();
227            m2 = text.text_glyph_pos(self.sel).next();
228        }
229
230        if let Some((c1, c2)) = m1.zip(m2) {
231            let left = c1.pos.0.min(c2.pos.0);
232            let right = c1.pos.0.max(c2.pos.0);
233            let top = (c1.pos.1 - c1.ascent).min(c2.pos.1 - c2.ascent);
234            let bottom = (c1.pos.1 - c1.descent).max(c2.pos.1 - c2.ascent);
235            let p1 = Vec2(left, top).cast_floor();
236            let p2 = Vec2(right, bottom).cast_ceil();
237            Some(Rect::from_coords(p1, p2))
238        } else if let Some(c) = m1.or(m2) {
239            let p1 = Vec2(c.pos.0, c.pos.1 - c.ascent).cast_floor();
240            let p2 = Vec2(c.pos.0, c.pos.1 - c.descent).cast_ceil();
241            Some(Rect::from_coords(p1, p2))
242        } else {
243            None
244        }
245    }
246}