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