1use std::{
11 cell::{Cell, RefCell},
12 ops::Range,
13 rc::Rc,
14 sync::Arc,
15};
16
17use cranpose_core::MutableState;
18use cranpose_ui_graphics::{Point, ProjectiveTransform, Rect, Size, WindowCoordinates};
19
20use crate::{
21 text::{
22 AnnotatedString, PreparedTextLayout, TextLayoutOptions, TextStyle,
23 layout_cache::CacheAdmission,
24 measure::{SharedText, TextFrame, prepare_shared},
25 },
26 text_selection::{
27 MULTI_TAP_SLOP_PX, MULTI_TAP_TIMEOUT_MS, SelectionAnchor, SelectionGranularity,
28 classify_tap_count, granularity_boundaries, tap_selection_granularity,
29 },
30};
31
32const LONG_PRESS_MS: u64 = 500;
34const LONG_PRESS_SLOP: f32 = 12.0;
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct SelectionEdge {
41 pub selectable: u64,
43 pub offset: usize,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct TextSelection {
52 pub anchor: SelectionEdge,
54 pub focus: SelectionEdge,
56}
57
58#[derive(Default)]
62pub(crate) struct SelectableGeometry {
63 window: Rc<Cell<WindowCoordinates>>,
64 content_origin: RefCell<crate::modifier::CoordinatorRect>,
65 content_size: Cell<Size>,
66}
67
68impl SelectableGeometry {
69 pub(crate) fn window_sink(&self) -> Rc<Cell<WindowCoordinates>> {
70 Rc::clone(&self.window)
71 }
72
73 pub(crate) fn set_content_origin(&self, origin: crate::modifier::CoordinatorRect) {
74 *self.content_origin.borrow_mut() = origin;
75 }
76
77 pub(crate) fn set_content_size(&self, size: Size) {
78 self.content_size.set(size);
79 }
80
81 fn rect(&self) -> Rect {
82 let offset = self.content_origin.borrow().origin();
83 let size = self.content_size.get();
84 self.window
85 .get()
86 .local_to_window
87 .bounds_for_rect(Rect::from_origin_size(offset, size))
88 }
89
90 fn content_to_window(&self) -> ProjectiveTransform {
91 let offset = self.content_origin.borrow().origin();
92 ProjectiveTransform::translation(offset.x, offset.y).then(self.window.get().local_to_window)
93 }
94}
95
96struct Selectable {
97 key: u64,
98 text: Rc<AnnotatedString>,
99 style: Arc<TextStyle>,
100 options: TextLayoutOptions,
101 geometry: Rc<SelectableGeometry>,
102}
103
104impl Selectable {
105 fn len(&self) -> usize {
106 self.text.text.len()
107 }
108
109 fn layout(&self) -> Rc<PreparedTextLayout> {
111 let frame = TextFrame::drawn(self.geometry.content_size.get().width, self.options);
112 let text = SharedText {
113 text: &self.text,
114 style: &self.style,
115 style_hash: self.style.render_hash(),
116 };
117 prepare_shared(&text, frame, CacheAdmission::First, None)
118 }
119
120 fn offset_at(&self, local: Point) -> usize {
122 self.layout().layout.offset_for_position(local.x, local.y)
123 }
124
125 fn highlight(&self, start: usize, end: usize) -> Vec<Rect> {
128 let mut rects = Vec::new();
129 self.layout().layout.selection_rects(start, end, &mut rects);
130 rects
131 }
132}
133
134pub(crate) struct RegistrarInner {
135 next_key: Cell<u64>,
136 selectables: RefCell<Vec<Selectable>>,
137 selection: MutableState<Option<TextSelection>>,
138}
139
140#[derive(Clone)]
143pub struct SelectionRegistrar {
144 inner: Rc<RegistrarInner>,
145}
146
147impl PartialEq for SelectionRegistrar {
148 fn eq(&self, other: &Self) -> bool {
149 Rc::ptr_eq(&self.inner, &other.inner)
150 }
151}
152
153impl SelectionRegistrar {
154 pub(crate) fn new(selection: MutableState<Option<TextSelection>>) -> Self {
155 Self {
156 inner: Rc::new(RegistrarInner {
157 next_key: Cell::new(1),
158 selectables: RefCell::new(Vec::new()),
159 selection,
160 }),
161 }
162 }
163
164 pub(crate) fn subscribe(&self) -> u64 {
166 let key = self.inner.next_key.get();
167 self.inner.next_key.set(key + 1);
168 key
169 }
170
171 pub(crate) fn update(
173 &self,
174 key: u64,
175 text: Rc<AnnotatedString>,
176 style: Arc<TextStyle>,
177 options: TextLayoutOptions,
178 geometry: Rc<SelectableGeometry>,
179 ) {
180 let entry = Selectable {
181 key,
182 text,
183 style,
184 options,
185 geometry,
186 };
187 let mut selectables = self.inner.selectables.borrow_mut();
188 match selectables.iter_mut().find(|held| held.key == key) {
189 Some(held) => *held = entry,
190 None => selectables.push(entry),
191 }
192 }
193
194 pub(crate) fn unsubscribe(&self, key: u64) {
197 self.inner
198 .selectables
199 .borrow_mut()
200 .retain(|held| held.key != key);
201 let touched = self
202 .inner
203 .selection
204 .get_non_reactive()
205 .is_some_and(|selection| {
206 selection.anchor.selectable == key || selection.focus.selectable == key
207 });
208 if touched {
209 self.inner.selection.set(None);
210 }
211 }
212
213 pub fn selection(&self) -> Option<TextSelection> {
215 self.inner.selection.get()
216 }
217
218 pub(crate) fn set_selection(&self, selection: Option<TextSelection>) {
219 self.inner.selection.set(selection);
220 }
221
222 fn reading_order(&self) -> Vec<u64> {
225 let selectables = self.inner.selectables.borrow();
226 let mut order: Vec<(u64, Rect)> = selectables
227 .iter()
228 .map(|held| (held.key, held.geometry.rect()))
229 .collect();
230 order.sort_by(|(_, a), (_, b)| a.y.total_cmp(&b.y).then(a.x.total_cmp(&b.x)));
231 order.into_iter().map(|(key, _)| key).collect()
232 }
233
234 fn with_selectable<R>(&self, key: u64, read: impl FnOnce(&Selectable) -> R) -> Option<R> {
235 self.inner
236 .selectables
237 .borrow()
238 .iter()
239 .find(|held| held.key == key)
240 .map(read)
241 }
242
243 pub(crate) fn edge_at(&self, point: Point) -> Option<SelectionEdge> {
248 let order = self.reading_order();
249 let selectables = self.inner.selectables.borrow();
250 let find = |key: u64| selectables.iter().find(|held| held.key == key);
251 let row: Vec<&Selectable> = order
252 .iter()
253 .filter_map(|key| find(*key))
254 .filter(|held| {
255 let rect = held.geometry.rect();
256 point.y >= rect.y && point.y <= rect.y + rect.height
257 })
258 .collect();
259 let horizontal_gap = |held: &&Selectable| {
260 let rect = held.geometry.rect();
261 (rect.x - point.x)
262 .max(point.x - (rect.x + rect.width))
263 .max(0.0)
264 };
265 if let Some(held) = row
266 .iter()
267 .min_by(|a, b| horizontal_gap(a).total_cmp(&horizontal_gap(b)))
268 {
269 let local = held
270 .geometry
271 .content_to_window()
272 .inverse()?
273 .map_point(point);
274 return Some(SelectionEdge {
275 selectable: held.key,
276 offset: held.offset_at(local),
277 });
278 }
279 let next = order
280 .iter()
281 .filter_map(|key| find(*key))
282 .find(|held| point.y < held.geometry.rect().y);
283 if let Some(held) = next {
284 return Some(SelectionEdge {
285 selectable: held.key,
286 offset: 0,
287 });
288 }
289 order
290 .last()
291 .and_then(|key| find(*key))
292 .map(|held| SelectionEdge {
293 selectable: held.key,
294 offset: held.len(),
295 })
296 }
297
298 fn ordered(&self, selection: TextSelection) -> Option<(SelectionEdge, SelectionEdge)> {
300 let order = self.reading_order();
301 let rank = |edge: SelectionEdge| {
302 order
303 .iter()
304 .position(|key| *key == edge.selectable)
305 .map(|index| (index, edge.offset))
306 };
307 let (anchor, focus) = (rank(selection.anchor)?, rank(selection.focus)?);
308 Some(if anchor <= focus {
309 (selection.anchor, selection.focus)
310 } else {
311 (selection.focus, selection.anchor)
312 })
313 }
314
315 fn covered(&self, selection: TextSelection, key: u64) -> Option<Range<usize>> {
317 let (first, last) = self.ordered(selection)?;
318 let order = self.reading_order();
319 let index = |key: u64| order.iter().position(|held| *held == key);
320 let (at, from, to) = (
321 index(key)?,
322 index(first.selectable)?,
323 index(last.selectable)?,
324 );
325 if at < from || at > to {
326 return None;
327 }
328 let len = self.with_selectable(key, Selectable::len)?;
329 let start = if key == first.selectable {
330 first.offset
331 } else {
332 0
333 };
334 let end = if key == last.selectable {
335 last.offset
336 } else {
337 len
338 };
339 (start < end).then_some(start..end.min(len))
340 }
341
342 pub(crate) fn highlight(&self, key: u64) -> Vec<Rect> {
345 let Some(selection) = self.selection() else {
346 return Vec::new();
347 };
348 let Some(range) = self.covered(selection, key) else {
349 return Vec::new();
350 };
351 self.with_selectable(key, |held| held.highlight(range.start, range.end))
352 .unwrap_or_default()
353 }
354
355 pub(crate) fn first_highlight_in_window(&self) -> Option<Rect> {
358 let selection = self.inner.selection.get_non_reactive()?;
359 let (first, _) = self.ordered(selection)?;
360 let range = self.covered(selection, first.selectable)?;
361 self.with_selectable(first.selectable, |held| {
362 let transform = held.geometry.content_to_window();
363 held.highlight(range.start, range.end)
364 .first()
365 .map(|line| transform.bounds_for_rect(*line))
366 })
367 .flatten()
368 }
369
370 pub fn selected_text(&self) -> String {
372 let Some(selection) = self.inner.selection.get_non_reactive() else {
373 return String::new();
374 };
375 self.reading_order()
376 .into_iter()
377 .filter_map(|key| {
378 let range = self.covered(selection, key)?;
379 self.with_selectable(key, |held| held.text.text[range].to_string())
380 })
381 .collect::<Vec<_>>()
382 .join("\n")
383 }
384
385 pub(crate) fn select_all(&self) {
387 let order = self.reading_order();
388 let (Some(first), Some(last)) = (order.first(), order.last()) else {
389 return;
390 };
391 let Some(len) = self.with_selectable(*last, Selectable::len) else {
392 return;
393 };
394 self.set_selection(Some(TextSelection {
395 anchor: SelectionEdge {
396 selectable: *first,
397 offset: 0,
398 },
399 focus: SelectionEdge {
400 selectable: *last,
401 offset: len,
402 },
403 }));
404 }
405
406 fn unit_at(&self, edge: SelectionEdge, granularity: SelectionGranularity) -> Option<Unit> {
408 self.with_selectable(edge.selectable, |held| Unit {
409 selectable: edge.selectable,
410 anchor: SelectionAnchor::at(&held.text.text, edge.offset, granularity),
411 })
412 }
413
414 fn dragged(&self, unit: Unit, edge: SelectionEdge) -> Option<TextSelection> {
418 let granularity = unit.anchor.granularity;
419 if edge.selectable == unit.selectable {
420 let range = self.with_selectable(edge.selectable, |held| {
421 unit.anchor.dragged_to(&held.text.text, edge.offset)
422 })?;
423 return Some(TextSelection {
424 anchor: SelectionEdge {
425 selectable: edge.selectable,
426 offset: range.start,
427 },
428 focus: SelectionEdge {
429 selectable: edge.selectable,
430 offset: range.end,
431 },
432 });
433 }
434 let (unit_start, unit_end) = self.with_selectable(edge.selectable, |held| {
435 granularity_boundaries(&held.text.text, edge.offset, granularity)
436 })?;
437 let pressed_start = SelectionEdge {
438 selectable: unit.selectable,
439 offset: unit.anchor.start,
440 };
441 let forward = self
442 .ordered(TextSelection {
443 anchor: pressed_start,
444 focus: edge,
445 })
446 .is_some_and(|(first, _)| first == pressed_start);
447 Some(if forward {
448 TextSelection {
449 anchor: pressed_start,
450 focus: SelectionEdge {
451 selectable: edge.selectable,
452 offset: unit_end,
453 },
454 }
455 } else {
456 TextSelection {
457 anchor: SelectionEdge {
458 selectable: unit.selectable,
459 offset: unit.anchor.end,
460 },
461 focus: SelectionEdge {
462 selectable: edge.selectable,
463 offset: unit_start,
464 },
465 }
466 })
467 }
468}
469
470#[derive(Clone, Copy, Debug, PartialEq)]
472struct Unit {
473 selectable: u64,
474 anchor: SelectionAnchor,
475}
476
477impl Unit {
478 fn selection(self) -> TextSelection {
479 TextSelection {
480 anchor: SelectionEdge {
481 selectable: self.selectable,
482 offset: self.anchor.start,
483 },
484 focus: SelectionEdge {
485 selectable: self.selectable,
486 offset: self.anchor.end,
487 },
488 }
489 }
490}
491
492#[derive(Clone, Copy)]
494struct Press {
495 start: Point,
496 touch: bool,
497 unit: Option<Unit>,
500 released_to_scroll: bool,
502}
503
504pub(crate) struct SelectionGesture {
506 registrar: SelectionRegistrar,
507 press: Cell<Option<Press>>,
508 last_tap: Cell<Option<(web_time::Instant, Point, u8)>>,
509 frame_clock: cranpose_core::internal::FrameClock,
510 long_press: RefCell<Option<cranpose_core::internal::FrameCallbackRegistration>>,
511 long_press_start: Cell<Option<u64>>,
512 menu_open: MutableState<bool>,
513}
514
515impl SelectionGesture {
516 pub(crate) fn new(
517 registrar: SelectionRegistrar,
518 frame_clock: cranpose_core::internal::FrameClock,
519 menu_open: MutableState<bool>,
520 ) -> Self {
521 Self {
522 registrar,
523 press: Cell::new(None),
524 last_tap: Cell::new(None),
525 frame_clock,
526 long_press: RefCell::new(None),
527 long_press_start: Cell::new(None),
528 menu_open,
529 }
530 }
531
532 fn activate(&self) {
535 crate::thread_state::with(|state| {
536 let active = &state.active_selection;
537 let previous = active.borrow().upgrade();
538 if let Some(previous) = previous
539 && !Rc::ptr_eq(&previous, &self.registrar.inner)
540 {
541 previous.selection.set(None);
542 }
543 *active.borrow_mut() = Rc::downgrade(&self.registrar.inner);
544 });
545 }
546
547 fn is_active(&self) -> bool {
548 crate::thread_state::with(|state| {
549 state
550 .active_selection
551 .borrow()
552 .upgrade()
553 .is_some_and(|held| Rc::ptr_eq(&held, &self.registrar.inner))
554 })
555 }
556
557 pub(crate) fn on_down(self: &Rc<Self>, event: &crate::PointerEvent) {
558 self.activate();
559 crate::text_field_focus::clear_focus();
560 self.menu_open.set(false);
561 let touch = event.source.is_touch_like();
562 let point = event.global_position;
563 let mut press = Press {
564 start: point,
565 touch,
566 unit: None,
567 released_to_scroll: false,
568 };
569 if touch {
570 self.registrar.set_selection(None);
571 self.press.set(Some(press));
572 self.arm_long_press();
573 return;
574 }
575 let granularity = tap_selection_granularity(self.tap_count(point));
576 press.unit = self
577 .registrar
578 .edge_at(point)
579 .and_then(|edge| self.registrar.unit_at(edge, granularity));
580 let selection = press
581 .unit
582 .filter(|unit| unit.anchor.start < unit.anchor.end)
583 .map(Unit::selection);
584 self.registrar.set_selection(selection);
585 self.press.set(Some(press));
586 }
587
588 fn tap_count(&self, point: Point) -> u8 {
589 let now = web_time::Instant::now();
590 let previous = self.last_tap.get();
591 let count = classify_tap_count(
592 previous.map(|(_, at, count)| (count, at.x, at.y)),
593 previous.map_or(u128::MAX, |(time, _, _)| {
594 now.duration_since(time).as_millis()
595 }),
596 point.x,
597 point.y,
598 MULTI_TAP_TIMEOUT_MS,
599 MULTI_TAP_SLOP_PX,
600 );
601 self.last_tap.set(Some((now, point, count)));
602 count
603 }
604
605 pub(crate) fn on_move(&self, event: &crate::PointerEvent) -> bool {
608 let Some(mut press) = self.press.get() else {
609 return false;
610 };
611 let point = event.global_position;
612 let Some(unit) = press.unit else {
613 if press.touch && !press.released_to_scroll {
614 let moved = (point.x - press.start.x)
615 .abs()
616 .max((point.y - press.start.y).abs());
617 if moved > LONG_PRESS_SLOP {
618 press.released_to_scroll = true;
619 self.press.set(Some(press));
620 self.cancel_long_press();
621 }
622 }
623 return false;
624 };
625 if let Some(selection) = self
626 .registrar
627 .edge_at(point)
628 .and_then(|edge| self.registrar.dragged(unit, edge))
629 {
630 self.registrar.set_selection(Some(selection));
631 }
632 true
633 }
634
635 pub(crate) fn on_up(&self) {
636 self.cancel_long_press();
637 let press = self.press.take();
638 let selected = self
639 .registrar
640 .selection()
641 .is_some_and(|selection| selection.anchor != selection.focus);
642 if press.is_some_and(|press| press.touch && press.unit.is_some()) && selected {
643 self.menu_open.set(true);
644 }
645 }
646
647 pub(crate) fn on_cancel(&self) {
648 self.cancel_long_press();
649 self.press.set(None);
650 }
651
652 fn arm_long_press(self: &Rc<Self>) {
653 let weak = Rc::downgrade(self);
654 let registration = self.frame_clock.with_frame_nanos(move |now| {
655 if let Some(gesture) = weak.upgrade() {
656 gesture.long_press_tick(now);
657 }
658 });
659 *self.long_press.borrow_mut() = Some(registration);
660 crate::request_render_invalidation();
661 }
662
663 fn cancel_long_press(&self) {
664 self.long_press.borrow_mut().take();
665 self.long_press_start.set(None);
666 }
667
668 fn long_press_tick(self: Rc<Self>, now: u64) {
669 self.long_press.borrow_mut().take();
670 let Some(mut press) = self.press.get() else {
671 return;
672 };
673 if press.released_to_scroll || press.unit.is_some() {
674 return;
675 }
676 let start = self.long_press_start.get().unwrap_or(now);
677 self.long_press_start.set(Some(start));
678 if now.saturating_sub(start) < LONG_PRESS_MS * 1_000_000 {
679 self.arm_long_press();
680 return;
681 }
682 press.unit = self
683 .registrar
684 .edge_at(press.start)
685 .and_then(|edge| self.registrar.unit_at(edge, SelectionGranularity::Word));
686 self.press.set(Some(press));
687 self.registrar
688 .set_selection(press.unit.map(Unit::selection));
689 crate::request_render_invalidation();
690 }
691
692 pub(crate) fn on_key(&self, event: &crate::KeyEvent) -> bool {
695 if !event.is_key_down() || !self.is_active() {
696 return false;
697 }
698 let command = event.modifiers().command_or_ctrl();
699 match event.key() {
700 crate::Key::C if command => self.copy(),
701 crate::Key::A if command => {
702 self.registrar.select_all();
703 true
704 }
705 crate::Key::Escape if self.registrar.selection().is_some() => {
706 self.dismiss();
707 true
708 }
709 _ => false,
710 }
711 }
712
713 pub(crate) fn copy(&self) -> bool {
715 let text = self.registrar.selected_text();
716 if text.is_empty() {
717 return false;
718 }
719 crate::clipboard_session::clipboard_write_text(&text);
720 true
721 }
722
723 pub(crate) fn dismiss(&self) {
725 self.registrar.set_selection(None);
726 self.menu_open.set(false);
727 }
728}
729
730#[cfg(test)]
731#[path = "tests/selection_container_tests.rs"]
732mod tests;