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