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