1use std::cell::RefCell;
11use std::rc::Rc;
12
13use repose_core::prelude::*;
14use repose_core::{Brush, CursorIcon, PointerEvent, Rect, Scene, SceneNode, View};
15use unicode_segmentation::UnicodeSegmentation;
16use web_time::{Duration, Instant};
17
18use crate::textfield::{caret_xy_for_byte, index_for_xy_bytes};
19use crate::{Text, TextStyle};
20
21const DOUBLE_TAP_MS: u64 = 300;
22const TRIPLE_TAP_MS: u64 = 500;
23const TAP_SLOP_PX: f32 = 12.0;
24
25pub trait SelectableTextExt {
29 fn selectable(
30 self,
31 on_selection_change: impl Fn(Option<(usize, usize)>) + 'static,
32 ) -> View;
33}
34
35impl SelectableTextExt for View {
36 fn selectable(
37 self,
38 on_selection_change: impl Fn(Option<(usize, usize)>) + 'static,
39 ) -> View {
40 let (text, font_size_dp) = match &self.kind {
41 ViewKind::Text {
42 text,
43 font_size,
44 ..
45 } => (text.clone(), *font_size),
46 _ => return self,
47 };
48 make_selectable(self, text, font_size_dp, on_selection_change)
49 }
50}
51
52pub fn SelectableText(
54 text: impl Into<String>,
55 font_size_dp: f32,
56 on_selection_change: impl Fn(Option<(usize, usize)>) + 'static,
57) -> View {
58 let text: String = text.into();
59 let v = Text(text.clone()).size(font_size_dp);
60 make_selectable(v, text, font_size_dp, on_selection_change)
61}
62
63fn make_selectable(
64 mut v: View,
65 text: String,
66 font_size_dp: f32,
67 on_selection_change: impl Fn(Option<(usize, usize)>) + 'static,
68) -> View {
69 let text_for_handlers = text.clone();
70 let text_for_paint = text.clone();
71
72 let selection: Rc<RefCell<Option<(usize, usize)>>> =
73 remember(|| RefCell::new(None));
74 let anchor: Rc<RefCell<usize>> = remember(|| RefCell::new(0));
75 let dragging: Rc<RefCell<bool>> = remember(|| RefCell::new(false));
76 let last_rect: Rc<RefCell<Rect>> = remember(|| RefCell::new(Rect::default()));
77
78 let last_tap_time: Rc<RefCell<Option<Instant>>> = remember(|| RefCell::new(None));
80 let last_tap_pos: Rc<RefCell<Option<(f32, f32)>>> = remember(|| RefCell::new(None));
81 let tap_count: Rc<RefCell<u8>> = remember(|| RefCell::new(0));
82
83 let callback = Rc::new(on_selection_change);
84
85 let set_sel = {
86 let selection = selection.clone();
87 let callback = callback.clone();
88 move |s: Option<(usize, usize)>| {
89 *selection.borrow_mut() = s;
90 callback(s);
91 }
92 };
93
94 fn prev_grapheme_boundary(text: &str, byte: usize) -> usize {
97 let mut last = 0usize;
98 for (i, _) in text.grapheme_indices(true) {
99 if i >= byte {
100 break;
101 }
102 last = i;
103 }
104 last
105 }
106
107 fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
108 for (i, _) in text.grapheme_indices(true) {
109 if i > byte {
110 return i;
111 }
112 }
113 text.len()
114 }
115
116 fn word_range(text: &str, byte: usize) -> (usize, usize) {
117 let byte = byte.min(text.len());
118 let is_word = |g: &str| g.chars().all(|c| c.is_alphanumeric() || c == '_');
119
120 let mut start = byte;
121 while start > 0 {
122 let p = prev_grapheme_boundary(text, start);
123 if is_word(&text[p..start]) {
124 start = p;
125 } else {
126 break;
127 }
128 }
129 let mut end = byte;
130 while end < text.len() {
131 let n = next_grapheme_boundary(text, end);
132 if is_word(&text[end..n]) {
133 end = n;
134 } else {
135 break;
136 }
137 }
138 if start == end {
139 let s = if byte == 0 {
140 0
141 } else {
142 prev_grapheme_boundary(text, byte)
143 };
144 let e = next_grapheme_boundary(text, byte);
145 (s, e.max(s))
146 } else {
147 (start, end)
148 }
149 }
150
151 let on_down = {
154 let text = text_for_handlers.clone();
155 let selection = selection.clone();
156 let anchor = anchor.clone();
157 let dragging = dragging.clone();
158 let last_rect = last_rect.clone();
159 let last_tap_time = last_tap_time.clone();
160 let last_tap_pos = last_tap_pos.clone();
161 let tap_count = tap_count.clone();
162 let set_sel = set_sel.clone();
163 move |ev: PointerEvent| {
164 let r = *last_rect.borrow();
165 if r.w <= 0.0 || r.h <= 0.0 {
166 return;
167 }
168 let font_px = dp_to_px(font_size_dp) * text_scale().0;
169 let lx = (ev.position.x - r.x).max(0.0);
170 let ly = (ev.position.y - r.y).max(0.0);
171 let wrap_w = r.w.max(1.0);
172 let byte = index_for_xy_bytes(&text, font_px, wrap_w, lx, ly);
173
174 let now = Instant::now();
176 let pos = (ev.position.x, ev.position.y);
177 let mut count = *tap_count.borrow();
178 let mut is_multi = false;
179 if let (Some(t), Some(p)) = (*last_tap_time.borrow(), *last_tap_pos.borrow()) {
180 let dt = now.saturating_duration_since(t);
181 let dist = ((pos.0 - p.0).powi(2) + (pos.1 - p.1).powi(2)).sqrt();
182 if dt < Duration::from_millis(DOUBLE_TAP_MS) && dist < TAP_SLOP_PX {
183 count = count.saturating_add(1);
184 is_multi = true;
185 } else if dt < Duration::from_millis(TRIPLE_TAP_MS) && dist < TAP_SLOP_PX {
186 count = count.saturating_add(1);
187 is_multi = true;
188 } else {
189 count = 1;
190 }
191 } else {
192 count = 1;
193 }
194 *tap_count.borrow_mut() = count;
195 *last_tap_time.borrow_mut() = Some(now);
196 *last_tap_pos.borrow_mut() = Some(pos);
197
198 let shift = ev.modifiers.shift;
199
200 if count >= 3 {
201 let sel = Some((0, text.len()));
203 *anchor.borrow_mut() = 0;
204 set_sel(sel);
205 *dragging.borrow_mut() = false;
206 if text.len() > 0 {
207 repose_core::clipboard::set_primary_selection(&text);
208 }
209 let _ = is_multi;
210 return;
211 }
212
213 if count == 2 {
214 let (s, e) = word_range(&text, byte);
216 let sel = Some((s, e));
217 *anchor.borrow_mut() = s;
218 set_sel(sel);
219 *dragging.borrow_mut() = true;
220 if e > s {
221 repose_core::clipboard::set_primary_selection(&text[s..e]);
222 }
223 let _ = is_multi;
224 return;
225 }
226
227 if shift {
229 let a = selection
230 .borrow()
231 .map(|(s, e)| {
232 let _ = (s, e);
233 *anchor.borrow()
234 })
235 .unwrap_or(*anchor.borrow());
236 let sel = Some((a.min(byte), a.max(byte)));
237 set_sel(sel);
238 } else {
239 *anchor.borrow_mut() = byte;
240 set_sel(Some((byte, byte)));
241 }
242 *dragging.borrow_mut() = true;
243 let _ = is_multi;
244 }
245 };
246
247 let on_move = {
248 let text = text_for_handlers.clone();
249 let anchor = anchor.clone();
250 let dragging = dragging.clone();
251 let last_rect = last_rect.clone();
252 let set_sel = set_sel.clone();
253 move |ev: PointerEvent| {
254 if !*dragging.borrow() {
255 return;
256 }
257 let r = *last_rect.borrow();
258 if r.w <= 0.0 || r.h <= 0.0 {
259 return;
260 }
261 let font_px = dp_to_px(font_size_dp) * text_scale().0;
262 let lx = (ev.position.x - r.x).max(0.0);
263 let ly = (ev.position.y - r.y).max(0.0);
264 let wrap_w = r.w.max(1.0);
265 let byte = index_for_xy_bytes(&text, font_px, wrap_w, lx, ly);
266 let a = *anchor.borrow();
267 let sel = Some((a.min(byte), a.max(byte)));
268 set_sel(sel);
269 if let Some((s, e)) = sel
270 && e > s
271 {
272 repose_core::clipboard::set_primary_selection(&text[s..e]);
273 }
274 }
275 };
276
277 let on_up = {
278 let text = text_for_handlers.clone();
279 let selection = selection.clone();
280 let dragging = dragging.clone();
281 let set_sel = set_sel.clone();
282 move |_ev: PointerEvent| {
283 *dragging.borrow_mut() = false;
284 let sel = *selection.borrow();
285 if let Some((a, b)) = sel {
286 let s = a.min(b);
287 let e = a.max(b);
288 if e > s {
289 repose_core::clipboard::set_primary_selection(&text[s..e]);
290 }
291 }
292 set_sel(sel);
293 }
294 };
295
296 let painter = {
299 let text = text_for_paint.clone();
300 let selection = selection.clone();
301 let last_rect = last_rect.clone();
302 move |scene: &mut Scene, rect: Rect, _alpha: f32| {
303 *last_rect.borrow_mut() = rect;
304
305 let (s, e) = match *selection.borrow() {
306 Some((a, b)) if a != b => {
307 if a < b {
308 (a, b)
309 } else {
310 (b, a)
311 }
312 }
313 _ => return,
314 };
315 if e == 0 || e <= s {
316 return;
317 }
318
319 let font_px = dp_to_px(font_size_dp) * text_scale().0;
320 let wrap_w = rect.w.max(1.0);
321 let (sx, sy, sli) = caret_xy_for_byte(&text, font_px, wrap_w, s);
322 let (ex, ey, eli) = caret_xy_for_byte(&text, font_px, wrap_w, e);
323 let th = theme();
324 let brush = Brush::Solid(th.primary.with_alpha(96));
325 let line_h = font_px * 1.2;
326
327 if sli == eli {
328 let x = sx.min(ex);
329 let w = (ex - sx).abs().max(2.0);
330 scene.nodes.push(SceneNode::Rect {
331 rect: Rect {
332 x: rect.x + x,
333 y: rect.y + sy,
334 w,
335 h: line_h,
336 },
337 brush,
338 radius: [0.0; 4],
339 });
340 } else {
341 scene.nodes.push(SceneNode::Rect {
343 rect: Rect {
344 x: rect.x + sx,
345 y: rect.y + sy,
346 w: (rect.w - sx).max(2.0),
347 h: line_h,
348 },
349 brush: brush,
350 radius: [0.0; 4],
351 });
352 if eli > sli + 1 {
354 scene.nodes.push(SceneNode::Rect {
355 rect: Rect {
356 x: rect.x,
357 y: rect.y + (sli as f32 + 1.0) * line_h,
358 w: rect.w,
359 h: (eli as f32 - sli as f32 - 1.0) * line_h,
360 },
361 brush: brush,
362 radius: [0.0; 4],
363 });
364 }
365 scene.nodes.push(SceneNode::Rect {
367 rect: Rect {
368 x: rect.x,
369 y: rect.y + ey,
370 w: ex.max(2.0),
371 h: line_h,
372 },
373 brush,
374 radius: [0.0; 4],
375 });
376 }
377 }
378 };
379
380 v.modifier = v
381 .modifier
382 .on_pointer_down(on_down)
383 .on_pointer_move(on_move)
384 .on_pointer_up(on_up)
385 .painter(painter)
386 .cursor(CursorIcon::Text)
387 .on_action({
388 let selection = selection.clone();
389 let text = text_for_handlers.clone();
390 move |action| match action {
391 repose_core::shortcuts::Action::Copy => {
392 let sel = *selection.borrow();
393 if let Some((a, b)) = sel {
394 let s = a.min(b);
395 let e = a.max(b);
396 if e > s {
397 repose_core::clipboard::copy_to_clipboard(&text[s..e]);
398 return true;
399 }
400 }
401 false
402 }
403 repose_core::shortcuts::Action::SelectAll => {
404 let len = text.len();
405 *selection.borrow_mut() = Some((0, len));
406 true
407 }
408 _ => false,
409 }
410 });
411
412 v
413}