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
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License in the LICENSE-APACHE file or at:
//     https://www.apache.org/licenses/LICENSE-2.0

//! Tools for text selection

use super::{TextApi, TextApiExt};
use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;

/// Action used by [`crate::event::components::TextInput`]
#[derive(Default)]
pub struct SelectionAction {
    pub anchor: bool,
    pub clear: bool,
    pub repeats: u32,
}

impl SelectionAction {
    /// Construct
    pub fn new(anchor: bool, clear: bool, repeats: u32) -> Self {
        SelectionAction {
            anchor,
            clear,
            repeats,
        }
    }
}

/// Text-selection logic
///
/// This struct holds an "edit pos" and a "selection pos", which together form
/// a range. There is no requirement on the order of these two positions. Each
/// may be adjusted independently.
#[derive(Clone, Debug, Default)]
pub struct SelectionHelper {
    edit_pos: usize,
    sel_pos: usize,
    anchor_pos: usize,
}

impl SelectionHelper {
    /// Construct from `(edit, selection)` positions
    ///
    /// The anchor position is set to the selection position.
    pub fn new(edit_pos: usize, sel_pos: usize) -> Self {
        let anchor_pos = sel_pos;
        SelectionHelper {
            edit_pos,
            sel_pos,
            anchor_pos,
        }
    }

    /// Reset to the default state
    ///
    /// All positions are set to 0.
    pub fn clear(&mut self) {
        *self = Self::default();
    }

    /// True if the edit pos equals the selection pos
    pub fn is_empty(&self) -> bool {
        self.edit_pos == self.sel_pos
    }
    /// Clear selection without changing edit pos
    pub fn set_empty(&mut self) {
        self.sel_pos = self.edit_pos;
    }

    /// Set both edit and selection positions to this value
    pub fn set_pos(&mut self, pos: usize) {
        self.edit_pos = pos;
        self.sel_pos = pos;
    }

    /// Get the edit pos
    pub fn edit_pos(&self) -> usize {
        self.edit_pos
    }
    /// Set the edit pos without adjusting the selection pos
    pub fn set_edit_pos(&mut self, pos: usize) {
        self.edit_pos = pos;
    }

    /// Get the selection pos
    pub fn sel_pos(&self) -> usize {
        self.sel_pos
    }
    /// Set the selection pos without adjusting the edit pos
    pub fn set_sel_pos(&mut self, pos: usize) {
        self.sel_pos = pos;
    }

    /// Apply new limit to the maximum length
    ///
    /// Call this method if the string changes under the selection to ensure
    /// that the selection does not exceed the length of the new string.
    pub fn set_max_len(&mut self, len: usize) {
        self.edit_pos = self.edit_pos.min(len);
        self.sel_pos = self.sel_pos.min(len);
        self.anchor_pos = self.anchor_pos.min(len);
    }

    /// Construct a range from the edit pos and selection pos
    ///
    /// The range is from the minimum of (edit pos, selection pos) to the
    /// maximum of the two.
    pub fn range(&self) -> Range<usize> {
        let mut range = self.edit_pos..self.sel_pos;
        if range.start > range.end {
            std::mem::swap(&mut range.start, &mut range.end);
        }
        range
    }

    /// Set the anchor position from the edit position
    pub fn set_anchor(&mut self) {
        self.anchor_pos = self.edit_pos;
    }

    /// Expand the selection from the range between edit pos and anchor pos
    ///
    /// This moves both edit pos and sel pos. To obtain repeatable behaviour,
    /// first use [`SelectionHelper::set_anchor`] to set the anchor position,
    /// then before each time this method is called set the edit position.
    ///
    /// If `repeats <= 2`, the selection is expanded by words, otherwise it is
    /// expanded by lines. Line expansion only works if text is line-wrapped
    /// (layout has been solved).
    pub fn expand<T: TextApi>(&mut self, text: &T, repeats: u32) {
        let string = text.as_str();
        let mut range = self.edit_pos..self.anchor_pos;
        if range.start > range.end {
            std::mem::swap(&mut range.start, &mut range.end);
        }
        let (mut start, mut end);
        if repeats <= 2 {
            end = string[range.start..]
                .char_indices()
                .nth(1)
                .map(|(i, _)| range.start + i)
                .unwrap_or(string.len());
            start = string[0..end]
                .split_word_bound_indices()
                .next_back()
                .map(|(index, _)| index)
                .unwrap_or(0);
            end = string[start..]
                .split_word_bound_indices()
                .find_map(|(index, _)| {
                    let pos = start + index;
                    (pos >= range.end).then_some(pos)
                })
                .unwrap_or(string.len());
        } else {
            start = match text.find_line(range.start) {
                Ok(Some(r)) => r.1.start,
                _ => 0,
            };
            end = match text.find_line(range.end) {
                Ok(Some(r)) => r.1.end,
                _ => string.len(),
            };
        }

        if self.edit_pos < self.sel_pos {
            std::mem::swap(&mut start, &mut end);
        }
        self.sel_pos = start;
        self.edit_pos = end;
    }

    /// Handle an action
    pub fn action<T: TextApi>(&mut self, text: &T, action: SelectionAction) {
        if action.anchor {
            self.set_anchor();
        }
        if action.clear {
            self.set_empty();
        }
        if action.repeats > 1 {
            self.expand(text, action.repeats);
        }
    }
}