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