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