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