1use std::{
2 cell::{Cell, RefCell},
3 collections::HashMap,
4 ops::Range,
5 rc::Rc,
6 sync::atomic::{AtomicU64, Ordering},
7};
8
9use gpui::{
10 App, AppContext as _, Bounds, Context, Element, ElementId, Entity, EntityId, EventEmitter,
11 Global, GlobalElementId, Half, Hitbox, HitboxBehavior, Hsla, InputEvent as _,
12 InspectorElementId, IntoElement, LayoutId, LongPressEvent, MouseButton, MouseDownEvent,
13 MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollDelta, ScrollWheelEvent, SharedString,
14 Style, Subscription, TextLayout, TouchDragEvent, TouchPhase, WeakEntity, Window, point, px,
15};
16
17use crate::text_boundary::{line_range_at, word_range_at};
18use crate::touch_selection::{
19 EdgeDrag, SelectionEdge, TouchHandle, TouchSelectionSnapshot, caret_in_view,
20};
21use crate::{AutoScroll, GlobalState};
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
25pub struct TextSelectionScopeId(u64);
26
27impl TextSelectionScopeId {
28 pub fn new() -> Self {
33 static NEXT_SCOPE_ID: AtomicU64 = AtomicU64::new(1);
34 let value = NEXT_SCOPE_ID
35 .try_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
36 value.checked_add(1)
37 })
38 .expect("text selection scope identifiers exhausted");
39 Self(value)
40 }
41
42 #[cfg(test)]
43 const fn from_raw(value: u64) -> Self {
44 Self(value)
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct TextSelectionContentKey(u64);
51
52impl TextSelectionContentKey {
53 pub const fn new(value: u64) -> Self {
55 Self(value)
56 }
57
58 pub const fn value(self) -> u64 {
60 self.0
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct TextSelectionEndpoint {
67 entity_id: Option<EntityId>,
68 point: Point<Pixels>,
69 content_key: Option<TextSelectionContentKey>,
70}
71
72impl TextSelectionEndpoint {
73 pub(crate) const fn new(entity_id: Option<EntityId>, point: Point<Pixels>) -> Self {
75 Self {
76 entity_id,
77 point,
78 content_key: None,
79 }
80 }
81
82 pub(crate) const fn with_content_key(mut self, content_key: TextSelectionContentKey) -> Self {
84 self.content_key = Some(content_key);
85 self
86 }
87
88 pub const fn entity_id(&self) -> Option<EntityId> {
90 self.entity_id
91 }
92
93 pub const fn content_point(&self) -> Point<Pixels> {
95 self.point
96 }
97
98 pub const fn content_key(&self) -> Option<TextSelectionContentKey> {
100 self.content_key
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct TextSelectionWindowPoints {
107 anchor: Point<Pixels>,
108 cursor: Point<Pixels>,
109}
110
111impl TextSelectionWindowPoints {
112 pub const fn anchor(&self) -> Point<Pixels> {
114 self.anchor
115 }
116
117 pub const fn cursor(&self) -> Point<Pixels> {
119 self.cursor
120 }
121}
122
123#[derive(Clone, Copy, Debug, PartialEq)]
125pub struct TextSelectionSnapshot {
126 anchor: TextSelectionEndpoint,
127 cursor: TextSelectionEndpoint,
128 is_selecting: bool,
129 window_points: Option<TextSelectionWindowPoints>,
130 coverage: TextSelectionCoverage,
131}
132
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
135pub enum TextSelectionCoverage {
136 #[default]
138 Bounded,
139 FromStart,
141 ToEnd,
143 Full,
145}
146
147impl TextSelectionSnapshot {
148 pub(crate) const fn new(anchor: TextSelectionEndpoint, cursor: TextSelectionEndpoint) -> Self {
150 Self {
151 anchor,
152 cursor,
153 is_selecting: false,
154 window_points: None,
155 coverage: TextSelectionCoverage::Bounded,
156 }
157 }
158
159 pub(crate) const fn with_selecting(mut self, is_selecting: bool) -> Self {
161 self.is_selecting = is_selecting;
162 self
163 }
164
165 pub(crate) const fn with_window_points(
167 mut self,
168 window_points: Option<TextSelectionWindowPoints>,
169 ) -> Self {
170 self.window_points = window_points;
171 self
172 }
173
174 #[cfg(test)]
176 pub(crate) const fn with_coverage(mut self, coverage: TextSelectionCoverage) -> Self {
177 self.coverage = coverage;
178 self
179 }
180
181 pub const fn anchor(&self) -> TextSelectionEndpoint {
183 self.anchor
184 }
185
186 pub const fn cursor(&self) -> TextSelectionEndpoint {
188 self.cursor
189 }
190
191 pub const fn is_selecting(&self) -> bool {
193 self.is_selecting
194 }
195
196 pub const fn window_points(&self) -> Option<TextSelectionWindowPoints> {
198 self.window_points
199 }
200
201 pub const fn coverage(&self) -> TextSelectionCoverage {
203 self.coverage
204 }
205}
206
207struct RenderedMarker;
215
216pub struct TextSelectionRegistration {
218 hitbox: Hitbox,
219 bounds: Bounds<Pixels>,
220 scroll_offset: Point<Pixels>,
221 scope: TextSelectionScopeId,
222 document_order: u64,
223 text_bounds: Vec<Bounds<Pixels>>,
224 self_scroll: bool,
225 selection_edges: Option<(Bounds<Pixels>, Bounds<Pixels>)>,
226 rendered: Option<WeakEntity<RenderedMarker>>,
227}
228
229impl TextSelectionRegistration {
230 pub fn new(hitbox: Hitbox, bounds: Bounds<Pixels>) -> Self {
232 Self {
233 hitbox,
234 bounds,
235 scroll_offset: Point::default(),
236 scope: TextSelectionScopeId::default(),
237 document_order: 0,
238 text_bounds: Vec::new(),
239 self_scroll: false,
240 selection_edges: None,
241 rendered: None,
242 }
243 }
244
245 pub fn with_rendered_element(
252 mut self,
253 participant: &TextSelectionHandle,
254 window: &mut Window,
255 cx: &mut App,
256 ) -> Self {
257 self.rendered = Some(
258 window
259 .use_keyed_state(
260 ElementId::NamedInteger(
261 "text-selection-participant".into(),
262 participant.entity_id().as_u64(),
263 ),
264 cx,
265 |_, _| RenderedMarker,
266 )
267 .downgrade(),
268 );
269 self
270 }
271
272 fn is_rendered(&self) -> bool {
275 self.rendered
276 .as_ref()
277 .is_some_and(|marker| marker.upgrade().is_some())
278 }
279
280 pub(crate) fn with_self_scroll(mut self, self_scroll: bool) -> Self {
285 self.self_scroll = self_scroll;
286 self
287 }
288
289 pub fn with_scroll_offset(mut self, scroll_offset: Point<Pixels>) -> Self {
291 self.scroll_offset = scroll_offset;
292 self
293 }
294
295 pub fn with_scope(mut self, scope: TextSelectionScopeId) -> Self {
297 self.scope = scope;
298 self
299 }
300
301 pub fn with_document_order(mut self, document_order: u64) -> Self {
303 self.document_order = document_order;
304 self
305 }
306
307 pub fn with_text_bounds(mut self, text_bounds: Vec<Bounds<Pixels>>) -> Self {
309 self.text_bounds = text_bounds;
310 self
311 }
312
313 pub fn with_selection_edges(mut self, start: Bounds<Pixels>, end: Bounds<Pixels>) -> Self {
317 self.selection_edges = Some((start, end));
318 self
319 }
320
321 pub fn hitbox(&self) -> &Hitbox {
323 &self.hitbox
324 }
325
326 pub const fn bounds(&self) -> Bounds<Pixels> {
328 self.bounds
329 }
330
331 pub const fn scroll_offset(&self) -> Point<Pixels> {
333 self.scroll_offset
334 }
335
336 pub const fn scope(&self) -> TextSelectionScopeId {
338 self.scope
339 }
340
341 pub const fn document_order(&self) -> u64 {
343 self.document_order
344 }
345
346 pub fn text_bounds(&self) -> &[Bounds<Pixels>] {
348 &self.text_bounds
349 }
350
351 pub const fn selection_edges(&self) -> Option<(Bounds<Pixels>, Bounds<Pixels>)> {
353 self.selection_edges
354 }
355}
356
357#[derive(Clone)]
359pub struct TextSelectionRun {
360 document_order: u64,
362 text: SharedString,
364 layout: TextLayout,
366 bounds: Bounds<Pixels>,
368}
369
370impl TextSelectionRun {
371 pub fn new(text: impl Into<SharedString>, layout: TextLayout, bounds: Bounds<Pixels>) -> Self {
373 Self {
374 document_order: 0,
375 text: text.into(),
376 layout,
377 bounds,
378 }
379 }
380
381 pub const fn with_document_order(mut self, document_order: u64) -> Self {
383 self.document_order = document_order;
384 self
385 }
386
387 pub const fn document_order(&self) -> u64 {
389 self.document_order
390 }
391
392 pub fn text(&self) -> &SharedString {
394 &self.text
395 }
396
397 pub fn layout(&self) -> &TextLayout {
399 &self.layout
400 }
401
402 pub const fn bounds(&self) -> Bounds<Pixels> {
404 self.bounds
405 }
406}
407
408#[derive(Clone, Debug, Default, PartialEq, Eq)]
410pub struct TextSelectionProjection {
411 ranges: Vec<Option<Range<usize>>>,
413 is_active: bool,
415}
416
417impl TextSelectionProjection {
418 pub fn ranges(&self) -> &[Option<Range<usize>>] {
420 &self.ranges
421 }
422
423 pub const fn is_active(&self) -> bool {
425 self.is_active
426 }
427}
428
429fn project_ranges(
435 snapshot: Option<TextSelectionSnapshot>,
436 runs: &[TextSelectionRun],
437) -> TextSelectionProjection {
438 let Some(snapshot) = snapshot else {
439 return TextSelectionProjection {
440 ranges: vec![None; runs.len()],
441 is_active: false,
442 };
443 };
444 let Some(window_points) = snapshot.window_points() else {
445 return TextSelectionProjection {
446 ranges: vec![None; runs.len()],
447 is_active: true,
448 };
449 };
450
451 TextSelectionProjection {
452 ranges: runs
453 .iter()
454 .map(|run| selection_range_for_run(run, window_points.anchor, window_points.cursor))
455 .collect(),
456 is_active: true,
457 }
458}
459
460fn selection_range_for_run(
461 run: &TextSelectionRun,
462 selection_start: Point<Pixels>,
463 selection_end: Point<Pixels>,
464) -> Option<Range<usize>> {
465 if run.text.len() != run.layout.len() {
466 return None;
467 }
468
469 if run.text.is_empty() {
470 return None;
471 }
472
473 let line_height = run.layout.line_height();
474 let (rows_top, rows_bottom) = text_rows_extent(&run.layout, line_height);
479 let band_top = selection_start.y.min(selection_end.y);
480 let band_bottom = selection_start.y.max(selection_end.y);
481 if rows_bottom <= band_top || rows_top > band_bottom {
482 return None;
483 }
484 if band_top < rows_top && band_bottom >= rows_bottom {
485 return Some(0..run.text.len());
486 }
487
488 let mut range = None;
489 for (offset, character) in run.text.char_indices() {
490 let next_offset = offset + character.len_utf8();
491 let Some(position) = run.layout.position_for_index(offset) else {
492 continue;
493 };
494
495 let char_width = run
496 .layout
497 .position_for_index(next_offset)
498 .filter(|next| next.y == position.y)
499 .map_or_else(|| line_height.half(), |next| next.x - position.x);
500
501 if point_in_selection_band(
502 position,
503 char_width,
504 selection_start,
505 selection_end,
506 line_height,
507 ) {
508 range.get_or_insert(offset..offset).end = next_offset;
509 }
510 }
511 range
512}
513
514pub(crate) fn text_rows_extent(text_layout: &TextLayout, line_height: Pixels) -> (Pixels, Pixels) {
523 let top = text_layout.bounds().top();
524 let layout_line_height = text_layout.line_height();
525 let lines = text_layout.line_layouts();
526 let mut last_line_top = top;
527 for line in lines.iter().take(lines.len().saturating_sub(1)) {
528 last_line_top += line.size(layout_line_height).height;
529 }
530 let last_row_top = lines.last().map_or(top, |line| {
531 last_line_top + line.wrap_boundaries.len() as f32 * layout_line_height
532 });
533 (top, last_row_top + line_height)
534}
535
536fn points_for_multi_click(
537 runs: &[TextSelectionRun],
538 position: Point<Pixels>,
539 click_count: usize,
540) -> Option<(Point<Pixels>, Point<Pixels>)> {
541 let run = runs.iter().find(|run| run.bounds.contains(&position))?;
542 if run.text.len() != run.layout.len() {
543 return None;
544 }
545 let offset = run.layout.index_for_position(position).ok()?;
546 let range = match click_count {
547 2 => word_range_at(&run.text, offset)?,
548 3.. => line_range_at(&run.text, offset),
549 _ => return None,
550 };
551 if range.is_empty() {
552 return None;
553 }
554 Some((
555 run.layout.position_for_index(range.start)?,
556 run.layout.position_for_index(range.end)?,
557 ))
558}
559
560fn point_in_selection_band(
561 position: Point<Pixels>,
562 char_width: Pixels,
563 selection_start: Point<Pixels>,
564 selection_end: Point<Pixels>,
565 line_height: Pixels,
566) -> bool {
567 let point_in_line =
568 |point: Point<Pixels>| point.y >= position.y && point.y < position.y + line_height;
569 let top = selection_start.y.min(selection_end.y);
570 let bottom = selection_start.y.max(selection_end.y);
571 let x = position.x + char_width.half();
572
573 if position.y + line_height <= top || position.y > bottom {
574 return false;
575 }
576
577 if point_in_line(selection_start) && point_in_line(selection_end) {
578 let left = selection_start.x.min(selection_end.x);
579 let right = selection_start.x.max(selection_end.x);
580 return x >= left && x <= right;
581 }
582
583 let (top_point, bottom_point) = if selection_start.y < selection_end.y {
584 (selection_start, selection_end)
585 } else {
586 (selection_end, selection_start)
587 };
588 if point_in_line(top_point) {
589 x >= top_point.x
590 } else if point_in_line(bottom_point) {
591 x <= bottom_point.x
592 } else {
593 true
594 }
595}
596
597type FocusCallback = Rc<dyn Fn(&mut Window, &mut App)>;
598type ClearHandler = Rc<dyn Fn(&mut App)>;
599type CopyCallback = Rc<dyn Fn(&mut App) -> String>;
600type ContentKeyResolver = Rc<dyn Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey>>;
601
602#[derive(Clone, Copy, Debug, PartialEq)]
604pub enum TextSelectionEvent {
605 SelectionChanged(Option<TextSelectionSnapshot>),
607 AutoScroll(Option<Pixels>),
609 Cleared,
611 TouchSelectionChanged,
616}
617
618struct CopyItem {
619 document_order: u64,
620 callback: Option<CopyCallback>,
621 fallback: String,
622}
623
624fn resolve_copy_items(mut items: Vec<CopyItem>, cx: &mut App) -> String {
625 items.sort_by_key(|item| item.document_order);
626 items
627 .into_iter()
628 .map(|item| {
629 item.callback
630 .map(|callback| callback(cx))
631 .unwrap_or(item.fallback)
632 })
633 .filter(|text| !text.trim().is_empty())
634 .collect::<Vec<_>>()
635 .join("\n")
636}
637
638fn dispatch_clear_handlers(handlers: Vec<ClearHandler>, cx: &mut App) {
639 for handler in handlers {
640 handler(cx);
641 }
642}
643
644struct SelectableTextState {
645 fallback_copy_text: String,
646 projected_copy_text: RefCell<Option<String>>,
649 runs: RefCell<Vec<TextSelectionRun>>,
651 local_selection: bool,
652 snapshot: Option<TextSelectionSnapshot>,
653 on_focus: Option<FocusCallback>,
654 clear: Option<ClearHandler>,
655 copy: Option<CopyCallback>,
656 content_key_resolver: Option<ContentKeyResolver>,
657}
658
659impl EventEmitter<TextSelectionEvent> for SelectableTextState {}
660
661impl SelectableTextState {
662 fn new(fallback_copy_text: impl Into<String>) -> Self {
663 Self {
664 fallback_copy_text: fallback_copy_text.into(),
665 projected_copy_text: RefCell::new(None),
666 runs: RefCell::new(Vec::new()),
667 local_selection: false,
668 snapshot: None,
669 on_focus: None,
670 clear: None,
671 copy: None,
672 content_key_resolver: None,
673 }
674 }
675
676 fn snapshot(&self) -> Option<TextSelectionSnapshot> {
678 self.snapshot
679 }
680
681 fn set_fallback_copy_text(&mut self, text: impl Into<String>) {
683 self.fallback_copy_text = text.into();
684 *self.projected_copy_text.get_mut() = None;
685 }
686
687 fn set_local_selection(&mut self, active: bool) {
689 self.local_selection = active;
690 }
691
692 fn update_runs(&self, runs: &[TextSelectionRun]) -> TextSelectionProjection {
699 *self.runs.borrow_mut() = runs.to_vec();
700 let states = project_ranges(self.snapshot, runs);
701 let mut selected_runs = runs
702 .iter()
703 .zip(states.ranges())
704 .enumerate()
705 .filter_map(|(index, (run, state))| {
706 state.as_ref().map(|range| {
707 debug_assert!(run.text.is_char_boundary(range.start));
708 debug_assert!(run.text.is_char_boundary(range.end));
709 (
710 run.document_order,
711 index,
712 run.text[range.clone()].to_string(),
713 )
714 })
715 })
716 .collect::<Vec<_>>();
717 selected_runs.sort_by_key(|(order, index, _)| (*order, *index));
718 *self.projected_copy_text.borrow_mut() =
719 Some(selected_runs.into_iter().map(|(_, _, text)| text).collect());
720 states
721 }
722
723 fn set_focus_handler(&mut self, callback: impl Fn(&mut Window, &mut App) + 'static) {
725 self.on_focus = Some(Rc::new(callback));
726 }
727
728 fn clear_with(&mut self, callback: impl Fn(&mut App) + 'static) {
729 self.clear = Some(Rc::new(callback));
730 }
731
732 fn copy_with(&mut self, callback: impl Fn(&mut App) -> String + 'static) {
734 self.copy = Some(Rc::new(callback));
735 }
736
737 fn resolve_content_key_with(
739 &mut self,
740 callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
741 ) {
742 self.content_key_resolver = Some(Rc::new(callback));
743 }
744
745 fn set_snapshot(&mut self, snapshot: Option<TextSelectionSnapshot>, cx: &mut Context<Self>) {
746 if self.snapshot == snapshot {
747 return;
748 }
749 self.snapshot = snapshot;
750 *self.projected_copy_text.get_mut() = None;
751 cx.emit(TextSelectionEvent::SelectionChanged(snapshot));
752 }
753
754 fn has_selection_to_clear(&self) -> bool {
757 self.snapshot.is_some() || self.local_selection
758 }
759
760 fn clear_state(&mut self, cx: &mut Context<Self>) -> Option<ClearHandler> {
761 self.snapshot = None;
762 *self.projected_copy_text.get_mut() = None;
763 self.local_selection = false;
764 cx.emit(TextSelectionEvent::Cleared);
765 cx.emit(TextSelectionEvent::SelectionChanged(None));
766 self.clear.clone()
767 }
768
769 fn set_auto_scroll(&self, delta: Option<Pixels>, cx: &mut Context<Self>) {
770 cx.emit(TextSelectionEvent::AutoScroll(delta));
771 }
772
773 fn focus(&self, window: &mut Window, cx: &mut App) {
774 if let Some(callback) = self.on_focus.clone() {
775 window.defer(cx, move |window, cx| callback(window, cx));
776 }
777 }
778
779 fn copy_item(&self, document_order: u64) -> Option<CopyItem> {
780 (self.snapshot.is_some() || self.local_selection).then(|| CopyItem {
781 document_order,
782 callback: self.copy.clone(),
783 fallback: self
784 .projected_copy_text
785 .borrow()
786 .clone()
787 .unwrap_or_else(|| self.fallback_copy_text.clone()),
788 })
789 }
790}
791
792#[derive(Default)]
794pub struct TouchHandleLayout {
795 hitboxes: Vec<(SelectionEdge, Hitbox)>,
796}
797
798#[derive(Clone)]
800pub struct TextSelectionHandle(Entity<SelectableTextState>);
801
802impl TextSelectionHandle {
803 pub fn new(fallback_copy_text: impl Into<String>, cx: &mut App) -> Self {
805 Self(cx.new(|_| SelectableTextState::new(fallback_copy_text)))
806 }
807
808 pub fn entity_id(&self) -> EntityId {
810 self.0.entity_id()
811 }
812
813 pub fn snapshot(&self, cx: &App) -> Option<TextSelectionSnapshot> {
815 self.0.read(cx).snapshot()
816 }
817
818 pub fn set_fallback_copy_text(&self, text: impl Into<String>, cx: &mut App) {
820 self.0
821 .update(cx, |state, _| state.set_fallback_copy_text(text));
822 }
823
824 pub fn set_local_selection(&self, active: bool, cx: &mut App) {
826 self.0
827 .update(cx, |state, _| state.set_local_selection(active));
828 }
829
830 pub fn has_local_selection(&self, cx: &App) -> bool {
832 self.0.read(cx).local_selection
833 }
834
835 pub fn register(
837 &self,
838 mut registration: TextSelectionRegistration,
839 window: &mut Window,
840 cx: &mut App,
841 ) {
842 if let Some(scope) = current_text_selection_scope(window.window_handle().window_id(), cx) {
843 registration.scope = scope;
844 }
845 let Some(state) = WindowSelectionState::existing(window, cx) else {
846 return;
847 };
848 let Some(registration) = state.read(cx).register_quietly(self, registration) else {
849 return;
850 };
851 state.update(cx, |state, cx| {
852 state.register_participant(self.clone(), registration, cx)
853 });
854 }
855
856 pub fn update_runs(&self, runs: &[TextSelectionRun], cx: &mut App) -> TextSelectionProjection {
858 self.0.read(cx).update_runs(runs)
859 }
860
861 pub(crate) fn set_hit_test_runs(&self, runs: &[TextSelectionRun], cx: &mut App) {
863 *self.0.read(cx).runs.borrow_mut() = runs.to_vec();
864 }
865
866 pub fn subscribe(
868 &self,
869 mut callback: impl FnMut(&TextSelectionEvent, &mut App) + 'static,
870 cx: &mut App,
871 ) -> Subscription {
872 cx.subscribe(&self.0, move |_, event, cx| callback(event, cx))
873 }
874
875 #[must_use = "retain the subscription or explicitly detach it"]
877 pub fn refresh_window_on_change(&self, window: &Window, cx: &mut App) -> Subscription {
878 let window = window.window_handle();
879 self.subscribe(
880 move |event, cx| {
881 if matches!(event, TextSelectionEvent::SelectionChanged(_)) {
882 _ = window.update(cx, |_, window, _| window.refresh());
883 }
884 },
885 cx,
886 )
887 }
888
889 pub fn focus_with(&self, callback: impl Fn(&mut Window, &mut App) + 'static, cx: &mut App) {
891 self.0
892 .update(cx, |state, _| state.set_focus_handler(callback));
893 }
894
895 pub fn clear_with(&self, callback: impl Fn(&mut App) + 'static, cx: &mut App) {
897 self.0.update(cx, |state, _| state.clear_with(callback));
898 }
899
900 pub fn copy_with(&self, callback: impl Fn(&mut App) -> String + 'static, cx: &mut App) {
902 self.0.update(cx, |state, _| state.copy_with(callback));
903 }
904
905 pub fn prepaint_touch_handles(&self, window: &mut Window, cx: &App) -> TouchHandleLayout {
910 let Some(state) = WindowSelectionState::existing(window, cx) else {
911 return TouchHandleLayout::default();
912 };
913 let hitboxes = state
914 .read(cx)
915 .touch_handles_of(self.entity_id())
916 .into_iter()
917 .map(|(edge, caret)| {
918 let hitbox = window.insert_hitbox(
919 TouchHandle::hit_bounds(edge, caret),
920 HitboxBehavior::BlockMouse,
921 );
922 (edge, hitbox)
923 })
924 .collect();
925 TouchHandleLayout { hitboxes }
926 }
927
928 pub fn paint_touch_handles(
936 &self,
937 layout: &TouchHandleLayout,
938 color: Hsla,
939 window: &mut Window,
940 cx: &mut App,
941 ) {
942 let Some(state) = WindowSelectionState::existing(window, cx) else {
943 return;
944 };
945 for (edge, caret) in state.read(cx).touch_handles_of(self.entity_id()) {
946 TouchHandle::paint(edge, caret, color, window);
947 }
948 let bounds: Vec<Bounds<Pixels>> = layout
949 .hitboxes
950 .iter()
951 .map(|(_, hitbox)| hitbox.bounds)
952 .collect();
953 if !state.read(cx).handles_painted_at(self.entity_id(), &bounds) {
954 state.update(cx, |state, _| {
955 state.register_handles(self.entity_id(), bounds)
956 });
957 }
958 for (edge, hitbox) in &layout.hitboxes {
959 let edge = *edge;
960 window.on_mouse_event({
963 let hitbox = hitbox.clone();
964 let state = state.downgrade();
965 move |event: &TouchDragEvent, phase, window, cx| {
966 if !phase.bubble()
967 || event.phase != TouchPhase::Started
968 || window.default_prevented()
969 || !hitbox.is_hovered(window)
970 {
971 return;
972 }
973 let Some(state) = state.upgrade() else {
974 return;
975 };
976 window.prevent_default();
977 cx.stop_propagation();
978 state.update(cx, |state, cx| {
979 state.begin_edge_drag(edge, event.position, window, cx)
980 });
981 WindowSelectionState::resolve_content_keys(&state, cx);
982 }
983 });
984 window.on_mouse_event({
986 let hitbox = hitbox.clone();
987 let state = state.downgrade();
988 move |event: &MouseDownEvent, phase, window, cx| {
989 if !phase.bubble()
990 || event.button != MouseButton::Left
991 || !hitbox.is_hovered(window)
992 {
993 return;
994 }
995 let Some(state) = state.upgrade() else {
996 return;
997 };
998 cx.stop_propagation();
999 state.update(cx, |state, cx| {
1000 state.begin_edge_drag(edge, event.position, window, cx)
1001 });
1002 WindowSelectionState::resolve_content_keys(&state, cx);
1003 }
1004 });
1005 }
1006 }
1007
1008 pub fn resolve_content_key_with(
1010 &self,
1011 callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
1012 cx: &mut App,
1013 ) {
1014 self.0
1015 .update(cx, |state, _| state.resolve_content_key_with(callback));
1016 }
1017
1018 fn downgrade(&self) -> WeakEntity<SelectableTextState> {
1019 self.0.downgrade()
1020 }
1021}
1022
1023#[derive(Clone)]
1024struct ParticipantRegistration {
1025 participant: WeakEntity<SelectableTextState>,
1026 registration: Rc<TextSelectionRegistration>,
1027 generation: u64,
1028}
1029
1030#[derive(Clone)]
1031struct SelectionEndpoint {
1032 participant: Option<WeakEntity<SelectableTextState>>,
1033 point: Point<Pixels>,
1034 inside: bool,
1035 inside_text: bool,
1036 content_key: Option<TextSelectionContentKey>,
1037 content_key_resolver: Option<(ContentKeyResolver, Point<Pixels>)>,
1038}
1039
1040impl SelectionEndpoint {
1041 fn snapshot(&self) -> TextSelectionEndpoint {
1042 let snapshot = TextSelectionEndpoint::new(self.entity_id(), self.point);
1043 if let Some(content_key) = self.content_key {
1044 snapshot.with_content_key(content_key)
1045 } else {
1046 snapshot
1047 }
1048 }
1049
1050 fn resolve(
1051 &self,
1052 participants: &HashMap<EntityId, ParticipantRegistration>,
1053 ) -> Option<Point<Pixels>> {
1054 let participant = self.participant.as_ref()?;
1055 let registration = participants.get(&participant.entity_id())?;
1056 participant.upgrade()?;
1057 Some(
1058 self.point
1059 + registration.registration.scroll_offset
1060 + registration.registration.bounds.origin,
1061 )
1062 }
1063
1064 fn entity_id(&self) -> Option<EntityId> {
1065 self.participant
1066 .as_ref()
1067 .map(|participant| participant.entity_id())
1068 }
1069}
1070
1071#[derive(Default)]
1077struct TouchSelection {
1078 active: bool,
1080 menu_open: bool,
1081 drag: Option<EdgeDrag>,
1082 ui_bounds: Vec<Bounds<Pixels>>,
1086 previous_ui_bounds: Vec<Bounds<Pixels>>,
1087 handle_bounds: HashMap<EntityId, Vec<Bounds<Pixels>>>,
1091 last_edges: std::cell::Cell<Option<(Bounds<Pixels>, Bounds<Pixels>, bool, bool)>>,
1096}
1097
1098impl TouchSelection {
1099 fn covers(&self, position: Point<Pixels>) -> bool {
1100 self.active
1101 && self
1102 .ui_bounds
1103 .iter()
1104 .chain(&self.previous_ui_bounds)
1105 .chain(self.handle_bounds.values().flatten())
1106 .any(|bounds| bounds.contains(&position))
1107 }
1108
1109 fn painted_any(&self) -> bool {
1112 !self.ui_bounds.is_empty() || !self.previous_ui_bounds.is_empty()
1113 }
1114
1115 fn begin_frame(&mut self) {
1117 self.previous_ui_bounds = std::mem::take(&mut self.ui_bounds);
1118 }
1119
1120 fn reset(&mut self) -> bool {
1122 let had = self.active;
1123 self.handle_bounds.clear();
1124 self.active = false;
1125 self.menu_open = false;
1126 self.drag = None;
1127 self.last_edges.set(None);
1128 had
1129 }
1130}
1131
1132#[derive(Default)]
1134struct WindowSelectionState {
1135 participants: RefCell<HashMap<EntityId, ParticipantRegistration>>,
1139 active_scope: TextSelectionScopeId,
1140 anchor: Option<SelectionEndpoint>,
1141 cursor: Option<SelectionEndpoint>,
1142 pending_extension_anchor: Option<SelectionEndpoint>,
1143 is_selecting: bool,
1144 did_hit_text: bool,
1145 frame_generation: Cell<u64>,
1146 finish_frame_scheduled: Cell<bool>,
1147 refresh_held_cursor: bool,
1148 mouse_down_prepared: bool,
1149 auto_scroll: AutoScroll,
1150 auto_scroll_stall: (Option<(Bounds<Pixels>, Point<Pixels>)>, u8),
1155 touch: TouchSelection,
1156 entity_id: Option<EntityId>,
1159}
1160
1161impl WindowSelectionState {
1162 fn resolve_content_keys(state: &Entity<Self>, cx: &mut App) {
1163 let pending = state.read_with(cx, |state, _| {
1164 [
1165 state
1166 .anchor
1167 .as_ref()
1168 .and_then(|endpoint| endpoint.content_key_resolver.clone()),
1169 state
1170 .cursor
1171 .as_ref()
1172 .and_then(|endpoint| endpoint.content_key_resolver.clone()),
1173 ]
1174 });
1175 if pending.iter().all(Option::is_none) && !state.read(cx).is_active() {
1178 return;
1179 }
1180 let resolved =
1181 pending.map(|pending| pending.and_then(|(callback, point)| callback(point, cx)));
1182 state.update(cx, |state, cx| {
1183 if let (Some(endpoint), Some(key)) = (state.anchor.as_mut(), resolved[0]) {
1184 endpoint.content_key = Some(key);
1185 endpoint.content_key_resolver = None;
1186 }
1187 if let (Some(endpoint), Some(key)) = (state.cursor.as_mut(), resolved[1]) {
1188 endpoint.content_key = Some(key);
1189 endpoint.content_key_resolver = None;
1190 }
1191 state.publish_snapshots(cx);
1192 });
1193 }
1194 fn acquire(window_id: gpui::WindowId, cx: &mut App) -> Entity<Self> {
1195 if !cx.has_global::<SelectionStateRegistry>() {
1196 cx.set_global(SelectionStateRegistry::default());
1197 }
1198 if let Some(state) = cx
1199 .global::<SelectionStateRegistry>()
1200 .0
1201 .get(&window_id)
1202 .and_then(WeakEntity::upgrade)
1203 {
1204 return state;
1205 }
1206
1207 let active_scope = if cx.has_global::<PendingTextSelectionScopes>() {
1208 cx.global_mut::<PendingTextSelectionScopes>()
1209 .0
1210 .remove(&window_id)
1211 .unwrap_or_default()
1212 } else {
1213 TextSelectionScopeId::default()
1214 };
1215
1216 let state = cx.new(move |cx| {
1217 let entity_id = cx.entity_id();
1218 cx.on_release(move |state: &mut WindowSelectionState, cx| {
1219 let handlers = state.clear_state(cx);
1220 if cx.has_global::<SelectionStateRegistry>() {
1221 let registry = &mut cx.global_mut::<SelectionStateRegistry>().0;
1222 if registry
1223 .get(&window_id)
1224 .is_some_and(|state| state.entity_id() == entity_id)
1225 {
1226 registry.remove(&window_id);
1227 }
1228 }
1229 if !handlers.is_empty() {
1230 cx.defer(move |cx| dispatch_clear_handlers(handlers, cx));
1231 }
1232 })
1233 .detach();
1234 Self {
1235 active_scope,
1236 entity_id: Some(entity_id),
1237 ..Self::default()
1238 }
1239 });
1240 cx.global_mut::<SelectionStateRegistry>()
1241 .0
1242 .insert(window_id, state.downgrade());
1243 state
1244 }
1245
1246 #[cfg(test)]
1247 fn ensure(window: &Window, cx: &mut App) -> Entity<Self> {
1248 Self::acquire(window.window_handle().window_id(), cx)
1249 }
1250
1251 fn existing(window: &Window, cx: &App) -> Option<Entity<Self>> {
1252 if !cx.has_global::<SelectionStateRegistry>() {
1253 return None;
1254 }
1255 cx.global::<SelectionStateRegistry>()
1256 .0
1257 .get(&window.window_handle().window_id())
1258 .and_then(WeakEntity::upgrade)
1259 }
1260
1261 #[cfg(test)]
1263 fn set_active_scope(&mut self, scope: TextSelectionScopeId, cx: &mut App) {
1264 let handlers = self.set_active_scope_state(scope, cx);
1265 dispatch_clear_handlers(handlers, cx);
1266 }
1267
1268 fn set_active_scope_state(
1269 &mut self,
1270 scope: TextSelectionScopeId,
1271 cx: &mut App,
1272 ) -> Vec<ClearHandler> {
1273 if self.active_scope == scope {
1274 return Vec::new();
1275 }
1276 let handlers = self.clear_state(cx);
1277 self.active_scope = scope;
1278 self.publish_snapshots(cx);
1279 handlers
1280 }
1281
1282 pub fn finish_frame(&mut self, cx: &mut App) -> Vec<ClearHandler> {
1298 self.finish_frame_scheduled.set(false);
1299 let generation = self.frame_generation.get();
1300 let stale = self
1301 .participants
1302 .get_mut()
1303 .iter()
1304 .filter_map(|(id, registration)| {
1305 (registration.generation != generation && !registration.registration.is_rendered())
1306 .then(|| (*id, registration.participant.clone()))
1307 })
1308 .collect::<Vec<_>>();
1309 let endpoint_departed = stale.iter().any(|(id, _)| {
1310 [
1311 self.anchor.as_ref(),
1312 self.cursor.as_ref(),
1313 self.pending_extension_anchor.as_ref(),
1314 ]
1315 .into_iter()
1316 .flatten()
1317 .any(|endpoint| endpoint.entity_id() == Some(*id))
1318 });
1319 let mut handlers = if endpoint_departed {
1320 self.clear_state(cx)
1321 } else {
1322 Vec::new()
1323 };
1324 for (id, participant) in stale {
1325 self.participants.get_mut().remove(&id);
1326 if let Some(participant) = participant.upgrade()
1327 && participant.read(cx).has_selection_to_clear()
1328 {
1329 if let Some(handler) = participant.update(cx, |state, cx| state.clear_state(cx)) {
1330 handlers.push(handler);
1331 }
1332 }
1333 }
1334 self.publish_snapshots(cx);
1335 self.frame_generation.set(generation.wrapping_add(1));
1336 handlers
1337 }
1338
1339 fn schedule_finish_frame(&self) -> bool {
1340 !self.finish_frame_scheduled.replace(true)
1341 }
1342
1343 fn is_active(&self) -> bool {
1346 self.anchor.is_some()
1347 || self.cursor.is_some()
1348 || self.pending_extension_anchor.is_some()
1349 || self.is_selecting
1350 || self.refresh_held_cursor
1351 || self.touch.active
1352 }
1353
1354 fn register_quietly(
1361 &self,
1362 selection: &TextSelectionHandle,
1363 registration: TextSelectionRegistration,
1364 ) -> Option<TextSelectionRegistration> {
1365 if self.is_active() {
1366 return Some(registration);
1367 }
1368 self.participants.borrow_mut().insert(
1369 selection.entity_id(),
1370 ParticipantRegistration {
1371 participant: selection.downgrade(),
1372 registration: Rc::new(registration),
1373 generation: self.frame_generation.get(),
1374 },
1375 );
1376 None
1377 }
1378
1379 fn finish_frame_quietly(&self, cx: &App) -> bool {
1383 if self.is_active() {
1384 return false;
1385 }
1386 let generation = self.frame_generation.get();
1387 let mut participants = self.participants.borrow_mut();
1388 let gone = |registration: &ParticipantRegistration| {
1389 registration.generation != generation && !registration.registration.is_rendered()
1390 };
1391 if participants.values().any(|registration| {
1392 gone(registration)
1393 && registration
1394 .participant
1395 .upgrade()
1396 .is_some_and(|participant| participant.read(cx).has_selection_to_clear())
1397 }) {
1398 return false;
1399 }
1400 participants.retain(|_, registration| {
1401 !gone(registration) && registration.participant.upgrade().is_some()
1402 });
1403 drop(participants);
1404 self.finish_frame_scheduled.set(false);
1405 self.frame_generation.set(generation.wrapping_add(1));
1406 true
1407 }
1408
1409 pub fn register_participant(
1411 &mut self,
1412 selection: TextSelectionHandle,
1413 registration: TextSelectionRegistration,
1414 cx: &mut App,
1415 ) {
1416 self.prune_dead_participants();
1417 if self.is_selecting
1418 && registration.self_scroll
1419 && self.anchor.as_ref().and_then(SelectionEndpoint::entity_id)
1420 == Some(selection.entity_id())
1421 && self
1422 .participants
1423 .get_mut()
1424 .get(&selection.entity_id())
1425 .is_some_and(|previous| {
1426 previous.registration.scroll_offset != registration.scroll_offset
1427 || previous.registration.bounds != registration.bounds
1428 })
1429 {
1430 self.refresh_held_cursor = true;
1431 }
1432 let edges_moved = self.touch.active
1435 && self
1436 .participants
1437 .get_mut()
1438 .get(&selection.entity_id())
1439 .is_none_or(|previous| {
1440 previous.registration.selection_edges != registration.selection_edges
1441 });
1442 self.participants.get_mut().insert(
1443 selection.entity_id(),
1444 ParticipantRegistration {
1445 participant: selection.downgrade(),
1446 registration: Rc::new(registration),
1447 generation: self.frame_generation.get(),
1448 },
1449 );
1450 self.publish_snapshots(cx);
1451 if edges_moved {
1452 self.touch_changed(cx);
1453 }
1454 }
1455
1456 #[cfg(test)]
1458 fn begin(&mut self, position: Point<Pixels>, extend: bool, cx: &mut App) {
1459 self.begin_impl(position, extend, false, None, cx);
1460 }
1461
1462 #[cfg(test)]
1464 fn update(&mut self, position: Point<Pixels>, cx: &mut App) {
1465 self.update_impl(position, None, cx);
1466 }
1467
1468 pub fn end(&mut self, cx: &mut App) {
1470 self.pending_extension_anchor = None;
1471 if !self.is_selecting {
1472 return;
1473 }
1474 self.is_selecting = false;
1475 if !self.did_hit_text {
1476 self.anchor = None;
1477 self.cursor = None;
1478 }
1479 self.stop_anchor_auto_scroll(cx);
1480 self.publish_snapshots(cx);
1481 }
1482
1483 pub fn clear(&mut self, cx: &mut App) {
1485 let handlers = self.clear_state(cx);
1486 dispatch_clear_handlers(handlers, cx);
1487 }
1488
1489 fn clear_state(&mut self, cx: &mut App) -> Vec<ClearHandler> {
1490 self.stop_anchor_auto_scroll(cx);
1491 self.anchor = None;
1492 self.cursor = None;
1493 self.pending_extension_anchor = None;
1494 self.is_selecting = false;
1495 self.did_hit_text = false;
1496 if self.touch.reset() {
1497 self.touch_changed(cx);
1498 }
1499 self.prune_dead_participants();
1500 self.participants
1501 .borrow()
1502 .values()
1503 .filter_map(|registration| registration.participant.upgrade())
1504 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1505 .collect()
1506 }
1507
1508 fn touch_changed(&self, cx: &mut App) {
1521 let participants = self
1522 .participants
1523 .borrow()
1524 .values()
1525 .filter_map(|registration| registration.participant.upgrade())
1526 .filter(|participant| participant.read(cx).snapshot.is_some())
1527 .collect::<Vec<_>>();
1528 let entity_id = self.entity_id;
1529 cx.defer(move |cx| {
1530 for participant in participants {
1531 participant.update(cx, |_, cx| {
1532 cx.emit(TextSelectionEvent::TouchSelectionChanged)
1533 });
1534 }
1535 if let Some(entity_id) = entity_id {
1536 cx.notify(entity_id);
1537 }
1538 });
1539 }
1540
1541 fn copy_items(&self, cx: &App) -> Vec<CopyItem> {
1542 self.participants
1543 .borrow()
1544 .values()
1545 .filter_map(|registration| {
1546 let participant = registration.participant.upgrade()?;
1547 participant
1548 .read(cx)
1549 .copy_item(registration.registration.document_order)
1550 })
1551 .collect()
1552 }
1553
1554 #[cfg(test)]
1555 fn selected_text(&self, cx: &mut App) -> String {
1556 resolve_copy_items(self.copy_items(cx), cx)
1557 }
1558
1559 fn selects_text(&self, cx: &App) -> bool {
1563 self.snapshot().is_some()
1564 && self.participants.borrow().values().any(|registration| {
1565 registration
1566 .participant
1567 .upgrade()
1568 .is_some_and(|participant| {
1569 let participant = participant.read(cx);
1570 project_ranges(participant.snapshot, &participant.runs.borrow())
1571 .ranges()
1572 .iter()
1573 .any(|range| range.as_ref().is_some_and(|range| !range.is_empty()))
1574 })
1575 })
1576 }
1577
1578 pub fn has_selection(&self, cx: &App) -> bool {
1580 self.snapshot().is_some()
1581 || self.participants.borrow().values().any(|registration| {
1582 registration
1583 .participant
1584 .upgrade()
1585 .is_some_and(|participant| participant.read(cx).local_selection)
1586 })
1587 }
1588
1589 pub fn snapshot(&self) -> Option<TextSelectionSnapshot> {
1591 if !self.did_hit_text {
1592 return None;
1593 }
1594 let anchor_endpoint = self.anchor.as_ref()?;
1595 let cursor_endpoint = self.cursor.as_ref()?;
1596 let anchor = anchor_endpoint.resolve(&self.participants.borrow())?;
1597 let cursor = cursor_endpoint.resolve(&self.participants.borrow())?;
1598 (anchor != cursor).then(|| {
1599 TextSelectionSnapshot::new(anchor_endpoint.snapshot(), cursor_endpoint.snapshot())
1600 .with_selecting(self.is_selecting)
1601 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
1602 })
1603 }
1604
1605 #[cfg(test)]
1607 fn is_selecting(&self) -> bool {
1608 self.is_selecting
1609 }
1610
1611 fn prepare_for_mouse_down(&mut self, extend: bool, cx: &mut App) -> Vec<ClearHandler> {
1612 let pending_extension_anchor = extend.then(|| self.anchor.clone()).flatten();
1613 self.stop_anchor_auto_scroll(cx);
1614 self.anchor = None;
1615 self.cursor = None;
1616 self.pending_extension_anchor = None;
1617 self.is_selecting = false;
1618 self.did_hit_text = false;
1619 if self.touch.reset() {
1620 self.touch_changed(cx);
1621 }
1622 self.prune_dead_participants();
1623 let handlers = self
1624 .participants
1625 .borrow()
1626 .values()
1627 .filter_map(|registration| registration.participant.upgrade())
1628 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1629 .collect();
1630 self.pending_extension_anchor = pending_extension_anchor;
1631 handlers
1632 }
1633
1634 fn touch_selection(&self) -> Option<TouchSelectionSnapshot> {
1641 if !self.touch.active {
1642 return None;
1643 }
1644 let mut start: Option<(u64, Bounds<Pixels>, bool)> = None;
1647 let mut end: Option<(u64, Bounds<Pixels>, bool)> = None;
1648 for registration in self.participants.borrow().values() {
1649 let geometry = ®istration.registration;
1650 if geometry.scope != self.active_scope || registration.participant.upgrade().is_none() {
1651 continue;
1652 }
1653 let Some((edge_start, edge_end)) = geometry.selection_edges else {
1654 continue;
1655 };
1656 let order = geometry.document_order;
1657 let viewport = geometry.hitbox.bounds;
1658 if start.is_none_or(|(best, ..)| order < best) {
1659 start = Some((order, edge_start, caret_in_view(edge_start, viewport)));
1660 }
1661 if end.is_none_or(|(best, ..)| order >= best) {
1662 end = Some((order, edge_end, caret_in_view(edge_end, viewport)));
1663 }
1664 }
1665 let edges = match (start, end) {
1666 (Some((_, start, start_visible)), Some((_, end, end_visible))) => {
1667 let edges = (start, end, start_visible, end_visible);
1668 self.touch.last_edges.set(Some(edges));
1669 edges
1670 }
1671 _ if self.touch.drag.is_some() => self.touch.last_edges.get()?,
1672 _ => return None,
1673 };
1674 let (start, end, start_visible, end_visible) = edges;
1675 Some(
1676 TouchSelectionSnapshot::new(start, end)
1677 .with_edge_visible(SelectionEdge::Start, start_visible)
1678 .with_edge_visible(SelectionEdge::End, end_visible)
1679 .with_menu_open(self.touch.menu_open)
1680 .with_dragging(self.touch.drag.map(|drag| drag.edge())),
1681 )
1682 }
1683
1684 fn touch_handles_of(&self, participant: EntityId) -> Vec<(SelectionEdge, Bounds<Pixels>)> {
1689 if !self.touch.active {
1690 return Vec::new();
1691 }
1692 let Some(own) = self.participants.borrow().get(&participant).cloned() else {
1693 return Vec::new();
1694 };
1695 let Some((start, end)) = own.registration.selection_edges else {
1696 return Vec::new();
1697 };
1698 if start == end {
1699 return Vec::new();
1700 }
1701 let order = own.registration.document_order;
1702 let participants = self.participants.borrow();
1703 let others = participants.iter().filter(|(id, registration)| {
1704 **id != participant
1705 && registration.registration.scope == self.active_scope
1706 && registration.registration.selection_edges.is_some()
1707 && registration.participant.upgrade().is_some()
1708 });
1709 let (mut earlier, mut later) = (false, false);
1710 for (_, registration) in others {
1711 let other = registration.registration.document_order;
1712 earlier |= other < order;
1713 later |= other > order;
1714 }
1715 let viewport = own.registration.hitbox.bounds;
1716 let mut handles = Vec::with_capacity(2);
1717 if !earlier && caret_in_view(start, viewport) {
1718 handles.push((SelectionEdge::Start, start));
1719 }
1720 if !later && caret_in_view(end, viewport) {
1721 handles.push((SelectionEdge::End, end));
1722 }
1723 handles
1724 }
1725
1726 fn keep_touch_selection(&mut self, cx: &mut App) {
1728 self.touch.active = self.snapshot().is_some();
1729 self.touch.menu_open = self.touch.active;
1730 self.touch.drag = None;
1731 self.touch_changed(cx);
1732 }
1733
1734 fn register_touch_ui(&mut self, bounds: Bounds<Pixels>) {
1736 self.touch.ui_bounds.push(bounds);
1737 }
1738
1739 fn handles_painted_at(&self, participant: EntityId, bounds: &[Bounds<Pixels>]) -> bool {
1741 self.touch
1742 .handle_bounds
1743 .get(&participant)
1744 .map_or(bounds.is_empty(), |painted| painted == bounds)
1745 }
1746
1747 fn register_handles(&mut self, participant: EntityId, bounds: Vec<Bounds<Pixels>>) {
1750 self.touch.handle_bounds.insert(participant, bounds);
1751 }
1752
1753 fn close_edit_menu(&mut self, cx: &mut App) {
1754 if !self.touch.menu_open {
1755 return;
1756 }
1757 self.touch.menu_open = false;
1758 self.touch_changed(cx);
1759 }
1760
1761 fn edit_menu_on_scroll(&mut self, phase: TouchPhase, cx: &mut App) {
1765 if !self.touch.active || self.touch.drag.is_some() {
1766 return;
1767 }
1768 match phase {
1769 TouchPhase::Ended | TouchPhase::Cancelled => {
1770 if !self.touch.menu_open {
1771 self.touch.menu_open = true;
1772 self.touch_changed(cx);
1773 }
1774 }
1775 _ => self.close_edit_menu(cx),
1776 }
1777 }
1778
1779 fn select_all_touched(&mut self, cx: &mut App) {
1786 if !self.touch.active {
1787 return;
1788 }
1789 let Some((participant, registration)) = self.anchor_registration() else {
1790 return;
1791 };
1792 let (anchor, cursor) = {
1796 let runs = &participant.read(cx).runs.borrow();
1797 let (Some(first), Some(last)) = (
1798 runs.iter().min_by_key(|run| run.document_order),
1799 runs.iter().max_by_key(|run| run.document_order),
1800 ) else {
1801 return;
1802 };
1803 let (Some(start), Some(end)) = (
1804 first.layout.position_for_index(0),
1805 last.layout.position_for_index(last.text.len()),
1806 ) else {
1807 return;
1808 };
1809 let inset = px(1.);
1812 (
1813 point(start.x + inset, start.y + first.layout.line_height() / 2.),
1814 point(end.x - inset, end.y + last.layout.line_height() / 2.),
1815 )
1816 };
1817 let content_key_resolver = participant.read(cx).content_key_resolver.clone();
1818 let to_endpoint = |window_point: Point<Pixels>| {
1819 let content_point =
1820 window_point - registration.bounds.origin - registration.scroll_offset;
1821 SelectionEndpoint {
1822 participant: Some(participant.downgrade()),
1823 point: content_point,
1824 inside: true,
1825 inside_text: true,
1826 content_key: None,
1827 content_key_resolver: content_key_resolver
1828 .clone()
1829 .map(|resolver| (resolver, content_point)),
1830 }
1831 };
1832 self.anchor = Some(to_endpoint(anchor));
1833 self.cursor = Some(to_endpoint(cursor));
1834 self.did_hit_text = true;
1835 self.is_selecting = false;
1836 self.touch.menu_open = true;
1837 self.publish_snapshots(cx);
1838 self.touch_changed(cx);
1839 }
1840
1841 fn begin_edge_drag(
1847 &mut self,
1848 edge: SelectionEdge,
1849 finger: Point<Pixels>,
1850 window: &mut Window,
1851 cx: &mut App,
1852 ) {
1853 let Some(snapshot) = self.touch_selection() else {
1854 return;
1855 };
1856 let handlers = self.prepare_for_mouse_down(false, cx);
1857 dispatch_clear_handlers(handlers, cx);
1858 let held = snapshot.edge(edge.opposite());
1859 let nudge = px(1.);
1862 let anchor_point = match edge {
1863 SelectionEdge::Start => point(held.left() - nudge, held.center().y),
1864 SelectionEdge::End => point(held.left() + nudge, held.center().y),
1865 };
1866 let anchor = self.endpoint(anchor_point, None, cx);
1867 let drag = EdgeDrag::begin(edge, snapshot.edge(edge), finger);
1868 let cursor = self.endpoint(drag.text_position(finger), Some(window), cx);
1869 self.did_hit_text = anchor.inside_text || cursor.inside_text;
1870 self.anchor = Some(anchor);
1871 self.cursor = Some(cursor);
1872 self.is_selecting = true;
1873 self.touch.active = true;
1874 self.touch.menu_open = false;
1875 self.touch.drag = Some(drag);
1876 self.publish_snapshots(cx);
1877 self.touch_changed(cx);
1878 }
1879
1880 fn update_edge_drag(&mut self, finger: Point<Pixels>, window: &Window, cx: &mut Context<Self>) {
1881 let Some(drag) = self.touch.drag else {
1882 return;
1883 };
1884 let before = self.cursor.clone();
1885 self.update_in_window(drag.text_position(finger), window, cx);
1886 if !self.selects_text(cx) {
1889 self.cursor = before;
1890 self.publish_snapshots(cx);
1891 return;
1892 }
1893 if let Some(points) = self
1896 .snapshot()
1897 .and_then(|snapshot| snapshot.window_points())
1898 {
1899 let (anchor, cursor) = (points.anchor(), points.cursor());
1900 let cursor_first = cursor.y < anchor.y || (cursor.y == anchor.y && cursor.x < anchor.x);
1901 let edge = if cursor_first {
1902 SelectionEdge::Start
1903 } else {
1904 SelectionEdge::End
1905 };
1906 if let Some(drag) = self.touch.drag.as_mut() {
1907 drag.set_edge(edge);
1908 }
1909 }
1910 self.touch_changed(cx);
1911 }
1912
1913 fn end_edge_drag(&mut self, cx: &mut App) {
1914 if self.touch.drag.take().is_none() {
1915 return;
1916 }
1917 self.end(cx);
1918 self.keep_touch_selection(cx);
1919 }
1920
1921 fn begin_in_window(
1922 &mut self,
1923 position: Point<Pixels>,
1924 extend: bool,
1925 window: &mut Window,
1926 cx: &mut App,
1927 ) {
1928 self.begin_impl(position, extend, true, Some(window), cx);
1929 }
1930
1931 fn update_in_window(
1932 &mut self,
1933 position: Point<Pixels>,
1934 window: &Window,
1935 cx: &mut Context<Self>,
1936 ) {
1937 if !cx.has_active_drag() {
1938 self.update_impl(position, Some(window), cx);
1939 self.update_auto_scroll(position, window, cx);
1940 }
1941 }
1942
1943 fn select_at(
1944 &mut self,
1945 position: Point<Pixels>,
1946 click_count: usize,
1947 window: &mut Window,
1948 cx: &mut App,
1949 ) {
1950 GlobalState::init(cx);
1951 if GlobalState::is_text_selection_suppressed(cx) {
1952 return;
1953 }
1954 let hit = self.endpoint(position, Some(window), cx);
1955 if !hit.inside_text {
1956 return;
1957 }
1958 let Some(participant) = hit
1959 .participant
1960 .and_then(|participant| participant.upgrade())
1961 else {
1962 return;
1963 };
1964 let points =
1965 points_for_multi_click(&participant.read(cx).runs.borrow(), position, click_count);
1966 let Some((anchor, cursor)) = points else {
1967 return;
1968 };
1969 let Some(registration) = self
1970 .participants
1971 .borrow()
1972 .get(&participant.entity_id())
1973 .cloned()
1974 else {
1975 return;
1976 };
1977 let content_key_resolver = participant.read(cx).content_key_resolver.clone();
1978 let to_endpoint = |point: Point<Pixels>| {
1979 let content_point = point
1980 - registration.registration.bounds.origin
1981 - registration.registration.scroll_offset;
1982 SelectionEndpoint {
1983 participant: Some(participant.downgrade()),
1984 point: content_point,
1985 inside: true,
1986 inside_text: true,
1987 content_key: None,
1988 content_key_resolver: content_key_resolver
1989 .clone()
1990 .map(|resolver| (resolver, content_point)),
1991 }
1992 };
1993 self.anchor = Some(to_endpoint(anchor));
1994 self.cursor = Some(to_endpoint(cursor));
1995 self.did_hit_text = true;
1996 self.is_selecting = false;
1997 participant.update(cx, |state, cx| state.focus(window, cx));
1998 self.publish_snapshots(cx);
1999 }
2000
2001 #[cfg(test)]
2002 fn update_in_window_with_active_drag(
2003 &mut self,
2004 position: Point<Pixels>,
2005 active_drag: bool,
2006 window: &Window,
2007 cx: &mut App,
2008 ) {
2009 if !active_drag {
2010 self.update_impl(position, Some(window), cx);
2011 }
2012 }
2013
2014 fn begin_impl(
2015 &mut self,
2016 position: Point<Pixels>,
2017 extend: bool,
2018 already_prepared: bool,
2019 window: Option<&mut Window>,
2020 cx: &mut App,
2021 ) {
2022 GlobalState::init(cx);
2023 if GlobalState::is_text_selection_suppressed(cx) {
2024 self.pending_extension_anchor = None;
2025 return;
2026 }
2027 let previous_anchor = extend
2028 .then(|| {
2029 self.pending_extension_anchor
2030 .take()
2031 .or_else(|| self.anchor.clone())
2032 })
2033 .flatten()
2034 .filter(|anchor| anchor.resolve(&self.participants.borrow()).is_some());
2035 if !extend && !already_prepared {
2036 self.clear(cx);
2037 }
2038 let endpoint = self.endpoint(position, window.as_deref(), cx);
2039 let focus_participant = endpoint
2040 .inside
2041 .then(|| endpoint.participant.clone())
2042 .flatten();
2043 let anchor = previous_anchor.unwrap_or_else(|| endpoint.clone());
2044 self.anchor = Some(anchor.clone());
2045 self.cursor = Some(endpoint.clone());
2046 self.did_hit_text = anchor.inside_text || endpoint.inside_text;
2047 self.is_selecting = true;
2048 if let Some(participant) = focus_participant.and_then(|participant| participant.upgrade()) {
2049 if let Some(window) = window {
2050 participant.update(cx, |state, cx| state.focus(window, cx));
2051 }
2052 }
2053 self.publish_snapshots(cx);
2054 }
2055
2056 fn update_impl(&mut self, position: Point<Pixels>, window: Option<&Window>, cx: &mut App) {
2057 if !self.is_selecting {
2058 return;
2059 }
2060 let endpoint = self.endpoint(position, window, cx);
2061 self.did_hit_text |= endpoint.inside_text;
2062 self.cursor = Some(endpoint);
2063 if window.is_none() {
2064 self.update_participant_auto_scroll(position, cx);
2065 }
2066 self.publish_snapshots(cx);
2067 }
2068
2069 fn endpoint(
2070 &mut self,
2071 position: Point<Pixels>,
2072 window: Option<&Window>,
2073 cx: &App,
2074 ) -> SelectionEndpoint {
2075 self.prune_dead_participants();
2076 let mut hit: Option<(
2077 WeakEntity<SelectableTextState>,
2078 Rc<TextSelectionRegistration>,
2079 f32,
2080 )> = None;
2081 let mut predecessor: Option<(
2082 WeakEntity<SelectableTextState>,
2083 Rc<TextSelectionRegistration>,
2084 )> = None;
2085 let mut first: Option<(
2086 WeakEntity<SelectableTextState>,
2087 Rc<TextSelectionRegistration>,
2088 )> = None;
2089
2090 for registration in self.participants.borrow().values() {
2091 if registration.registration.scope != self.active_scope
2092 || registration.participant.upgrade().is_none()
2093 {
2094 continue;
2095 }
2096 let participant_geometry = ®istration.registration;
2097 let hovered = window.map_or_else(
2098 || participant_geometry.bounds.contains(&position),
2099 |window| participant_geometry.hitbox.is_hovered(window),
2100 );
2101 if hovered {
2102 let area = f32::from(participant_geometry.bounds.size.width)
2103 * f32::from(participant_geometry.bounds.size.height);
2104 if hit.as_ref().is_none_or(|(_, best, best_area)| {
2105 area < *best_area
2106 || (area == *best_area
2107 && participant_geometry.document_order < best.document_order)
2108 }) {
2109 hit = Some((
2110 registration.participant.clone(),
2111 participant_geometry.clone(),
2112 area,
2113 ));
2114 }
2115 }
2116 if participant_geometry.bounds.top() <= position.y
2117 && predecessor.as_ref().is_none_or(|(_, best)| {
2118 participant_geometry.bounds.top() > best.bounds.top()
2119 || (participant_geometry.bounds.top() == best.bounds.top()
2120 && participant_geometry.document_order < best.document_order)
2121 })
2122 {
2123 predecessor = Some((
2124 registration.participant.clone(),
2125 participant_geometry.clone(),
2126 ));
2127 }
2128 if first.as_ref().is_none_or(|(_, best)| {
2129 participant_geometry.bounds.top() < best.bounds.top()
2130 || (participant_geometry.bounds.top() == best.bounds.top()
2131 && participant_geometry.document_order < best.document_order)
2132 }) {
2133 first = Some((
2134 registration.participant.clone(),
2135 participant_geometry.clone(),
2136 ));
2137 }
2138 }
2139
2140 let selection = hit
2141 .map(|(participant, registration, _)| (participant, registration, true))
2142 .or_else(|| {
2143 predecessor
2144 .or(first)
2145 .map(|(participant, registration)| (participant, registration, false))
2146 });
2147 match selection {
2148 Some((participant, registration, inside)) => {
2149 let point = position - registration.bounds.origin - registration.scroll_offset;
2150 let content_key_resolver = participant.upgrade().and_then(|participant| {
2151 participant
2152 .read(cx)
2153 .content_key_resolver
2154 .clone()
2155 .map(|callback| (callback, point))
2156 });
2157 SelectionEndpoint {
2158 point,
2159 participant: Some(participant),
2160 inside,
2161 inside_text: inside
2162 && registration
2163 .text_bounds
2164 .iter()
2165 .any(|bounds| bounds.contains(&position)),
2166 content_key: None,
2167 content_key_resolver,
2168 }
2169 }
2170 None => SelectionEndpoint {
2171 participant: None,
2172 point: position,
2173 inside: false,
2174 inside_text: false,
2175 content_key: None,
2176 content_key_resolver: None,
2177 },
2178 }
2179 }
2180
2181 fn publish_snapshots(&mut self, cx: &mut App) {
2182 self.prune_dead_participants();
2183 let snapshot = self.snapshot();
2184 let single_participant = self.single_participant();
2185 for (id, registration) in self.participants.borrow().iter() {
2186 let Some(participant) = registration.participant.upgrade() else {
2187 continue;
2188 };
2189 let participant_snapshot = (registration.registration.scope == self.active_scope
2190 && self.participates(*id, registration)
2191 && single_participant.is_none_or(|single| single == *id))
2192 .then_some(snapshot)
2193 .flatten()
2194 .map(|mut snapshot| {
2195 snapshot.coverage = self.coverage_for(*id);
2196 snapshot
2197 });
2198 if participant.read(cx).snapshot == participant_snapshot {
2199 continue;
2200 }
2201 participant.update(cx, |state, cx| state.set_snapshot(participant_snapshot, cx));
2202 }
2203 }
2204
2205 fn coverage_for(&self, id: EntityId) -> TextSelectionCoverage {
2206 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2207 return TextSelectionCoverage::Bounded;
2208 };
2209 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2210 return TextSelectionCoverage::Bounded;
2211 };
2212 if anchor == cursor {
2213 return TextSelectionCoverage::Bounded;
2214 }
2215 let anchor_order = self.participants.borrow()[&anchor]
2216 .registration
2217 .document_order;
2218 let cursor_order = self.participants.borrow()[&cursor]
2219 .registration
2220 .document_order;
2221 if id != anchor && id != cursor {
2222 TextSelectionCoverage::Full
2223 } else if (id == anchor) == (anchor_order < cursor_order) {
2224 TextSelectionCoverage::ToEnd
2225 } else {
2226 TextSelectionCoverage::FromStart
2227 }
2228 }
2229
2230 fn single_participant(&self) -> Option<EntityId> {
2231 let anchor = self.anchor.as_ref()?.entity_id()?;
2232 let cursor = self.cursor.as_ref()?.entity_id()?;
2233 (anchor == cursor).then_some(anchor)
2234 }
2235
2236 fn participates(&self, id: EntityId, registration: &ParticipantRegistration) -> bool {
2237 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2238 return false;
2239 };
2240 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2241 return false;
2242 };
2243 let Some(anchor_registration) = self.participants.borrow().get(&anchor).cloned() else {
2244 return false;
2245 };
2246 let Some(cursor_registration) = self.participants.borrow().get(&cursor).cloned() else {
2247 return false;
2248 };
2249 let start = anchor_registration
2250 .registration
2251 .document_order
2252 .min(cursor_registration.registration.document_order);
2253 let end = anchor_registration
2254 .registration
2255 .document_order
2256 .max(cursor_registration.registration.document_order);
2257 (start..=end).contains(®istration.registration.document_order)
2258 || id == anchor
2259 || id == cursor
2260 }
2261
2262 fn update_auto_scroll(
2263 &mut self,
2264 position: Point<Pixels>,
2265 window: &Window,
2266 cx: &mut Context<Self>,
2267 ) {
2268 if !self.is_selecting {
2271 return;
2272 }
2273 let Some((_, registration)) = self.anchor_registration() else {
2274 return;
2275 };
2276 if registration.self_scroll {
2279 self.auto_scroll.stop();
2280 self.update_participant_auto_scroll(position, cx);
2281 return;
2282 }
2283 let visible_bounds = registration.hitbox.content_mask.bounds;
2288 const HIT_TEST_INSET: Pixels = px(1.);
2290 if visible_bounds.size.width < HIT_TEST_INSET * 2.
2292 || visible_bounds.size.height < HIT_TEST_INSET * 2.
2293 {
2294 self.stop_anchor_auto_scroll(cx);
2295 return;
2296 }
2297 let delta = AutoScroll::compute_delta(position.y, visible_bounds);
2298 let event_position = point(
2299 position.x.clamp(
2300 visible_bounds.left() + HIT_TEST_INSET,
2301 visible_bounds.right() - HIT_TEST_INSET,
2302 ),
2303 position.y.clamp(
2304 visible_bounds.top() + HIT_TEST_INSET,
2305 visible_bounds.bottom() - HIT_TEST_INSET,
2306 ),
2307 );
2308 self.auto_scroll.last_drag_position = Some(event_position);
2309 self.auto_scroll_stall = (None, 0);
2310 let window = window.window_handle();
2311 self.auto_scroll.set(delta, cx, move |delta, state, cx| {
2312 let Some(position) = state.auto_scroll.last_drag_position else {
2313 return;
2314 };
2315 const STALLED_TICKS: u8 = 3;
2319 let geometry = state
2320 .anchor_registration()
2321 .map(|(_, registration)| (registration.bounds, registration.scroll_offset));
2322 let (last, stalled) = &mut state.auto_scroll_stall;
2323 if *last == geometry {
2324 *stalled = stalled.saturating_add(1);
2325 if *stalled >= STALLED_TICKS {
2326 return;
2327 }
2328 } else {
2329 *last = geometry;
2330 *stalled = 0;
2331 }
2332 let window = window;
2333 cx.defer(move |cx| {
2334 _ = window.update(cx, |_, window, cx| {
2335 window.dispatch_event(
2336 ScrollWheelEvent {
2337 position,
2338 delta: ScrollDelta::Pixels(point(px(0.), -delta)),
2339 ..Default::default()
2340 }
2341 .to_platform_input(),
2342 cx,
2343 );
2344 });
2345 });
2346 });
2347 }
2348
2349 fn update_participant_auto_scroll(&self, position: Point<Pixels>, cx: &mut App) {
2352 let Some((participant, registration)) = self.anchor_registration() else {
2353 return;
2354 };
2355 let visible_bounds = registration
2356 .bounds
2357 .intersect(®istration.hitbox.content_mask.bounds);
2358 let delta = if visible_bounds.size.width > px(0.) && visible_bounds.size.height > px(0.) {
2359 AutoScroll::compute_delta(position.y, visible_bounds)
2360 } else {
2361 None
2362 };
2363 participant.update(cx, |state, cx| state.set_auto_scroll(delta, cx));
2364 }
2365
2366 fn stop_anchor_auto_scroll(&mut self, cx: &mut App) {
2367 self.auto_scroll.stop();
2368 let Some(participant) = self.anchor_participant() else {
2369 return;
2370 };
2371 participant.update(cx, |state, cx| state.set_auto_scroll(None, cx));
2372 }
2373
2374 fn anchor_participant(&self) -> Option<Entity<SelectableTextState>> {
2376 self.anchor
2377 .as_ref()
2378 .filter(|anchor| anchor.inside)?
2379 .participant
2380 .as_ref()?
2381 .upgrade()
2382 }
2383
2384 fn anchor_registration(
2386 &self,
2387 ) -> Option<(Entity<SelectableTextState>, Rc<TextSelectionRegistration>)> {
2388 let participant = self.anchor_participant()?;
2389 let registration = self
2390 .participants
2391 .borrow()
2392 .get(&participant.entity_id())?
2393 .clone();
2394 Some((participant, registration.registration.clone()))
2395 }
2396
2397 fn prune_dead_participants(&mut self) {
2398 self.participants
2399 .get_mut()
2400 .retain(|_, registration| registration.participant.upgrade().is_some());
2401 }
2402}
2403
2404#[derive(Default)]
2405struct SelectionStateRegistry(HashMap<gpui::WindowId, WeakEntity<WindowSelectionState>>);
2408
2409impl Global for SelectionStateRegistry {}
2410
2411#[derive(Default)]
2412struct PendingTextSelectionScopes(HashMap<gpui::WindowId, TextSelectionScopeId>);
2413
2414impl Global for PendingTextSelectionScopes {}
2415
2416#[derive(Default)]
2417struct TextSelectionScopeStacks(HashMap<gpui::WindowId, Vec<TextSelectionScopeId>>);
2418
2419impl Global for TextSelectionScopeStacks {}
2420
2421fn push_text_selection_scope(window_id: gpui::WindowId, scope: TextSelectionScopeId, cx: &mut App) {
2422 if !cx.has_global::<TextSelectionScopeStacks>() {
2423 cx.set_global(TextSelectionScopeStacks::default());
2424 }
2425 cx.global_mut::<TextSelectionScopeStacks>()
2426 .0
2427 .entry(window_id)
2428 .or_default()
2429 .push(scope);
2430}
2431
2432fn pop_text_selection_scope(window_id: gpui::WindowId, cx: &mut App) {
2433 let stacks = &mut cx.global_mut::<TextSelectionScopeStacks>().0;
2434 let remove_stack = stacks.get_mut(&window_id).is_some_and(|stack| {
2435 stack.pop();
2436 stack.is_empty()
2437 });
2438 if remove_stack {
2439 stacks.remove(&window_id);
2440 }
2441}
2442
2443fn current_text_selection_scope(
2444 window_id: gpui::WindowId,
2445 cx: &App,
2446) -> Option<TextSelectionScopeId> {
2447 cx.has_global::<TextSelectionScopeStacks>()
2448 .then(|| {
2449 cx.global::<TextSelectionScopeStacks>()
2450 .0
2451 .get(&window_id)
2452 .and_then(|stack| stack.last().copied())
2453 })
2454 .flatten()
2455}
2456
2457fn with_text_selection_scope<T>(
2458 window_id: gpui::WindowId,
2459 scope: TextSelectionScopeId,
2460 cx: &mut App,
2461 callback: impl FnOnce(&mut App) -> T,
2462) -> T {
2463 push_text_selection_scope(window_id, scope, cx);
2464 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(cx)));
2465 pop_text_selection_scope(window_id, cx);
2466 match result {
2467 Ok(result) => result,
2468 Err(payload) => std::panic::resume_unwind(payload),
2469 }
2470}
2471
2472pub struct TextSelection;
2474
2475impl TextSelection {
2476 pub fn is_selectable_at(position: Point<Pixels>, window: &Window, cx: &mut App) -> bool {
2485 live_text_selection_state(window, cx).is_some_and(|state| {
2486 state.update(cx, |state, cx| {
2487 !state.touch.covers(position)
2488 && state.endpoint(position, Some(window), cx).inside_text
2489 })
2490 })
2491 }
2492
2493 pub fn selected_text(window: &mut Window, cx: &mut App) -> String {
2495 let Some(state) = live_text_selection_state(window, cx) else {
2496 return String::new();
2497 };
2498 let items = state.read(cx).copy_items(cx);
2499 resolve_copy_items(items, cx)
2500 }
2501
2502 pub fn has_selection(window: &mut Window, cx: &mut App) -> bool {
2505 live_text_selection_state(window, cx).is_some_and(|state| state.read(cx).has_selection(cx))
2506 }
2507
2508 pub fn clear(window: &mut Window, cx: &mut App) {
2510 if let Some(state) = live_text_selection_state(window, cx) {
2511 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
2512 dispatch_clear_handlers(handlers, cx);
2513 }
2514 }
2515
2516 pub fn clear_for_window(window_id: gpui::WindowId, cx: &mut App) {
2521 clear_window_text_selection(window_id, cx);
2522 }
2523
2524 pub fn end(window: &mut Window, cx: &mut App) {
2526 if let Some(state) = live_text_selection_state(window, cx) {
2527 state.update(cx, |state, cx| state.end(cx));
2528 }
2529 }
2530
2531 pub fn observe_touch_selection(
2535 window: &Window,
2536 cx: &mut App,
2537 callback: impl Fn(&mut App) + 'static,
2538 ) -> Subscription {
2539 let state = WindowSelectionState::acquire(window.window_handle().window_id(), cx);
2540 cx.observe(&state, move |_, cx| callback(cx))
2541 }
2542
2543 pub fn touch_selection(window: &Window, cx: &App) -> Option<TouchSelectionSnapshot> {
2546 WindowSelectionState::existing(window, cx)?
2547 .read(cx)
2548 .touch_selection()
2549 }
2550
2551 pub fn register_touch_ui(bounds: Bounds<Pixels>, window: &Window, cx: &mut App) {
2555 if let Some(state) = WindowSelectionState::existing(window, cx) {
2556 state.update(cx, |state, _| state.register_touch_ui(bounds));
2557 }
2558 }
2559
2560 pub fn close_edit_menu(window: &mut Window, cx: &mut App) {
2562 if let Some(state) = live_text_selection_state(window, cx) {
2563 state.update(cx, |state, cx| state.close_edit_menu(cx));
2564 }
2565 }
2566
2567 pub fn select_all(window: &mut Window, cx: &mut App) {
2570 if let Some(state) = live_text_selection_state(window, cx) {
2571 state.update(cx, |state, cx| state.select_all_touched(cx));
2572 }
2573 }
2574
2575 pub fn begin_edge_drag(
2578 edge: SelectionEdge,
2579 finger: Point<Pixels>,
2580 window: &mut Window,
2581 cx: &mut App,
2582 ) {
2583 if let Some(state) = live_text_selection_state(window, cx) {
2584 state.update(cx, |state, cx| {
2585 state.begin_edge_drag(edge, finger, window, cx)
2586 });
2587 WindowSelectionState::resolve_content_keys(&state, cx);
2588 }
2589 }
2590
2591 pub fn update_edge_drag(finger: Point<Pixels>, window: &mut Window, cx: &mut App) {
2593 if let Some(state) = live_text_selection_state(window, cx) {
2594 state.update(cx, |state, cx| state.update_edge_drag(finger, window, cx));
2595 WindowSelectionState::resolve_content_keys(&state, cx);
2596 }
2597 }
2598
2599 pub fn end_edge_drag(window: &mut Window, cx: &mut App) {
2601 if let Some(state) = live_text_selection_state(window, cx) {
2602 state.update(cx, |state, cx| state.end_edge_drag(cx));
2603 }
2604 }
2605
2606 pub fn activate_scope(scope: TextSelectionScopeId, window: &mut Window, cx: &mut App) {
2608 let Some(state) = WindowSelectionState::existing(window, cx) else {
2609 if !cx.has_global::<PendingTextSelectionScopes>() {
2610 cx.set_global(PendingTextSelectionScopes::default());
2611 }
2612 cx.global_mut::<PendingTextSelectionScopes>()
2613 .0
2614 .insert(window.window_handle().window_id(), scope);
2615 return;
2616 };
2617 if state.read(cx).active_scope == scope {
2618 return;
2619 }
2620 let handlers = state.update(cx, |state, cx| state.set_active_scope_state(scope, cx));
2621 dispatch_clear_handlers(handlers, cx);
2622 }
2623}
2624
2625pub struct TextSelectionLayer;
2630
2631pub(crate) fn text_selection_scope(
2632 scope: TextSelectionScopeId,
2633 element: impl IntoElement,
2634) -> impl IntoElement {
2635 TextSelectionScopeMarker {
2636 scope,
2637 element: element.into_element(),
2638 }
2639}
2640
2641struct TextSelectionScopeMarker<E> {
2642 scope: TextSelectionScopeId,
2643 element: E,
2644}
2645
2646impl<E: Element> IntoElement for TextSelectionScopeMarker<E> {
2647 type Element = Self;
2648
2649 fn into_element(self) -> Self::Element {
2650 self
2651 }
2652}
2653
2654impl<E: Element> Element for TextSelectionScopeMarker<E> {
2655 type RequestLayoutState = E::RequestLayoutState;
2656 type PrepaintState = E::PrepaintState;
2657
2658 fn id(&self) -> Option<ElementId> {
2659 self.element.id()
2660 }
2661
2662 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
2663 self.element.source_location()
2664 }
2665
2666 fn request_layout(
2667 &mut self,
2668 id: Option<&GlobalElementId>,
2669 inspector_id: Option<&InspectorElementId>,
2670 window: &mut Window,
2671 cx: &mut App,
2672 ) -> (LayoutId, Self::RequestLayoutState) {
2673 let window_id = window.window_handle().window_id();
2674 with_text_selection_scope(window_id, self.scope, cx, |cx| {
2675 self.element.request_layout(id, inspector_id, window, cx)
2676 })
2677 }
2678
2679 fn prepaint(
2680 &mut self,
2681 id: Option<&GlobalElementId>,
2682 inspector_id: Option<&InspectorElementId>,
2683 bounds: Bounds<Pixels>,
2684 request_layout: &mut Self::RequestLayoutState,
2685 window: &mut Window,
2686 cx: &mut App,
2687 ) -> Self::PrepaintState {
2688 let window_id = window.window_handle().window_id();
2689 with_text_selection_scope(window_id, self.scope, cx, |cx| {
2690 self.element
2691 .prepaint(id, inspector_id, bounds, request_layout, window, cx)
2692 })
2693 }
2694
2695 fn paint(
2696 &mut self,
2697 id: Option<&GlobalElementId>,
2698 inspector_id: Option<&InspectorElementId>,
2699 bounds: Bounds<Pixels>,
2700 request_layout: &mut Self::RequestLayoutState,
2701 prepaint: &mut Self::PrepaintState,
2702 window: &mut Window,
2703 cx: &mut App,
2704 ) {
2705 let window_id = window.window_handle().window_id();
2706 with_text_selection_scope(window_id, self.scope, cx, |cx| {
2707 self.element.paint(
2708 id,
2709 inspector_id,
2710 bounds,
2711 request_layout,
2712 prepaint,
2713 window,
2714 cx,
2715 );
2716 });
2717 }
2718}
2719
2720#[doc(hidden)]
2721pub struct TextSelectionLayerPrepaintState(Entity<WindowSelectionState>);
2722
2723impl IntoElement for TextSelectionLayer {
2724 type Element = Self;
2725
2726 fn into_element(self) -> Self::Element {
2727 self
2728 }
2729}
2730
2731impl Element for TextSelectionLayer {
2732 type RequestLayoutState = ();
2733 type PrepaintState = TextSelectionLayerPrepaintState;
2734
2735 fn id(&self) -> Option<ElementId> {
2736 Some("window-text-selection".into())
2737 }
2738
2739 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
2740 None
2741 }
2742
2743 fn request_layout(
2744 &mut self,
2745 _: Option<&GlobalElementId>,
2746 _: Option<&InspectorElementId>,
2747 window: &mut Window,
2748 cx: &mut App,
2749 ) -> (LayoutId, Self::RequestLayoutState) {
2750 (window.request_layout(Style::default(), [], cx), ())
2751 }
2752
2753 fn prepaint(
2754 &mut self,
2755 global_id: Option<&GlobalElementId>,
2756 _: Option<&InspectorElementId>,
2757 _: Bounds<Pixels>,
2758 _: &mut Self::RequestLayoutState,
2759 window: &mut Window,
2760 cx: &mut App,
2761 ) -> Self::PrepaintState {
2762 GlobalState::init(cx);
2768 GlobalState::global(cx).begin_selection_frame();
2769 let state = retain_text_selection_state(global_id, window, cx);
2770 if state.read(cx).touch.painted_any() {
2772 state.update(cx, |state, _| state.touch.begin_frame());
2773 }
2774 TextSelectionLayerPrepaintState(state)
2775 }
2776
2777 fn paint(
2778 &mut self,
2779 _: Option<&GlobalElementId>,
2780 _: Option<&InspectorElementId>,
2781 _: Bounds<Pixels>,
2782 _: &mut Self::RequestLayoutState,
2783 state: &mut Self::PrepaintState,
2784 window: &mut Window,
2785 cx: &mut App,
2786 ) {
2787 paint_text_selection(&state.0, window, cx);
2788 }
2789}
2790
2791fn retain_text_selection_state(
2792 global_id: Option<&GlobalElementId>,
2793 window: &mut Window,
2794 cx: &mut App,
2795) -> Entity<WindowSelectionState> {
2796 let window_id = window.window_handle().window_id();
2797 let state = window.with_element_state::<Entity<WindowSelectionState>, _>(
2798 global_id.expect("TextSelection has a stable element id"),
2799 |retained, _| {
2800 let state = retained.unwrap_or_else(|| WindowSelectionState::acquire(window_id, cx));
2801 (state.clone(), state)
2802 },
2803 );
2804 let registered = cx.has_global::<SelectionStateRegistry>()
2807 && cx
2808 .global::<SelectionStateRegistry>()
2809 .0
2810 .get(&window_id)
2811 .is_some_and(|registered| registered.entity_id() == state.entity_id());
2812 if !registered {
2813 if !cx.has_global::<SelectionStateRegistry>() {
2814 cx.set_global(SelectionStateRegistry::default());
2815 }
2816 cx.global_mut::<SelectionStateRegistry>()
2817 .0
2818 .insert(window_id, state.downgrade());
2819 }
2820 state
2821}
2822
2823fn paint_text_selection(state: &Entity<WindowSelectionState>, window: &mut Window, cx: &mut App) {
2824 if state.read(cx).schedule_finish_frame() {
2825 let state = state.downgrade();
2826 window.defer(cx, move |window, cx| {
2827 let Some(state) = state.upgrade() else {
2828 return;
2829 };
2830 if state.read(cx).finish_frame_quietly(cx) {
2831 return;
2832 }
2833 let handlers = state.update(cx, |state, cx| state.finish_frame(cx));
2834 dispatch_clear_handlers(handlers, cx);
2835 let refresh_cursor = state.update(cx, |state, cx| {
2838 if std::mem::take(&mut state.refresh_held_cursor) && state.is_selecting {
2839 let position = window.mouse_position();
2842 let position = state
2843 .touch
2844 .drag
2845 .map_or(position, |drag| drag.text_position(position));
2846 state.update_in_window(position, window, cx);
2847 true
2848 } else {
2849 false
2850 }
2851 });
2852 if refresh_cursor {
2853 WindowSelectionState::resolve_content_keys(&state, cx);
2854 }
2855 });
2856 }
2857
2858 let mouse_down_state = state.downgrade();
2859 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
2860 if event.button != MouseButton::Left {
2861 return;
2862 }
2863 let Some(state) = mouse_down_state.upgrade() else {
2864 return;
2865 };
2866 if state.read(cx).touch.covers(event.position) {
2869 return;
2870 }
2871 if phase.capture() {
2872 GlobalState::init(cx);
2873 GlobalState::reset_text_selection_suppression(cx);
2874 let handlers = state.update(cx, |state, cx| {
2875 if state.mouse_down_prepared {
2876 return Vec::new();
2877 }
2878 state.mouse_down_prepared = true;
2879 state.prepare_for_mouse_down(event.click_count == 1 && event.modifiers.shift, cx)
2880 });
2881 dispatch_clear_handlers(handlers, cx);
2882 } else if event.click_count == 1 {
2883 if GlobalState::is_text_selection_suppressed(cx) {
2884 state.update(cx, |state, _| state.pending_extension_anchor = None);
2885 return;
2886 }
2887 state.update(cx, |state, cx| {
2888 if !state.is_selecting {
2889 state.begin_in_window(event.position, event.modifiers.shift, window, cx)
2890 }
2891 });
2892 WindowSelectionState::resolve_content_keys(&state, cx);
2893 } else if event.click_count >= 2 {
2894 if GlobalState::is_text_selection_suppressed(cx) {
2895 return;
2896 }
2897 let touch = GlobalState::is_touch_press(cx);
2898 state.update(cx, |state, cx| {
2899 state.select_at(event.position, event.click_count, window, cx);
2900 if touch {
2903 state.keep_touch_selection(cx);
2904 }
2905 });
2906 WindowSelectionState::resolve_content_keys(&state, cx);
2907 }
2908 });
2909
2910 window.on_mouse_event(move |event: &TouchDragEvent, phase, _, cx| {
2913 if phase.capture() && event.phase == TouchPhase::Started {
2914 GlobalState::note_touch(cx);
2915 }
2916 });
2917
2918 let long_press_state = state.downgrade();
2921 window.on_mouse_event(move |event: &LongPressEvent, phase, window, cx| {
2922 if !phase.bubble() {
2923 return;
2924 }
2925 let Some(state) = long_press_state.upgrade() else {
2926 return;
2927 };
2928 if event.phase == TouchPhase::Started {
2929 if window.default_prevented()
2930 || state.read(cx).touch.covers(event.start_position)
2931 || !state.update(cx, |state, cx| {
2932 state
2933 .endpoint(event.start_position, Some(window), cx)
2934 .inside_text
2935 })
2936 {
2937 return;
2938 }
2939 GlobalState::init(cx);
2940 GlobalState::reset_text_selection_suppression(cx);
2941 let handlers = state.update(cx, |state, cx| state.prepare_for_mouse_down(false, cx));
2942 dispatch_clear_handlers(handlers, cx);
2943 let selected = state.update(cx, |state, cx| {
2944 state.select_at(event.start_position, 2, window, cx);
2945 state.anchor.is_some()
2946 });
2947 if !selected {
2948 return;
2949 }
2950 window.capture_long_press(&state);
2951 } else if !window.has_long_press_capture(&state) {
2952 return;
2953 } else {
2954 state.update(cx, |state, cx| match event.phase {
2955 TouchPhase::Moved => {
2956 state.is_selecting = true;
2957 state.update_in_window(event.position, window, cx);
2958 }
2959 TouchPhase::Ended | TouchPhase::Cancelled => {
2960 state.end(cx);
2961 state.keep_touch_selection(cx);
2964 }
2965 _ => {}
2966 });
2967 }
2968 window.prevent_default();
2969 cx.stop_propagation();
2970 WindowSelectionState::resolve_content_keys(&state, cx);
2971 });
2972
2973 let drag_state = state.downgrade();
2976 window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
2977 if !phase.bubble() || event.phase == TouchPhase::Started {
2978 return;
2979 }
2980 let Some(state) = drag_state.upgrade() else {
2981 return;
2982 };
2983 if state.read(cx).touch.drag.is_none() {
2984 return;
2985 }
2986 cx.stop_propagation();
2987 state.update(cx, |state, cx| match event.phase {
2988 TouchPhase::Moved => state.update_edge_drag(event.position, window, cx),
2989 _ => state.end_edge_drag(cx),
2990 });
2991 WindowSelectionState::resolve_content_keys(&state, cx);
2992 });
2993 let drag_state = state.downgrade();
2994 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
2995 if phase.bubble()
2996 && event.pressed_button == Some(MouseButton::Left)
2997 && let Some(state) = drag_state.upgrade()
2998 && state.read(cx).touch.drag.is_some()
2999 {
3000 state.update(cx, |state, cx| {
3001 state.update_edge_drag(event.position, window, cx)
3002 });
3003 WindowSelectionState::resolve_content_keys(&state, cx);
3004 }
3005 });
3006 let drag_state = state.downgrade();
3007 window.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| {
3008 if phase.bubble()
3009 && event.button == MouseButton::Left
3010 && let Some(state) = drag_state.upgrade()
3011 {
3012 state.update(cx, |state, cx| state.end_edge_drag(cx));
3013 }
3014 });
3015
3016 let mouse_move_state = state.downgrade();
3017 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
3018 if phase.bubble()
3019 && let Some(state) = mouse_move_state.upgrade()
3020 && state.read(cx).is_selecting
3022 {
3023 state.update(cx, |state, cx| {
3024 if state.touch.drag.is_some() {
3027 return;
3028 }
3029 state.update_in_window(event.position, window, cx)
3030 });
3031 WindowSelectionState::resolve_content_keys(&state, cx);
3032 }
3033 });
3034
3035 let mouse_up_state = state.downgrade();
3036 window.on_mouse_event(move |_: &MouseUpEvent, phase, _, cx| {
3037 if phase.bubble()
3038 && let Some(state) = mouse_up_state.upgrade()
3039 {
3040 state.update(cx, |state, cx| {
3041 state.mouse_down_prepared = false;
3042 state.end(cx)
3043 });
3044 }
3045 });
3046
3047 let scroll_state = state.downgrade();
3048 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
3049 let Some(state) = scroll_state.upgrade() else {
3050 return;
3051 };
3052 if phase.capture() {
3056 if state.read(cx).touch.active {
3057 state.update(cx, |state, cx| {
3058 state.edit_menu_on_scroll(event.touch_phase, cx)
3059 });
3060 }
3061 return;
3062 }
3063 if phase.bubble() && state.read(cx).is_selecting {
3065 let position = window.mouse_position();
3066 state.update(cx, |state, cx| {
3067 let position = state
3069 .touch
3070 .drag
3071 .map_or(position, |drag| drag.text_position(position));
3072 state.update_in_window(position, window, cx)
3073 });
3074 WindowSelectionState::resolve_content_keys(&state, cx);
3075 }
3076 });
3077}
3078
3079fn live_text_selection_state(
3080 window: &Window,
3081 cx: &mut App,
3082) -> Option<Entity<WindowSelectionState>> {
3083 WindowSelectionState::existing(window, cx)
3084}
3085
3086pub(crate) fn clear_window_text_selection(window_id: gpui::WindowId, cx: &mut App) {
3087 if !cx.has_global::<SelectionStateRegistry>() {
3088 return;
3089 }
3090 let Some(state) = cx
3091 .global::<SelectionStateRegistry>()
3092 .0
3093 .get(&window_id)
3094 .and_then(WeakEntity::upgrade)
3095 else {
3096 return;
3097 };
3098 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
3099 dispatch_clear_handlers(handlers, cx);
3100}
3101
3102#[cfg(test)]
3103mod tests {
3104 use super::*;
3105 use crate::ElementExt as _;
3106 use gpui::{
3107 Bounds, ContentMask, Context, Hitbox, HitboxBehavior, HitboxId, InteractiveElement as _,
3108 IntoElement, ParentElement as _, Render, SharedString, Styled as _, StyledText,
3109 TestAppContext, TextLayout, Window, div, point, prelude::FluentBuilder as _, px, size,
3110 };
3111 use std::{
3112 cell::{Cell, RefCell},
3113 rc::Rc,
3114 };
3115
3116 struct FakeParticipant {
3117 selection: TextSelectionHandle,
3118 }
3119
3120 struct WindowSelectionView {
3121 selection: TextSelectionHandle,
3122 }
3123
3124 struct SelectionElementOnlyView;
3125 struct ToggleSelectionElementView {
3126 enabled: bool,
3127 selection: TextSelectionHandle,
3128 }
3129
3130 struct DoubleSelectionElementView {
3131 selection: TextSelectionHandle,
3132 }
3133
3134 struct WindowOwnedSelectionView {
3135 selection: TextSelectionHandle,
3136 }
3137
3138 struct FirstFrameScopedSelectionView {
3139 selection: TextSelectionHandle,
3140 }
3141
3142 struct PlainRunLayoutView {
3143 texts: Vec<SharedString>,
3144 layouts: Vec<TextLayout>,
3145 }
3146
3147 impl Render for WindowSelectionView {
3148 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3149 div()
3150 }
3151 }
3152
3153 impl Render for SelectionElementOnlyView {
3154 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3155 div()
3156 .size_full()
3157 .child(TextSelectionLayer)
3158 .child(
3159 div()
3160 .size_full()
3161 .on_mouse_down(MouseButton::Left, |_, _, cx| {
3162 GlobalState::suppress_text_selection(cx);
3163 }),
3164 )
3165 }
3166 }
3167
3168 impl Render for ToggleSelectionElementView {
3169 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3170 let selection = self.selection.clone();
3171 div().when(self.enabled, |this| {
3172 this.child(TextSelectionLayer)
3173 .child(div().size_full().on_prepaint(move |bounds, window, cx| {
3174 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3175 selection.register(
3176 TextSelectionRegistration::new(hitbox, bounds)
3177 .with_text_bounds(vec![bounds]),
3178 window,
3179 cx,
3180 );
3181 }))
3182 })
3183 }
3184 }
3185
3186 impl Render for DoubleSelectionElementView {
3187 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3188 let selection = self.selection.clone();
3189 div()
3190 .size_full()
3191 .child(TextSelectionLayer)
3192 .child(TextSelectionLayer)
3193 .on_prepaint(move |bounds, window, cx| {
3194 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3195 selection.register(
3196 TextSelectionRegistration::new(hitbox, bounds)
3197 .with_text_bounds(vec![bounds]),
3198 window,
3199 cx,
3200 );
3201 })
3202 }
3203 }
3204
3205 impl Render for WindowOwnedSelectionView {
3206 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3207 let selection = self.selection.clone();
3208 div()
3209 .size_full()
3210 .child(TextSelectionLayer)
3211 .child(div().size_full().on_prepaint(move |bounds, window, cx| {
3212 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3213 selection.register(
3214 TextSelectionRegistration::new(hitbox, bounds)
3215 .with_text_bounds(vec![bounds]),
3216 window,
3217 cx,
3218 );
3219 }))
3220 }
3221 }
3222
3223 impl Render for FirstFrameScopedSelectionView {
3224 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3225 let scope = TextSelectionScopeId::from_raw(23);
3226 TextSelection::activate_scope(scope, window, cx);
3227 let selection = self.selection.clone();
3228
3229 div().child(TextSelectionLayer).child(
3230 div()
3231 .size_full()
3232 .on_prepaint(move |bounds, window, cx| {
3233 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3234 selection.register(
3235 TextSelectionRegistration::new(hitbox, bounds)
3236 .with_text_bounds(vec![bounds]),
3237 window,
3238 cx,
3239 );
3240 })
3241 .text_selection_scope(scope),
3242 )
3243 }
3244 }
3245
3246 impl Render for PlainRunLayoutView {
3247 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3248 self.layouts.clear();
3249 let children = self
3250 .texts
3251 .iter()
3252 .enumerate()
3253 .map(|(index, text)| {
3254 let text = StyledText::new(text.clone());
3255 self.layouts.push(text.layout().clone());
3256 div().absolute().top(px(index as f32 * 40.)).child(text)
3257 })
3258 .collect::<Vec<_>>();
3259 div().size_full().children(children)
3260 }
3261 }
3262
3263 impl FakeParticipant {
3264 fn new(text: &str, cx: &mut gpui::App) -> Self {
3265 let selection = TextSelectionHandle::new(text, cx);
3266 Self { selection }
3267 }
3268
3269 fn register(
3270 &self,
3271 selection_state: &mut WindowSelectionState,
3272 y: f32,
3273 scope: TextSelectionScopeId,
3274 document_order: u64,
3275 cx: &mut gpui::App,
3276 ) {
3277 let bounds = Bounds::new(point(px(0.), px(y)), size(px(100.), px(10.)));
3278 selection_state.register_participant(
3279 self.selection.clone(),
3280 TextSelectionRegistration::new(
3281 Hitbox {
3282 id: HitboxId::placeholder(),
3283 bounds,
3284 content_mask: ContentMask { bounds },
3285 behavior: HitboxBehavior::Normal,
3286 },
3287 bounds,
3288 )
3289 .with_scope(scope)
3290 .with_document_order(document_order)
3291 .with_text_bounds(vec![bounds]),
3292 cx,
3293 );
3294 }
3295 }
3296
3297 fn laid_out_runs(texts: &[&str], cx: &mut TestAppContext) -> Vec<(SharedString, TextLayout)> {
3298 let texts = texts
3299 .iter()
3300 .map(|text| SharedString::from(*text))
3301 .collect::<Vec<_>>();
3302 let view = cx.add_window({
3303 let texts = texts.clone();
3304 move |_, _| PlainRunLayoutView {
3305 texts,
3306 layouts: Vec::new(),
3307 }
3308 });
3309 cx.update_window(*view, |_, window, cx| {
3310 let _ = window.draw(cx);
3311 })
3312 .unwrap();
3313 let layouts = cx.update(|cx| view.read(cx).unwrap().layouts.clone());
3314 texts.into_iter().zip(layouts).collect()
3315 }
3316
3317 fn plain_snapshot(anchor: Point<Pixels>, cursor: Point<Pixels>) -> TextSelectionSnapshot {
3318 TextSelectionSnapshot::new(
3319 TextSelectionEndpoint::new(None, anchor),
3320 TextSelectionEndpoint::new(None, cursor),
3321 )
3322 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
3323 }
3324
3325 #[gpui::test]
3326 fn scope_stack_is_cleaned_after_panicking_subtree(cx: &mut TestAppContext) {
3327 let window_id = {
3328 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3329 window_cx.update(|window, _| window.window_handle().window_id())
3330 };
3331 let scope = TextSelectionScopeId::from_raw(41);
3332
3333 cx.update(|cx| {
3334 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3335 with_text_selection_scope(window_id, scope, cx, |_| panic!("subtree failed"));
3336 }));
3337
3338 assert!(result.is_err());
3339 assert_eq!(current_text_selection_scope(window_id, cx), None);
3340 });
3341 }
3342
3343 #[gpui::test]
3344 fn reentrant_scope_from_one_window_does_not_pollute_another(cx: &mut TestAppContext) {
3345 let first_window_id = {
3346 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3347 window_cx.update(|window, _| window.window_handle().window_id())
3348 };
3349 let second_window_id = {
3350 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3351 window_cx.update(|window, _| window.window_handle().window_id())
3352 };
3353 let scope = TextSelectionScopeId::from_raw(42);
3354
3355 cx.update(|cx| {
3356 with_text_selection_scope(first_window_id, scope, cx, |cx| {
3357 assert_eq!(current_text_selection_scope(second_window_id, cx), None);
3358 assert_eq!(
3359 current_text_selection_scope(first_window_id, cx),
3360 Some(scope)
3361 );
3362 });
3363 });
3364 }
3365
3366 #[gpui::test]
3367 fn selection_callback_can_reenter_its_selection_state(cx: &mut TestAppContext) {
3368 let called = Rc::new(Cell::new(false));
3369 let called_from_callback = called.clone();
3370 let (selection_state, participant) = cx.update(|cx| {
3371 let selection_state = cx.new(|_| WindowSelectionState::default());
3372 let selection_state_for_callback = selection_state.clone();
3373 let participant = FakeParticipant::new("participant", cx);
3374 participant
3375 .selection
3376 .subscribe(
3377 move |event, cx| {
3378 if matches!(event, TextSelectionEvent::SelectionChanged(Some(_))) {
3379 selection_state_for_callback
3380 .update(cx, |_, _| called_from_callback.set(true));
3381 }
3382 },
3383 cx,
3384 )
3385 .detach();
3386 (selection_state, participant)
3387 });
3388 cx.run_until_parked();
3389 cx.update(|cx| {
3390 selection_state.update(cx, |selection_state, cx| {
3391 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx);
3392 selection_state.begin(point(px(1.), px(1.)), false, cx);
3393 selection_state.update(point(px(20.), px(1.)), cx);
3394 });
3395 });
3396 cx.run_until_parked();
3397 assert!(called.get());
3398 }
3399
3400 #[gpui::test]
3401 fn selection_events_preserve_snapshot_then_clear_order(cx: &mut TestAppContext) {
3402 let observed = Rc::new(RefCell::new(Vec::new()));
3403 let observed_for_callback = observed.clone();
3404 let selection = cx.update(|cx| {
3405 let selection = TextSelectionHandle::new("selection", cx);
3406 selection
3407 .subscribe(
3408 move |event, _| {
3409 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
3410 observed_for_callback.borrow_mut().push(snapshot.is_some());
3411 }
3412 },
3413 cx,
3414 )
3415 .detach();
3416 selection
3417 });
3418 cx.run_until_parked();
3419 cx.update(|cx| {
3420 selection.0.update(cx, |state, cx| {
3421 state.set_snapshot(
3422 Some(plain_snapshot(point(px(1.), px(1.)), point(px(8.), px(1.)))),
3423 cx,
3424 );
3425 state.clear_state(cx);
3426 });
3427 });
3428 cx.run_until_parked();
3429 assert_eq!(&*observed.borrow(), &[true, false]);
3430 }
3431
3432 fn text_run(order: u64, text: SharedString, layout: TextLayout) -> TextSelectionRun {
3433 let bounds = layout.bounds();
3434 TextSelectionRun::new(text, layout, bounds).with_document_order(order)
3435 }
3436
3437 #[gpui::test]
3438 fn public_selection_data_uses_builders_and_readers(cx: &mut TestAppContext) {
3439 let bounds = Bounds::new(point(px(1.), px(2.)), size(px(30.), px(10.)));
3440 let hitbox = Hitbox {
3441 id: HitboxId::placeholder(),
3442 bounds,
3443 content_mask: ContentMask { bounds },
3444 behavior: HitboxBehavior::Normal,
3445 };
3446 let scope = TextSelectionScopeId::from_raw(7);
3447 let endpoint = TextSelectionEndpoint::new(None, bounds.origin)
3448 .with_content_key(TextSelectionContentKey::new(11));
3449 let snapshot = TextSelectionSnapshot::new(endpoint, endpoint)
3450 .with_selecting(true)
3451 .with_window_points(Some(TextSelectionWindowPoints {
3452 anchor: bounds.origin,
3453 cursor: bounds.bottom_right(),
3454 }))
3455 .with_coverage(TextSelectionCoverage::Full);
3456 let registration = TextSelectionRegistration::new(hitbox, bounds)
3457 .with_scroll_offset(point(px(3.), px(4.)))
3458 .with_scope(scope)
3459 .with_document_order(9)
3460 .with_text_bounds(vec![bounds]);
3461
3462 assert_eq!(endpoint.entity_id(), None);
3463 assert_eq!(endpoint.content_point(), bounds.origin);
3464 assert_eq!(
3465 endpoint.content_key(),
3466 Some(TextSelectionContentKey::new(11))
3467 );
3468 assert_eq!(snapshot.anchor(), endpoint);
3469 assert_eq!(snapshot.cursor(), endpoint);
3470 assert!(snapshot.is_selecting());
3471 assert_eq!(snapshot.coverage(), TextSelectionCoverage::Full);
3472 assert_eq!(
3473 snapshot.window_points(),
3474 Some(TextSelectionWindowPoints {
3475 anchor: bounds.origin,
3476 cursor: bounds.bottom_right(),
3477 })
3478 );
3479 assert_eq!(registration.bounds(), bounds);
3480 assert_eq!(registration.scroll_offset(), point(px(3.), px(4.)));
3481 assert_eq!(registration.scope(), scope);
3482 assert_eq!(registration.document_order(), 9);
3483 assert_eq!(registration.text_bounds(), &[bounds]);
3484
3485 let (text, layout) = laid_out_runs(&["aé"], cx).pop().unwrap();
3486 let text_run = TextSelectionRun::new(text.clone(), layout.clone(), layout.bounds())
3487 .with_document_order(3);
3488 assert_eq!(text_run.document_order(), 3);
3489 assert_eq!(text_run.text(), &text);
3490 assert_eq!(text_run.layout().len(), layout.len());
3491 assert_eq!(text_run.bounds(), layout.bounds());
3492
3493 let projection = TextSelectionProjection {
3494 ranges: vec![Some(1..3)],
3495 is_active: true,
3496 };
3497 assert_eq!(projection.ranges(), &[Some(1..3)]);
3498 assert!(projection.is_active());
3499 }
3500
3501 #[gpui::test]
3502 fn selection_handle_is_the_public_adapter_seam(cx: &mut TestAppContext) {
3503 let selected = Rc::new(Cell::new(false));
3504 let selected_from_callback = selected.clone();
3505 cx.update(|cx| {
3506 let selection = TextSelectionHandle::new("initial", cx);
3507 let entity_id = selection.entity_id();
3508 selection.set_fallback_copy_text("updated", cx);
3509 selection.set_local_selection(true, cx);
3510 selection
3511 .subscribe(
3512 move |event, _| {
3513 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
3514 selected_from_callback.set(snapshot.is_some());
3515 }
3516 },
3517 cx,
3518 )
3519 .detach();
3520 selection.focus_with(|_, _| {}, cx);
3521 selection.copy_with(|_| "copied".to_string(), cx);
3522 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(3)), cx);
3523
3524 assert_eq!(selection.entity_id(), entity_id);
3525 assert_eq!(selection.snapshot(cx), None);
3526 assert_eq!(
3527 selection.update_runs(&[], cx),
3528 TextSelectionProjection::default()
3529 );
3530 });
3531 assert!(!selected.get());
3532 }
3533
3534 #[gpui::test]
3535 fn selection_handle_can_subscribe_its_window_to_refresh(cx: &mut TestAppContext) {
3536 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3537 selection: TextSelectionHandle::new("refresh", cx),
3538 });
3539 cx.update(|window, cx| {
3540 let selection = TextSelectionHandle::new("refresh", cx);
3541 selection.refresh_window_on_change(window, cx).detach();
3542 });
3543 }
3544
3545 #[gpui::test]
3546 fn plain_projection_preserves_forward_reversed_and_unicode_ranges(cx: &mut TestAppContext) {
3547 let (text, layout) = laid_out_runs(&["aé🙂z"], cx).pop().unwrap();
3548 let run = text_run(0, text, layout.clone());
3549 let start = layout.position_for_index(1).unwrap();
3550 let end = layout.position_for_index(7).unwrap();
3551
3552 let forward = project_ranges(Some(plain_snapshot(start, end)), std::slice::from_ref(&run));
3553 let reversed = project_ranges(Some(plain_snapshot(end, start)), &[run]);
3554
3555 assert_eq!(forward.ranges(), &[Some(1..7)]);
3556 assert_eq!(reversed.ranges(), &[Some(1..7)]);
3557 assert!(forward.is_active());
3558 assert!(reversed.is_active());
3559 }
3560
3561 #[gpui::test]
3562 fn double_click_expands_a_plain_run_to_the_input_word_boundary(cx: &mut TestAppContext) {
3563 let (text, layout) = laid_out_runs(&["one café, three"], cx).pop().unwrap();
3564 let run = text_run(0, text, layout.clone());
3565 let click = layout.position_for_index(6).unwrap();
3566
3567 let (anchor, cursor) =
3568 points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
3569 let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
3570
3571 assert_eq!(states.ranges(), &[Some(4..9)]);
3572 }
3573
3574 #[gpui::test]
3575 fn multi_click_uses_text_layout_window_coordinates_at_a_nonzero_origin(
3576 cx: &mut TestAppContext,
3577 ) {
3578 let mut runs = laid_out_runs(&["above", "alpha beta"], cx);
3579 let (text, layout) = runs.pop().unwrap();
3580 assert!(layout.bounds().origin.y > px(0.));
3581 let run = text_run(0, text, layout.clone());
3582 let click = layout.position_for_index(7).unwrap();
3583
3584 let (anchor, cursor) =
3585 points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
3586 let projection = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
3587
3588 assert_eq!(projection.ranges(), &[Some(6..10)]);
3589 }
3590
3591 #[gpui::test]
3592 fn triple_click_expands_to_the_input_logical_line_not_the_visual_row(cx: &mut TestAppContext) {
3593 let (text, layout) = laid_out_runs(&["second line"], cx).pop().unwrap();
3594 let run = text_run(0, text, layout.clone());
3595 let click = layout.position_for_index(4).unwrap();
3596
3597 let (anchor, cursor) =
3598 points_for_multi_click(std::slice::from_ref(&run), click, 4).unwrap();
3599 let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
3600
3601 assert_eq!(states.ranges(), &[Some(0..11)]);
3602 assert_eq!(line_range_at("first line\nsecond line\nthird", 15), 11..22);
3603 }
3604
3605 #[gpui::test]
3606 fn plain_projection_spans_multiple_runs_and_leaves_empty_gutters_unselected(
3607 cx: &mut TestAppContext,
3608 ) {
3609 let mut runs = laid_out_runs(&["first", "", "second"], cx);
3610 let (first_text, first_layout) = runs.remove(0);
3611 let (gutter_text, gutter_layout) = runs.remove(0);
3612 let (second_text, second_layout) = runs.remove(0);
3613 let start = first_layout.position_for_index(2).unwrap();
3614 let end = second_layout.position_for_index(3).unwrap();
3615 let states = project_ranges(
3616 Some(plain_snapshot(start, end)),
3617 &[
3618 text_run(2, second_text, second_layout),
3619 text_run(1, gutter_text, gutter_layout),
3620 text_run(0, first_text, first_layout),
3621 ],
3622 );
3623
3624 assert_eq!(states.ranges(), &[Some(0..3), None, Some(2..5)]);
3625 assert!(states.is_active());
3626 }
3627
3628 #[gpui::test]
3629 fn plain_projection_caches_multiple_participant_copies_in_document_order(
3630 cx: &mut TestAppContext,
3631 ) {
3632 let mut runs = laid_out_runs(&["one", "two"], cx);
3633 let (first_text, first_layout) = runs.remove(0);
3634 let (second_text, second_layout) = runs.remove(0);
3635 let snapshot = plain_snapshot(
3636 first_layout.position_for_index(1).unwrap(),
3637 second_layout.position_for_index(2).unwrap(),
3638 );
3639 cx.update(|cx| {
3640 let mut selection_state = WindowSelectionState::default();
3641 let first = FakeParticipant::new("", cx);
3642 let second = FakeParticipant::new("", cx);
3643 first.register(
3644 &mut selection_state,
3645 0.,
3646 TextSelectionScopeId::default(),
3647 1,
3648 cx,
3649 );
3650 second.register(
3651 &mut selection_state,
3652 20.,
3653 TextSelectionScopeId::default(),
3654 0,
3655 cx,
3656 );
3657
3658 first
3659 .selection
3660 .0
3661 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
3662 let projection = first
3663 .selection
3664 .update_runs(&[text_run(0, first_text, first_layout)], cx);
3665 assert_eq!(projection.ranges(), &[Some(1..3)]);
3666 assert!(projection.is_active());
3667 second
3668 .selection
3669 .0
3670 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
3671 let projection = second
3672 .selection
3673 .update_runs(&[text_run(0, second_text, second_layout)], cx);
3674 assert_eq!(projection.ranges(), &[Some(0..2)]);
3675 assert!(projection.is_active());
3676
3677 assert_eq!(selection_state.selected_text(cx), "tw\nne");
3678 });
3679 }
3680
3681 #[gpui::test]
3682 fn plain_projection_invalidates_cached_copy_when_the_snapshot_changes(cx: &mut TestAppContext) {
3683 let (text, layout) = laid_out_runs(&["first"], cx).pop().unwrap();
3684 let first_snapshot = plain_snapshot(
3685 layout.position_for_index(1).unwrap(),
3686 layout.position_for_index(3).unwrap(),
3687 );
3688 let changed_snapshot = plain_snapshot(
3689 layout.position_for_index(3).unwrap(),
3690 layout.position_for_index(5).unwrap(),
3691 );
3692 let run = text_run(0, text, layout);
3693 cx.update(|cx| {
3694 let mut selection_state = WindowSelectionState::default();
3695 let participant = FakeParticipant::new("", cx);
3696 participant.register(
3697 &mut selection_state,
3698 0.,
3699 TextSelectionScopeId::default(),
3700 0,
3701 cx,
3702 );
3703 participant.selection.0.update(cx, |state, cx| {
3704 state.set_snapshot(Some(first_snapshot), cx);
3705 state.update_runs(std::slice::from_ref(&run));
3706 });
3707 assert_eq!(selection_state.selected_text(cx), "ir");
3708
3709 participant.selection.0.update(cx, |state, cx| {
3710 state.set_snapshot(Some(changed_snapshot), cx);
3711 });
3712 assert_eq!(selection_state.selected_text(cx), "");
3713
3714 participant.selection.update_runs(&[run], cx);
3715 assert_eq!(selection_state.selected_text(cx), "st");
3716 selection_state.clear(cx);
3717 participant.selection.set_local_selection(true, cx);
3718 assert_eq!(selection_state.selected_text(cx), "");
3719 });
3720 }
3721
3722 #[gpui::test]
3723 fn plain_projection_orders_cached_runs_by_frame_order_not_input_order(cx: &mut TestAppContext) {
3724 let mut runs = laid_out_runs(&["one", "two"], cx);
3725 let (first_text, first_layout) = runs.remove(0);
3726 let (second_text, second_layout) = runs.remove(0);
3727 let snapshot = plain_snapshot(
3728 first_layout.position_for_index(1).unwrap(),
3729 second_layout.position_for_index(2).unwrap(),
3730 );
3731 cx.update(|cx| {
3732 let mut selection_state = WindowSelectionState::default();
3733 let participant = FakeParticipant::new("", cx);
3734 participant.register(
3735 &mut selection_state,
3736 0.,
3737 TextSelectionScopeId::default(),
3738 0,
3739 cx,
3740 );
3741 participant.selection.0.update(cx, |state, cx| {
3742 state.set_snapshot(Some(snapshot), cx);
3743 state.update_runs(&[
3744 text_run(1, first_text, first_layout),
3745 text_run(0, second_text, second_layout),
3746 ]);
3747 });
3748
3749 assert_eq!(selection_state.selected_text(cx), "twne");
3750 });
3751 }
3752
3753 #[gpui::test]
3754 fn plain_projection_safely_rejects_a_text_layout_length_mismatch(cx: &mut TestAppContext) {
3755 let (_, layout) = laid_out_runs(&["short"], cx).pop().unwrap();
3756 let start = layout.position_for_index(0).unwrap();
3757 let end = layout.position_for_index(5).unwrap();
3758 let states = project_ranges(
3759 Some(plain_snapshot(start, end)),
3760 &[text_run(0, SharedString::from("longer"), layout)],
3761 );
3762
3763 assert_eq!(states.ranges(), &[None]);
3764 assert!(states.is_active());
3765 }
3766
3767 #[gpui::test]
3768 fn begin_update_and_end_publish_a_cross_participant_selection(cx: &mut TestAppContext) {
3769 cx.update(|cx| {
3770 let mut selection_state = WindowSelectionState::default();
3771 let first = FakeParticipant::new("first", cx);
3772 let second = FakeParticipant::new("second", cx);
3773 first.register(
3774 &mut selection_state,
3775 0.,
3776 TextSelectionScopeId::default(),
3777 0,
3778 cx,
3779 );
3780 second.register(
3781 &mut selection_state,
3782 20.,
3783 TextSelectionScopeId::default(),
3784 1,
3785 cx,
3786 );
3787
3788 selection_state.begin(point(px(1.), px(1.)), false, cx);
3789 selection_state.update(point(px(1.), px(25.)), cx);
3790 assert!(selection_state.has_selection(cx));
3791 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
3792
3793 selection_state.end(cx);
3794 assert!(!selection_state.is_selecting());
3795 });
3796 }
3797
3798 #[gpui::test]
3799 fn shift_extension_keeps_its_original_anchor_when_reversed(cx: &mut TestAppContext) {
3800 cx.update(|cx| {
3801 let mut selection_state = WindowSelectionState::default();
3802 let participant = FakeParticipant::new("participant", cx);
3803 participant.register(
3804 &mut selection_state,
3805 0.,
3806 TextSelectionScopeId::default(),
3807 0,
3808 cx,
3809 );
3810
3811 selection_state.begin(point(px(2.), px(2.)), false, cx);
3812 selection_state.end(cx);
3813 selection_state.begin(point(px(8.), px(2.)), true, cx);
3814 selection_state.end(cx);
3815 let first_anchor = selection_state.snapshot().unwrap().anchor();
3816
3817 selection_state.begin(point(px(0.), px(2.)), true, cx);
3818 selection_state.end(cx);
3819 let reversed = selection_state.snapshot().unwrap();
3820 assert_eq!(reversed.anchor(), first_anchor);
3821 assert!(reversed.cursor().content_point().x < reversed.anchor().content_point().x);
3822 });
3823 }
3824
3825 #[gpui::test]
3826 fn content_key_resolver_runs_outside_the_window_state_lease(cx: &mut TestAppContext) {
3827 cx.update(|cx| {
3828 let state = cx.new(|_| WindowSelectionState::default());
3829 let participant = FakeParticipant::new("virtual", cx);
3830 let state_for_callback = state.clone();
3831 participant.selection.resolve_content_key_with(
3832 move |_, cx| {
3833 let _ = state_for_callback.read(cx).snapshot();
3834 Some(TextSelectionContentKey::new(7))
3835 },
3836 cx,
3837 );
3838 state.update(cx, |state, cx| {
3839 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
3840 state.begin(point(px(1.), px(1.)), false, cx);
3841 state.update(point(px(8.), px(1.)), cx);
3842 });
3843
3844 WindowSelectionState::resolve_content_keys(&state, cx);
3845
3846 assert_eq!(
3847 state.read(cx).snapshot().unwrap().cursor().content_key(),
3848 Some(TextSelectionContentKey::new(7))
3849 );
3850 });
3851 }
3852
3853 #[gpui::test]
3854 fn active_dnd_does_not_move_a_text_selection_cursor(cx: &mut TestAppContext) {
3855 let window = cx.add_window(|_, cx| WindowSelectionView {
3856 selection: TextSelectionHandle::new("unused", cx),
3857 });
3858 window
3859 .update(cx, |_, window, cx| {
3860 let mut state = WindowSelectionState::default();
3861 let participant = FakeParticipant::new("participant", cx);
3862 participant.register(&mut state, 0., TextSelectionScopeId::default(), 0, cx);
3863 state.begin(point(px(1.), px(1.)), false, cx);
3864 let before = state.cursor.as_ref().unwrap().point;
3865 state.update_in_window_with_active_drag(point(px(80.), px(1.)), true, window, cx);
3866 assert_eq!(state.cursor.as_ref().unwrap().point, before);
3867 })
3868 .unwrap();
3869 }
3870
3871 #[gpui::test]
3872 fn shift_extension_falls_back_when_the_anchor_participant_was_swept(cx: &mut TestAppContext) {
3873 cx.update(|cx| {
3874 let mut selection_state = WindowSelectionState::default();
3875 let first = FakeParticipant::new("first", cx);
3876 let second = FakeParticipant::new("second", cx);
3877 first.register(
3878 &mut selection_state,
3879 0.,
3880 TextSelectionScopeId::default(),
3881 0,
3882 cx,
3883 );
3884 selection_state.begin(point(px(1.), px(1.)), false, cx);
3885 selection_state.update(point(px(8.), px(1.)), cx);
3886 selection_state.end(cx);
3887
3888 selection_state.finish_frame(cx);
3889 selection_state.finish_frame(cx);
3890 second.register(
3891 &mut selection_state,
3892 20.,
3893 TextSelectionScopeId::default(),
3894 1,
3895 cx,
3896 );
3897 selection_state.begin(point(px(1.), px(21.)), true, cx);
3898 selection_state.update(point(px(8.), px(21.)), cx);
3899 selection_state.end(cx);
3900
3901 assert_eq!(selection_state.selected_text(cx), "second");
3902 });
3903 }
3904
3905 #[gpui::test]
3906 fn scope_and_suppression_prevent_unrelated_participants_from_participating(
3907 cx: &mut TestAppContext,
3908 ) {
3909 cx.update(|cx| {
3910 let mut selection_state = WindowSelectionState::default();
3911 let base = FakeParticipant::new("base", cx);
3912 let modal = FakeParticipant::new("modal", cx);
3913 base.register(
3914 &mut selection_state,
3915 0.,
3916 TextSelectionScopeId::default(),
3917 0,
3918 cx,
3919 );
3920 modal.register(&mut selection_state, 20., TextSelectionScopeId(1), 1, cx);
3921
3922 selection_state.set_active_scope(TextSelectionScopeId(1), cx);
3923 selection_state.begin(point(px(1.), px(21.)), false, cx);
3924 selection_state.update(point(px(8.), px(21.)), cx);
3925 selection_state.end(cx);
3926 assert_eq!(selection_state.selected_text(cx), "modal");
3927
3928 selection_state.clear(cx);
3929 GlobalState::init(cx);
3930 GlobalState::suppress_text_selection(cx);
3931 selection_state.begin(point(px(1.), px(21.)), false, cx);
3932 selection_state.update(point(px(8.), px(21.)), cx);
3933 assert!(!selection_state.has_selection(cx));
3934 });
3935 }
3936
3937 #[gpui::test]
3938 fn dead_participants_are_pruned_and_empty_selection_falls_back_safely(cx: &mut TestAppContext) {
3939 let selection_state = cx.update(|cx| {
3940 let selection_state = cx.new(|_| WindowSelectionState::default());
3941 let participant = FakeParticipant::new("gone", cx);
3942 selection_state.update(cx, |selection_state, cx| {
3943 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx)
3944 });
3945 selection_state
3946 });
3947 cx.update(|cx| {
3948 selection_state.update(cx, |selection_state, cx| {
3949 selection_state.begin(point(px(1.), px(1.)), false, cx);
3950 selection_state.update(point(px(8.), px(1.)), cx);
3951 selection_state.end(cx);
3952
3953 assert_eq!(selection_state.selected_text(cx), "");
3954 assert!(!selection_state.has_selection(cx));
3955 });
3956 });
3957 }
3958
3959 #[gpui::test]
3960 fn text_selection_namespace_reports_copies_ends_and_clears_selection(cx: &mut TestAppContext) {
3961 let (view, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3962 selection: TextSelectionHandle::new("copied", cx),
3963 });
3964 cx.update(|window, cx| {
3965 let selection = view.read(cx).selection.clone();
3966 let selection_state = WindowSelectionState::ensure(window, cx);
3967 selection_state.update(cx, |selection_state, cx| {
3968 FakeParticipant { selection }.register(
3969 selection_state,
3970 0.,
3971 TextSelectionScopeId::default(),
3972 0,
3973 cx,
3974 );
3975 selection_state.begin(point(px(1.), px(1.)), false, cx);
3976 selection_state.update(point(px(8.), px(1.)), cx);
3977 });
3978
3979 assert!(TextSelection::has_selection(window, cx));
3980 assert_eq!(TextSelection::selected_text(window, cx), "copied");
3981 TextSelection::end(window, cx);
3982 assert!(TextSelection::has_selection(window, cx));
3983 TextSelection::clear(window, cx);
3984 assert!(!TextSelection::has_selection(window, cx));
3985 assert_eq!(TextSelection::selected_text(window, cx), "");
3986 });
3987 }
3988
3989 #[gpui::test]
3990 fn two_windows_isolate_selection_copy_clear_and_release_ownership(cx: &mut TestAppContext) {
3991 let first = cx.add_window(|_, cx| WindowOwnedSelectionView {
3992 selection: TextSelectionHandle::new("first", cx),
3993 });
3994 let second = cx.add_window(|_, cx| WindowOwnedSelectionView {
3995 selection: TextSelectionHandle::new("second", cx),
3996 });
3997 let first_selection = cx.update(|cx| first.read(cx).unwrap().selection.clone());
3998 let second_selection = cx.update(|cx| second.read(cx).unwrap().selection.clone());
3999
4000 let first_state = cx
4001 .update_window(*first, |_, window, cx| {
4002 let _ = window.draw(cx);
4003 first_selection.set_local_selection(true, cx);
4004 assert_eq!(TextSelection::selected_text(window, cx), "first");
4005 WindowSelectionState::existing(window, cx)
4006 .unwrap()
4007 .downgrade()
4008 })
4009 .unwrap();
4010 cx.update_window(*second, |_, window, cx| {
4011 let _ = window.draw(cx);
4012 second_selection.set_local_selection(true, cx);
4013 assert_eq!(TextSelection::selected_text(window, cx), "second");
4014 })
4015 .unwrap();
4016
4017 cx.update_window(*first, |_, window, cx| {
4018 TextSelection::clear(window, cx);
4019 assert_eq!(TextSelection::selected_text(window, cx), "");
4020 })
4021 .unwrap();
4022 cx.update_window(*second, |_, window, cx| {
4023 assert_eq!(TextSelection::selected_text(window, cx), "second");
4024 })
4025 .unwrap();
4026
4027 cx.update_window(*first, |_, window, _| window.remove_window())
4028 .unwrap();
4029 cx.run_until_parked();
4030
4031 assert!(first_state.upgrade().is_none());
4032 cx.update_window(*second, |_, window, cx| {
4033 assert_eq!(TextSelection::selected_text(window, cx), "second");
4034 })
4035 .unwrap();
4036 cx.update(|cx| {
4037 assert_eq!(cx.global::<SelectionStateRegistry>().0.len(), 1);
4038 });
4039 }
4040
4041 #[gpui::test]
4042 fn copy_callback_can_reenter_window_and_handle_selection(cx: &mut TestAppContext) {
4043 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
4044 cx.update(|window, cx| {
4045 let _ = window.draw(cx);
4046 let state = WindowSelectionState::existing(window, cx).unwrap();
4047 let selection = TextSelectionHandle::new("fallback", cx);
4048 let state_for_copy = state.clone();
4049 let selection_for_copy = selection.clone();
4050 selection.copy_with(
4051 move |cx: &mut App| {
4052 state_for_copy.update(cx, |state, _| {
4053 assert!(state.snapshot().is_some());
4054 });
4055 assert!(selection_for_copy.snapshot(cx).is_some());
4056 selection_for_copy.set_fallback_copy_text("reentered", cx);
4057 "reentrant copy".to_string()
4058 },
4059 cx,
4060 );
4061 state.update(cx, |state, cx| {
4062 FakeParticipant {
4063 selection: selection.clone(),
4064 }
4065 .register(state, 0., TextSelectionScopeId::default(), 0, cx);
4066 state.begin(point(px(1.), px(1.)), false, cx);
4067 state.update(point(px(8.), px(1.)), cx);
4068 state.end(cx);
4069 });
4070
4071 assert_eq!(TextSelection::selected_text(window, cx), "reentrant copy");
4072 });
4073 }
4074
4075 #[gpui::test]
4076 fn cross_participant_selection_excludes_participants_outside_its_document_interval(
4077 cx: &mut TestAppContext,
4078 ) {
4079 cx.update(|cx| {
4080 let mut selection_state = WindowSelectionState::default();
4081 let first = FakeParticipant::new("first", cx);
4082 let second = FakeParticipant::new("second", cx);
4083 let third = FakeParticipant::new("third", cx);
4084 first.register(
4085 &mut selection_state,
4086 0.,
4087 TextSelectionScopeId::default(),
4088 0,
4089 cx,
4090 );
4091 second.register(
4092 &mut selection_state,
4093 20.,
4094 TextSelectionScopeId::default(),
4095 1,
4096 cx,
4097 );
4098 third.register(
4099 &mut selection_state,
4100 40.,
4101 TextSelectionScopeId::default(),
4102 2,
4103 cx,
4104 );
4105
4106 selection_state.begin(point(px(1.), px(1.)), false, cx);
4107 selection_state.update(point(px(1.), px(25.)), cx);
4108 selection_state.end(cx);
4109
4110 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
4111 assert!(third.selection.snapshot(cx).is_none());
4112 });
4113 }
4114
4115 #[gpui::test]
4116 fn changing_scope_clears_the_previous_scope_selection(cx: &mut TestAppContext) {
4117 cx.update(|cx| {
4118 let mut selection_state = WindowSelectionState::default();
4119 let base = FakeParticipant::new("base", cx);
4120 let modal = FakeParticipant::new("modal", cx);
4121 base.register(
4122 &mut selection_state,
4123 0.,
4124 TextSelectionScopeId::default(),
4125 0,
4126 cx,
4127 );
4128 modal.register(
4129 &mut selection_state,
4130 20.,
4131 TextSelectionScopeId::from_raw(1),
4132 1,
4133 cx,
4134 );
4135
4136 selection_state.begin(point(px(1.), px(1.)), false, cx);
4137 selection_state.update(point(px(8.), px(1.)), cx);
4138 selection_state.end(cx);
4139 selection_state.set_active_scope(TextSelectionScopeId::from_raw(1), cx);
4140
4141 assert!(!selection_state.has_selection(cx));
4142 assert!(base.selection.snapshot(cx).is_none());
4143 });
4144 }
4145
4146 #[gpui::test]
4147 fn blank_only_drag_never_publishes_or_copies_selection(cx: &mut TestAppContext) {
4148 cx.update(|cx| {
4149 let mut selection_state = WindowSelectionState::default();
4150 let participant = FakeParticipant::new("participant", cx);
4151 participant.register(
4152 &mut selection_state,
4153 0.,
4154 TextSelectionScopeId::default(),
4155 0,
4156 cx,
4157 );
4158
4159 selection_state.begin(point(px(200.), px(1.)), false, cx);
4160 selection_state.update(point(px(200.), px(8.)), cx);
4161 selection_state.end(cx);
4162
4163 assert!(!selection_state.has_selection(cx));
4164 assert_eq!(selection_state.selected_text(cx), "");
4165 assert!(participant.selection.snapshot(cx).is_none());
4166 });
4167 }
4168
4169 #[gpui::test]
4170 fn unmounted_selection_does_not_revive_when_the_participant_returns(cx: &mut TestAppContext) {
4171 cx.update(|cx| {
4172 let mut selection_state = WindowSelectionState::default();
4173 let participant = FakeParticipant::new("stale", cx);
4174 participant.register(
4175 &mut selection_state,
4176 0.,
4177 TextSelectionScopeId::default(),
4178 0,
4179 cx,
4180 );
4181 selection_state.begin(point(px(1.), px(1.)), false, cx);
4182 selection_state.update(point(px(8.), px(1.)), cx);
4183 selection_state.end(cx);
4184 assert_eq!(selection_state.selected_text(cx), "stale");
4185
4186 selection_state.finish_frame(cx);
4187 selection_state.finish_frame(cx);
4188 assert_eq!(selection_state.selected_text(cx), "");
4189 assert!(participant.selection.snapshot(cx).is_none());
4190 participant.register(
4191 &mut selection_state,
4192 0.,
4193 TextSelectionScopeId::default(),
4194 0,
4195 cx,
4196 );
4197 assert_eq!(selection_state.selected_text(cx), "");
4198 assert!(participant.selection.snapshot(cx).is_none());
4199
4200 selection_state.begin(point(px(1.), px(1.)), false, cx);
4201 selection_state.update(point(px(8.), px(1.)), cx);
4202 selection_state.end(cx);
4203 assert_eq!(selection_state.selected_text(cx), "stale");
4204 });
4205 }
4206
4207 #[gpui::test]
4208 fn removing_an_unselected_participant_preserves_the_current_selection(cx: &mut TestAppContext) {
4209 cx.update(|cx| {
4210 let mut selection_state = WindowSelectionState::default();
4211 let selected = FakeParticipant::new("selected", cx);
4212 let unrelated = FakeParticipant::new("unrelated", cx);
4213 let scope = TextSelectionScopeId::default();
4214 selected.register(&mut selection_state, 0., scope, 0, cx);
4215 unrelated.register(&mut selection_state, 20., scope, 1, cx);
4216 selection_state.begin(point(px(1.), px(1.)), false, cx);
4217 selection_state.update(point(px(8.), px(1.)), cx);
4218 selection_state.end(cx);
4219 assert_eq!(selection_state.selected_text(cx), "selected");
4220
4221 selection_state.finish_frame(cx);
4222 selected.register(&mut selection_state, 0., scope, 0, cx);
4223 selection_state.finish_frame(cx);
4224 assert_eq!(selection_state.selected_text(cx), "selected");
4225 unrelated.register(&mut selection_state, 20., scope, 1, cx);
4226 assert_eq!(selection_state.selected_text(cx), "selected");
4227 });
4228 }
4229
4230 #[gpui::test]
4231 fn removing_either_range_endpoint_discards_the_shared_selection(cx: &mut TestAppContext) {
4232 for remove_anchor in [true, false] {
4233 cx.update(|cx| {
4234 let mut selection_state = WindowSelectionState::default();
4235 let anchor = FakeParticipant::new("anchor", cx);
4236 let cursor = FakeParticipant::new("cursor", cx);
4237 let scope = TextSelectionScopeId::default();
4238 anchor.register(&mut selection_state, 0., scope, 0, cx);
4239 cursor.register(&mut selection_state, 20., scope, 1, cx);
4240 selection_state.begin(point(px(1.), px(1.)), false, cx);
4241 selection_state.update(point(px(8.), px(21.)), cx);
4242 selection_state.end(cx);
4243 assert_eq!(selection_state.selected_text(cx), "anchor\ncursor");
4244 selection_state.finish_frame(cx);
4245
4246 if remove_anchor {
4247 cursor.register(&mut selection_state, 20., scope, 1, cx);
4248 } else {
4249 anchor.register(&mut selection_state, 0., scope, 0, cx);
4250 }
4251 selection_state.finish_frame(cx);
4252 assert_eq!(selection_state.selected_text(cx), "");
4253 anchor.register(&mut selection_state, 0., scope, 0, cx);
4254 cursor.register(&mut selection_state, 20., scope, 1, cx);
4255 assert_eq!(selection_state.selected_text(cx), "");
4256 });
4257 }
4258 }
4259
4260 #[gpui::test]
4261 fn clear_or_unmount_stops_auto_scroll_before_discarding_the_anchor(cx: &mut TestAppContext) {
4262 for unmount in [false, true] {
4263 let commands = Rc::new(RefCell::new(Vec::new()));
4264 let observed = commands.clone();
4265 let (mut selection_state, participant) = cx.update(|cx| {
4266 let selection_state = WindowSelectionState::default();
4267 let participant = FakeParticipant::new("scroll", cx);
4268 participant
4269 .selection
4270 .subscribe(
4271 move |event, _| {
4272 if let TextSelectionEvent::AutoScroll(delta) = event {
4273 observed.borrow_mut().push(*delta);
4274 }
4275 },
4276 cx,
4277 )
4278 .detach();
4279 (selection_state, participant)
4280 });
4281 cx.run_until_parked();
4282 cx.update(|cx| {
4283 participant.register(
4284 &mut selection_state,
4285 0.,
4286 TextSelectionScopeId::default(),
4287 0,
4288 cx,
4289 );
4290
4291 selection_state.begin(point(px(1.), px(1.)), false, cx);
4292 selection_state.update(point(px(1.), px(25.)), cx);
4293 if unmount {
4294 selection_state.finish_frame(cx);
4295 selection_state.finish_frame(cx);
4296 } else {
4297 selection_state.clear(cx);
4298 }
4299 });
4300 cx.run_until_parked();
4301 assert!(commands.borrow().iter().any(Option::is_some));
4302 assert_eq!(commands.borrow().last(), Some(&None));
4303 }
4304 }
4305
4306 #[gpui::test]
4307 fn drag_auto_scroll_stops_when_the_content_mask_collapses(cx: &mut TestAppContext) {
4308 let window = cx.add_window(|_, cx| WindowSelectionView {
4309 selection: TextSelectionHandle::new("unused", cx),
4310 });
4311 window
4312 .update(cx, |_, window, cx| {
4313 let state = cx.new(|_| WindowSelectionState::default());
4314 let participant = FakeParticipant::new("participant", cx);
4315 state.update(cx, |state, cx| {
4316 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
4317 state.begin(point(px(1.), px(1.)), false, cx);
4318 });
4319 let collapsed = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(0.)));
4322 state.update(cx, |state, cx| {
4323 state.register_participant(
4324 participant.selection.clone(),
4325 TextSelectionRegistration::new(
4326 Hitbox {
4327 id: HitboxId::placeholder(),
4328 bounds: collapsed,
4329 content_mask: ContentMask { bounds: collapsed },
4330 behavior: HitboxBehavior::Normal,
4331 },
4332 collapsed,
4333 )
4334 .with_text_bounds(vec![collapsed]),
4335 cx,
4336 );
4337 state.update_in_window(point(px(1.), px(50.)), window, cx);
4338 assert!(!state.auto_scroll.is_active());
4339 assert!(state.auto_scroll.last_drag_position.is_none());
4340 });
4341 })
4342 .unwrap();
4343 }
4344
4345 #[gpui::test]
4346 fn pointer_moves_after_a_click_do_not_auto_scroll(cx: &mut TestAppContext) {
4347 let window = cx.add_window(|_, cx| WindowSelectionView {
4348 selection: TextSelectionHandle::new("unused", cx),
4349 });
4350 window
4351 .update(cx, |_, window, cx| {
4352 let state = cx.new(|_| WindowSelectionState::default());
4353 let participant = FakeParticipant::new("participant", cx);
4354 state.update(cx, |state, cx| {
4355 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
4356 state.begin(point(px(1.), px(1.)), false, cx);
4358 state.end(cx);
4359 assert!(state.anchor.is_some());
4360
4361 state.update_in_window(point(px(1.), px(50.)), window, cx);
4362 assert!(!state.auto_scroll.is_active());
4363 });
4364 })
4365 .unwrap();
4366 }
4367
4368 #[gpui::test]
4369 fn proxy_endpoints_break_equal_position_ties_by_document_order(cx: &mut TestAppContext) {
4370 cx.update(|cx| {
4371 let mut selection_state = WindowSelectionState::default();
4372 let later = FakeParticipant::new("later", cx);
4373 let earlier = FakeParticipant::new("earlier", cx);
4374 later.register(
4375 &mut selection_state,
4376 0.,
4377 TextSelectionScopeId::default(),
4378 2,
4379 cx,
4380 );
4381 earlier.register(
4382 &mut selection_state,
4383 0.,
4384 TextSelectionScopeId::default(),
4385 1,
4386 cx,
4387 );
4388
4389 selection_state.begin(point(px(1.), px(1.)), false, cx);
4390 selection_state.update(point(px(200.), px(25.)), cx);
4391 let endpoint = selection_state.snapshot().unwrap().cursor();
4392
4393 assert_eq!(endpoint.entity_id(), Some(earlier.selection.entity_id()));
4394 });
4395 }
4396
4397 #[gpui::test]
4398 fn equal_area_hovered_participants_break_ties_by_document_order(cx: &mut TestAppContext) {
4399 cx.update(|cx| {
4400 for _ in 0..64 {
4401 let mut selection_state = WindowSelectionState::default();
4402 let later = FakeParticipant::new("later", cx);
4403 let earliest = FakeParticipant::new("earliest", cx);
4404 let middle = FakeParticipant::new("middle", cx);
4405 later.register(
4406 &mut selection_state,
4407 0.,
4408 TextSelectionScopeId::default(),
4409 30,
4410 cx,
4411 );
4412 earliest.register(
4413 &mut selection_state,
4414 0.,
4415 TextSelectionScopeId::default(),
4416 10,
4417 cx,
4418 );
4419 middle.register(
4420 &mut selection_state,
4421 0.,
4422 TextSelectionScopeId::default(),
4423 20,
4424 cx,
4425 );
4426
4427 selection_state.begin(point(px(1.), px(1.)), false, cx);
4428 selection_state.update(point(px(8.), px(1.)), cx);
4429
4430 assert_eq!(
4431 selection_state.snapshot().unwrap().anchor().entity_id(),
4432 Some(earliest.selection.entity_id())
4433 );
4434 }
4435 });
4436 }
4437
4438 #[gpui::test]
4439 fn text_selection_namespace_is_a_safe_no_op_until_the_element_is_rendered(
4440 cx: &mut TestAppContext,
4441 ) {
4442 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
4443 selection: TextSelectionHandle::new("not enabled", cx),
4444 });
4445 cx.update(|window, cx| {
4446 assert!(!TextSelection::has_selection(window, cx));
4447 assert_eq!(TextSelection::selected_text(window, cx), "");
4448 TextSelection::clear(window, cx);
4449 TextSelection::end(window, cx);
4450 assert!(!TextSelection::has_selection(window, cx));
4451 });
4452 }
4453
4454 #[gpui::test]
4455 fn unit_selection_element_supports_scope_and_registration_on_the_first_frame(
4456 cx: &mut TestAppContext,
4457 ) {
4458 let (view, cx) = cx.add_window_view(|_, cx| FirstFrameScopedSelectionView {
4459 selection: TextSelectionHandle::new("first frame", cx),
4460 });
4461 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
4462
4463 cx.update(|window, cx| {
4464 let _ = window.draw(cx);
4465 let state = WindowSelectionState::existing(window, cx).unwrap();
4466 assert_eq!(
4467 state.read(cx).active_scope,
4468 TextSelectionScopeId::from_raw(23)
4469 );
4470 assert!(
4471 state
4472 .read(cx)
4473 .participants
4474 .borrow()
4475 .contains_key(&selection.entity_id())
4476 );
4477 });
4478 }
4479
4480 #[gpui::test]
4481 fn lazy_registration_does_not_enable_queries_without_the_element(cx: &mut TestAppContext) {
4482 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
4483 selection: TextSelectionHandle::new("registered", cx),
4484 });
4485 cx.update(|window, cx| {
4486 let selection = TextSelectionHandle::new("registered", cx);
4487 selection.set_local_selection(true, cx);
4488 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(20.)));
4489 let hitbox = Hitbox {
4490 id: HitboxId::placeholder(),
4491 bounds,
4492 content_mask: ContentMask { bounds },
4493 behavior: HitboxBehavior::Normal,
4494 };
4495 selection.register(
4496 TextSelectionRegistration::new(hitbox, bounds).with_text_bounds(vec![bounds]),
4497 window,
4498 cx,
4499 );
4500 assert_eq!(TextSelection::selected_text(window, cx), "");
4501 assert!(!TextSelection::has_selection(window, cx));
4502 TextSelection::clear(window, cx);
4503 assert_eq!(TextSelection::selected_text(window, cx), "");
4504 });
4505 }
4506
4507 #[cfg(feature = "gpui-fast")]
4510 struct SelectionStateReader {
4511 renders: Rc<Cell<usize>>,
4512 }
4513
4514 #[cfg(feature = "gpui-fast")]
4515 impl Render for SelectionStateReader {
4516 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4517 self.renders.set(self.renders.get() + 1);
4518 if let Some(state) = WindowSelectionState::existing(window, cx) {
4519 let _ = state.read(cx).participants.borrow().len();
4520 }
4521 div().size_full()
4522 }
4523 }
4524
4525 #[cfg(feature = "gpui-fast")]
4528 struct ReaderBesideSelectionView {
4529 text: gpui::Entity<ToggleSelectionElementView>,
4530 reader: gpui::Entity<SelectionStateReader>,
4531 }
4532
4533 #[cfg(feature = "gpui-fast")]
4534 impl Render for ReaderBesideSelectionView {
4535 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4536 div()
4537 .size_full()
4538 .child(self.reader.clone())
4539 .child(self.text.clone())
4540 }
4541 }
4542
4543 #[cfg(feature = "gpui-fast")]
4547 #[gpui::test]
4548 fn redrawing_selectable_text_leaves_its_readers_retained(cx: &mut TestAppContext) {
4549 let renders = Rc::new(Cell::new(0));
4550 let (view, cx) = cx.add_window_view(|_, cx| ReaderBesideSelectionView {
4551 text: cx.new(|cx| ToggleSelectionElementView {
4552 enabled: true,
4553 selection: TextSelectionHandle::new("text", cx),
4554 }),
4555 reader: cx.new(|_| SelectionStateReader {
4556 renders: renders.clone(),
4557 }),
4558 });
4559 let text = cx.update(|_, cx| view.read(cx).text.clone());
4560 let redraw = |cx: &mut gpui::VisualTestContext| {
4561 text.update(cx, |_, cx| cx.notify());
4562 cx.update(|window, cx| {
4563 let _ = window.draw(cx);
4564 window.simulate_next_frame(cx);
4565 });
4566 cx.run_until_parked();
4567 };
4568 redraw(cx);
4569 redraw(cx);
4570 let before = renders.get();
4571 for _ in 0..4 {
4572 redraw(cx);
4573 }
4574 assert_eq!(renders.get(), before, "the reader rendered again");
4575 }
4576
4577 #[gpui::test]
4578 fn retained_selection_state_releases_and_does_not_resurrect_selection(cx: &mut TestAppContext) {
4579 let (view, cx) = cx.add_window_view(|_, cx| ToggleSelectionElementView {
4580 enabled: true,
4581 selection: TextSelectionHandle::new("local", cx),
4582 });
4583 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
4584 cx.update(|window, cx| {
4585 let _ = window.draw(cx);
4586 selection.set_local_selection(true, cx);
4587 assert!(TextSelection::has_selection(window, cx));
4588
4589 window.simulate_next_frame(cx);
4590 assert!(TextSelection::has_selection(window, cx));
4591 let _ = window.draw(cx);
4592 assert!(TextSelection::has_selection(window, cx));
4593 });
4594 view.update(cx, |view, cx| {
4595 view.enabled = false;
4596 cx.notify();
4597 });
4598 cx.update(|window, cx| {
4599 let _ = window.draw(cx);
4600 });
4601 cx.update(|window, cx| {
4602 window.simulate_next_frame(cx);
4603 });
4604 cx.update(|window, cx| {
4605 window.simulate_next_frame(cx);
4606 });
4607 cx.run_until_parked();
4608 cx.update(|window, cx| {
4609 assert!(!TextSelection::has_selection(window, cx));
4610 assert_eq!(TextSelection::selected_text(window, cx), "");
4611 assert!(!selection.has_local_selection(cx));
4612 });
4613
4614 view.update(cx, |view, cx| {
4615 view.enabled = true;
4616 cx.notify();
4617 });
4618 cx.update(|window, cx| {
4619 let _ = window.draw(cx);
4620 assert!(!TextSelection::has_selection(window, cx));
4621 assert_eq!(TextSelection::selected_text(window, cx), "");
4622 });
4623 }
4624
4625 #[gpui::test]
4626 fn mounted_selection_element_does_not_keep_an_idle_frame_queue_alive(cx: &mut TestAppContext) {
4627 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
4628 cx.update(|window, cx| {
4629 let _ = window.draw(cx);
4630 assert_eq!(window.simulate_next_frame(cx), 0);
4631 assert_eq!(window.simulate_next_frame(cx), 0);
4632 assert!(live_text_selection_state(window, cx).is_some());
4633 });
4634 }
4635
4636 #[gpui::test]
4637 fn selection_element_initializes_suppression_and_respects_bubble_suppression(
4638 cx: &mut TestAppContext,
4639 ) {
4640 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
4641 cx.update(|window, cx| {
4642 let _ = window.draw(cx);
4643 });
4644 cx.simulate_mouse_down(
4645 point(px(1.), px(1.)),
4646 MouseButton::Left,
4647 gpui::Modifiers::default(),
4648 );
4649 cx.simulate_mouse_up(
4650 point(px(1.), px(1.)),
4651 MouseButton::Left,
4652 gpui::Modifiers::default(),
4653 );
4654 cx.update(|window, cx| {
4655 assert!(GlobalState::is_text_selection_suppressed(cx));
4656 assert!(!TextSelection::has_selection(window, cx));
4657 });
4658 }
4659
4660 #[gpui::test]
4661 fn frame_sweep_keeps_a_participant_registered_before_the_selection_element_paints(
4662 cx: &mut TestAppContext,
4663 ) {
4664 cx.update(|cx| {
4665 let mut selection_state = WindowSelectionState::default();
4666 let participant = FakeParticipant::new("painted first", cx);
4667 participant.register(
4668 &mut selection_state,
4669 0.,
4670 TextSelectionScopeId::default(),
4671 0,
4672 cx,
4673 );
4674 selection_state.begin(point(px(1.), px(1.)), false, cx);
4675 selection_state.update(point(px(8.), px(1.)), cx);
4676 selection_state.end(cx);
4677
4678 selection_state.finish_frame(cx);
4679
4680 assert_eq!(selection_state.selected_text(cx), "painted first");
4681 assert!(participant.selection.snapshot(cx).is_some());
4682 });
4683 }
4684
4685 #[gpui::test]
4686 fn two_selection_elements_schedule_only_one_post_frame_sweep(cx: &mut TestAppContext) {
4687 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
4688 selection: TextSelectionHandle::new("once", cx),
4689 });
4690 cx.update(|window, cx| {
4691 let selection_state = WindowSelectionState::ensure(window, cx);
4692 let selection = view.read(cx).selection.clone();
4693 selection_state.update(cx, |selection_state, cx| {
4694 FakeParticipant { selection }.register(
4695 selection_state,
4696 0.,
4697 TextSelectionScopeId::default(),
4698 0,
4699 cx,
4700 );
4701 selection_state.begin(point(px(1.), px(1.)), false, cx);
4702 selection_state.update(point(px(8.), px(1.)), cx);
4703 selection_state.end(cx);
4704 });
4705
4706 let _ = window.draw(cx);
4707 window.simulate_next_frame(cx);
4708
4709 let items = selection_state.read(cx).copy_items(cx);
4710 assert_eq!(resolve_copy_items(items, cx), "once");
4711 });
4712 }
4713
4714 #[gpui::test]
4715 fn duplicate_selection_elements_gate_real_pointer_gestures_and_reentrant_clear(
4716 cx: &mut TestAppContext,
4717 ) {
4718 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
4719 selection: TextSelectionHandle::new("once", cx),
4720 });
4721 let clear_count = Rc::new(Cell::new(0));
4722 cx.update(|window, cx| {
4723 let state = WindowSelectionState::ensure(window, cx);
4724 let state_for_clear = state.clone();
4725 let count = clear_count.clone();
4726 let selection = view.read(cx).selection.clone();
4727 selection
4728 .subscribe(
4729 move |event, cx| {
4730 if matches!(event, TextSelectionEvent::Cleared) {
4731 count.set(count.get() + 1);
4732 let _ = state_for_clear.read(cx).snapshot();
4733 }
4734 },
4735 cx,
4736 )
4737 .detach();
4738 let _ = window.draw(cx);
4739 });
4740
4741 cx.simulate_mouse_down(
4742 point(px(10.), px(10.)),
4743 MouseButton::Left,
4744 gpui::Modifiers::default(),
4745 );
4746 cx.simulate_mouse_up(
4747 point(px(10.), px(10.)),
4748 MouseButton::Left,
4749 gpui::Modifiers::default(),
4750 );
4751 cx.simulate_mouse_down(
4752 point(px(70.), px(10.)),
4753 MouseButton::Left,
4754 gpui::Modifiers {
4755 shift: true,
4756 ..Default::default()
4757 },
4758 );
4759 cx.simulate_mouse_up(
4760 point(px(70.), px(10.)),
4761 MouseButton::Left,
4762 gpui::Modifiers::default(),
4763 );
4764 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
4765
4766 cx.simulate_mouse_down(
4767 point(px(15.), px(10.)),
4768 MouseButton::Left,
4769 gpui::Modifiers::default(),
4770 );
4771 cx.simulate_mouse_move(
4772 point(px(85.), px(10.)),
4773 Some(MouseButton::Left),
4774 gpui::Modifiers::default(),
4775 );
4776 cx.simulate_mouse_up(
4777 point(px(85.), px(10.)),
4778 MouseButton::Left,
4779 gpui::Modifiers::default(),
4780 );
4781 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
4782 assert_eq!(clear_count.get(), 3);
4783 }
4784
4785 #[gpui::test]
4786 fn selection_layer_handles_real_double_and_triple_click_events(cx: &mut TestAppContext) {
4787 let (text, layout) = laid_out_runs(&["alpha beta"], cx).pop().unwrap();
4788 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
4789 selection: TextSelectionHandle::new("", cx),
4790 });
4791 cx.update(|window, cx| {
4792 let _ = window.draw(cx);
4793 let selection = view.read(cx).selection.clone();
4794 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(17)), cx);
4795 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
4796 });
4797
4798 let position = layout.position_for_index(7).unwrap();
4799 cx.simulate_event(MouseDownEvent {
4800 position,
4801 modifiers: gpui::Modifiers::default(),
4802 button: MouseButton::Left,
4803 click_count: 2,
4804 first_mouse: false,
4805 });
4806 cx.simulate_event(MouseUpEvent {
4807 position,
4808 modifiers: gpui::Modifiers::default(),
4809 button: MouseButton::Left,
4810 click_count: 2,
4811 });
4812 cx.update(|window, cx| {
4813 let selection = view.read(cx).selection.clone();
4814 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
4815 assert_eq!(TextSelection::selected_text(window, cx), "beta");
4816 let snapshot = selection.snapshot(cx).unwrap();
4817 assert_eq!(
4818 snapshot.anchor().content_key(),
4819 Some(TextSelectionContentKey::new(17))
4820 );
4821 assert_eq!(
4822 snapshot.cursor().content_key(),
4823 Some(TextSelectionContentKey::new(17))
4824 );
4825 });
4826
4827 cx.simulate_event(MouseDownEvent {
4828 position,
4829 modifiers: gpui::Modifiers::default(),
4830 button: MouseButton::Left,
4831 click_count: 3,
4832 first_mouse: false,
4833 });
4834 cx.simulate_event(MouseUpEvent {
4835 position,
4836 modifiers: gpui::Modifiers::default(),
4837 button: MouseButton::Left,
4838 click_count: 3,
4839 });
4840 cx.update(|window, cx| {
4841 let selection = view.read(cx).selection.clone();
4842 selection.update_runs(&[text_run(0, text, layout)], cx);
4843 assert_eq!(TextSelection::selected_text(window, cx), "alpha beta");
4844 });
4845 }
4846}