1use std::ops::Range;
2
3use densa_core::{Color, Point, Rect};
4use parley::{BoundingBox, Cursor, Layout, Selection};
5
6use crate::Paragraph;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum Motion {
17 Left,
19 Right,
21 WordLeft,
23 WordRight,
25 Previous,
27 Next,
29 PreviousWord,
31 NextWord,
33 Up,
36 Down,
39 LineStart,
41 LineEnd,
43 TextStart,
45 TextEnd,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
52#[non_exhaustive]
53pub enum Affinity {
54 #[default]
56 Downstream,
57 Upstream,
60}
61
62impl From<Affinity> for parley::Affinity {
63 fn from(affinity: Affinity) -> Self {
64 match affinity {
65 Affinity::Downstream => parley::Affinity::Downstream,
66 Affinity::Upstream => parley::Affinity::Upstream,
67 }
68 }
69}
70
71fn from_parley(affinity: parley::Affinity) -> Affinity {
72 match affinity {
73 parley::Affinity::Upstream => Affinity::Upstream,
74 parley::Affinity::Downstream => Affinity::Downstream,
75 }
76}
77
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
90pub struct TextSelection {
91 inner: Selection,
92}
93
94impl TextSelection {
95 pub fn caret_at(paragraph: &Paragraph, index: usize) -> Self {
97 Self::caret_with(paragraph, index, Affinity::Downstream)
98 }
99
100 pub fn caret_with(paragraph: &Paragraph, index: usize, affinity: Affinity) -> Self {
102 let cursor = cursor(paragraph, index, affinity);
103 Self {
104 inner: Selection::new(cursor, cursor),
105 }
106 }
107
108 pub fn from_range(paragraph: &Paragraph, anchor: usize, focus: usize) -> Self {
111 let anchor = cursor(paragraph, anchor, Affinity::Downstream);
112 let focus = cursor(paragraph, focus, Affinity::Upstream);
113 Self {
114 inner: Selection::new(anchor, focus),
115 }
116 }
117
118 pub fn select_all(paragraph: &Paragraph) -> Self {
120 Self::from_range(paragraph, 0, paragraph.text_len())
121 }
122
123 pub fn at_point(paragraph: &Paragraph, point: Point) -> Self {
126 Self {
127 inner: Selection::from_point(paragraph.layout(), point.x, point.y),
128 }
129 }
130
131 pub fn word_at_point(paragraph: &Paragraph, point: Point) -> Self {
133 Self {
134 inner: Selection::word_from_point(paragraph.layout(), point.x, point.y),
135 }
136 }
137
138 pub fn line_at_point(paragraph: &Paragraph, point: Point) -> Self {
140 Self {
141 inner: Selection::line_from_point(paragraph.layout(), point.x, point.y),
142 }
143 }
144
145 #[must_use]
148 pub fn extend_to_point(self, paragraph: &Paragraph, point: Point) -> Self {
149 Self {
150 inner: self
151 .inner
152 .extend_to_point(paragraph.layout(), point.x, point.y),
153 }
154 }
155
156 #[must_use]
159 pub fn shift_click(self, paragraph: &Paragraph, point: Point) -> Self {
160 Self {
161 inner: self
162 .inner
163 .shift_click_extension(paragraph.layout(), point.x, point.y),
164 }
165 }
166
167 #[must_use]
170 pub fn moved(self, paragraph: &Paragraph, motion: Motion, extend: bool) -> Self {
171 let layout = paragraph.layout();
172 let s = self.inner;
173 let inner = match motion {
174 Motion::Left => s.previous_visual(layout, extend),
175 Motion::Right => s.next_visual(layout, extend),
176 Motion::WordLeft => s.previous_visual_word(layout, extend),
177 Motion::WordRight => s.next_visual_word(layout, extend),
178 Motion::Up => s.previous_line(layout, extend),
179 Motion::Down => s.next_line(layout, extend),
180 Motion::LineStart => s.line_start(layout, extend),
181 Motion::LineEnd => s.line_end(layout, extend),
182 Motion::Previous => to(s, previous_logical(layout, s.focus()), extend),
183 Motion::Next => to(s, next_logical(layout, s.focus()), extend),
184 Motion::PreviousWord => to(s, s.focus().previous_logical_word(layout), extend),
185 Motion::NextWord => to(s, s.focus().next_logical_word(layout), extend),
186 Motion::TextStart => to(s, cursor(paragraph, 0, Affinity::Downstream), extend),
187 Motion::TextEnd => {
188 let end = cursor(paragraph, paragraph.text_len(), Affinity::Downstream);
189 to(s, end, extend)
190 }
191 };
192 Self { inner }
193 }
194
195 #[must_use]
200 pub fn refreshed(self, paragraph: &Paragraph) -> Self {
201 let clamp = |c: Cursor| cursor(paragraph, c.index(), from_parley(c.affinity()));
202 let anchor = clamp(self.inner.anchor());
203 let focus = clamp(self.inner.focus());
204 Self {
205 inner: Selection::new(anchor, focus).refresh(paragraph.layout()),
206 }
207 }
208
209 #[must_use]
211 pub fn collapsed(self) -> Self {
212 Self {
213 inner: self.inner.collapse(),
214 }
215 }
216
217 pub fn is_caret(&self) -> bool {
219 self.inner.is_collapsed()
220 }
221
222 pub fn range(&self) -> Range<usize> {
224 self.inner.text_range()
225 }
226
227 pub fn anchor(&self) -> usize {
229 self.inner.anchor().index()
230 }
231
232 pub fn focus(&self) -> usize {
234 self.inner.focus().index()
235 }
236
237 pub fn focus_affinity(&self) -> Affinity {
239 from_parley(self.inner.focus().affinity())
240 }
241
242 pub fn caret_rect(&self, paragraph: &Paragraph, width: f32) -> Rect {
245 to_rect(self.inner.focus().geometry(paragraph.layout(), width))
246 }
247
248 pub fn for_each_rect(&self, paragraph: &Paragraph, mut f: impl FnMut(Rect)) {
251 self.inner
252 .geometry_with(paragraph.layout(), |bounds, _line| f(to_rect(bounds)));
253 }
254}
255
256pub(crate) fn backspace_start(paragraph: &Paragraph, text: &str, index: usize) -> usize {
260 let at = cursor(paragraph, index, Affinity::Downstream);
261 let [upstream, _] = at.logical_clusters(paragraph.layout());
262 let Some(cluster) = upstream else {
263 return index;
264 };
265 if cluster.is_hard_line_break() || cluster.is_emoji() {
266 return cluster.text_range().start;
267 }
268 text.get(..index)
269 .and_then(|before| before.char_indices().next_back())
270 .map_or(index, |(start, _)| start)
271}
272
273fn cursor(paragraph: &Paragraph, index: usize, affinity: Affinity) -> Cursor {
275 let index = index.min(paragraph.text_len());
276 Cursor::from_byte_index(paragraph.layout(), index, affinity.into())
277}
278
279fn to(selection: Selection, target: Cursor, extend: bool) -> Selection {
281 if extend {
282 selection.extend(target)
283 } else {
284 Selection::new(target, target)
285 }
286}
287
288fn previous_logical(layout: &Layout<Color>, at: Cursor) -> Cursor {
290 let [upstream, _] = at.logical_clusters(layout);
291 upstream.map_or(at, |cluster| {
292 let start = cluster.text_range().start;
293 Cursor::from_byte_index(layout, start, parley::Affinity::Downstream)
294 })
295}
296
297fn next_logical(layout: &Layout<Color>, at: Cursor) -> Cursor {
299 let [_, downstream] = at.logical_clusters(layout);
300 downstream.map_or(at, |cluster| {
301 let end = cluster.text_range().end;
302 Cursor::from_byte_index(layout, end, parley::Affinity::Upstream)
303 })
304}
305
306fn to_rect(b: BoundingBox) -> Rect {
307 Rect::from_edges(b.x0 as f32, b.y0 as f32, b.x1 as f32, b.y1 as f32)
310}