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> {
1294 self.finish_frame_scheduled.set(false);
1295 let generation = self.frame_generation.get();
1296 let stale = self
1297 .participants
1298 .get_mut()
1299 .iter()
1300 .filter_map(|(id, registration)| {
1301 (registration.generation != generation && !registration.registration.is_rendered())
1302 .then(|| (*id, registration.participant.clone()))
1303 })
1304 .collect::<Vec<_>>();
1305 let mut handlers = Vec::new();
1306 for (id, participant) in stale {
1307 self.participants.get_mut().remove(&id);
1308 if let Some(participant) = participant.upgrade()
1309 && participant.read(cx).has_selection_to_clear()
1310 {
1311 if let Some(handler) = participant.update(cx, |state, cx| state.clear_state(cx)) {
1312 handlers.push(handler);
1313 }
1314 }
1315 }
1316 self.publish_snapshots(cx);
1317 self.frame_generation.set(generation.wrapping_add(1));
1318 handlers
1319 }
1320
1321 fn schedule_finish_frame(&self) -> bool {
1322 !self.finish_frame_scheduled.replace(true)
1323 }
1324
1325 fn is_active(&self) -> bool {
1328 self.anchor.is_some()
1329 || self.cursor.is_some()
1330 || self.pending_extension_anchor.is_some()
1331 || self.is_selecting
1332 || self.refresh_held_cursor
1333 || self.touch.active
1334 }
1335
1336 fn register_quietly(
1343 &self,
1344 selection: &TextSelectionHandle,
1345 registration: TextSelectionRegistration,
1346 ) -> Option<TextSelectionRegistration> {
1347 if self.is_active() {
1348 return Some(registration);
1349 }
1350 self.participants.borrow_mut().insert(
1351 selection.entity_id(),
1352 ParticipantRegistration {
1353 participant: selection.downgrade(),
1354 registration: Rc::new(registration),
1355 generation: self.frame_generation.get(),
1356 },
1357 );
1358 None
1359 }
1360
1361 fn finish_frame_quietly(&self, cx: &App) -> bool {
1365 if self.is_active() {
1366 return false;
1367 }
1368 let generation = self.frame_generation.get();
1369 let mut participants = self.participants.borrow_mut();
1370 let gone = |registration: &ParticipantRegistration| {
1371 registration.generation != generation && !registration.registration.is_rendered()
1372 };
1373 if participants.values().any(|registration| {
1374 gone(registration)
1375 && registration
1376 .participant
1377 .upgrade()
1378 .is_some_and(|participant| participant.read(cx).has_selection_to_clear())
1379 }) {
1380 return false;
1381 }
1382 participants.retain(|_, registration| {
1383 !gone(registration) && registration.participant.upgrade().is_some()
1384 });
1385 drop(participants);
1386 self.finish_frame_scheduled.set(false);
1387 self.frame_generation.set(generation.wrapping_add(1));
1388 true
1389 }
1390
1391 pub fn register_participant(
1393 &mut self,
1394 selection: TextSelectionHandle,
1395 registration: TextSelectionRegistration,
1396 cx: &mut App,
1397 ) {
1398 self.prune_dead_participants();
1399 if self.is_selecting
1400 && registration.self_scroll
1401 && self.anchor.as_ref().and_then(SelectionEndpoint::entity_id)
1402 == Some(selection.entity_id())
1403 && self
1404 .participants
1405 .get_mut()
1406 .get(&selection.entity_id())
1407 .is_some_and(|previous| {
1408 previous.registration.scroll_offset != registration.scroll_offset
1409 || previous.registration.bounds != registration.bounds
1410 })
1411 {
1412 self.refresh_held_cursor = true;
1413 }
1414 let edges_moved = self.touch.active
1417 && self
1418 .participants
1419 .get_mut()
1420 .get(&selection.entity_id())
1421 .is_none_or(|previous| {
1422 previous.registration.selection_edges != registration.selection_edges
1423 });
1424 self.participants.get_mut().insert(
1425 selection.entity_id(),
1426 ParticipantRegistration {
1427 participant: selection.downgrade(),
1428 registration: Rc::new(registration),
1429 generation: self.frame_generation.get(),
1430 },
1431 );
1432 self.publish_snapshots(cx);
1433 if edges_moved {
1434 self.touch_changed(cx);
1435 }
1436 }
1437
1438 #[cfg(test)]
1440 fn begin(&mut self, position: Point<Pixels>, extend: bool, cx: &mut App) {
1441 self.begin_impl(position, extend, false, None, cx);
1442 }
1443
1444 #[cfg(test)]
1446 fn update(&mut self, position: Point<Pixels>, cx: &mut App) {
1447 self.update_impl(position, None, cx);
1448 }
1449
1450 pub fn end(&mut self, cx: &mut App) {
1452 self.pending_extension_anchor = None;
1453 if !self.is_selecting {
1454 return;
1455 }
1456 self.is_selecting = false;
1457 if !self.did_hit_text {
1458 self.anchor = None;
1459 self.cursor = None;
1460 }
1461 self.stop_anchor_auto_scroll(cx);
1462 self.publish_snapshots(cx);
1463 }
1464
1465 pub fn clear(&mut self, cx: &mut App) {
1467 let handlers = self.clear_state(cx);
1468 dispatch_clear_handlers(handlers, cx);
1469 }
1470
1471 fn clear_state(&mut self, cx: &mut App) -> Vec<ClearHandler> {
1472 self.stop_anchor_auto_scroll(cx);
1473 self.anchor = None;
1474 self.cursor = None;
1475 self.pending_extension_anchor = None;
1476 self.is_selecting = false;
1477 self.did_hit_text = false;
1478 if self.touch.reset() {
1479 self.touch_changed(cx);
1480 }
1481 self.prune_dead_participants();
1482 self.participants
1483 .borrow()
1484 .values()
1485 .filter_map(|registration| registration.participant.upgrade())
1486 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1487 .collect()
1488 }
1489
1490 fn touch_changed(&self, cx: &mut App) {
1503 let participants = self
1504 .participants
1505 .borrow()
1506 .values()
1507 .filter_map(|registration| registration.participant.upgrade())
1508 .filter(|participant| participant.read(cx).snapshot.is_some())
1509 .collect::<Vec<_>>();
1510 let entity_id = self.entity_id;
1511 cx.defer(move |cx| {
1512 for participant in participants {
1513 participant.update(cx, |_, cx| {
1514 cx.emit(TextSelectionEvent::TouchSelectionChanged)
1515 });
1516 }
1517 if let Some(entity_id) = entity_id {
1518 cx.notify(entity_id);
1519 }
1520 });
1521 }
1522
1523 fn copy_items(&self, cx: &App) -> Vec<CopyItem> {
1524 self.participants
1525 .borrow()
1526 .values()
1527 .filter_map(|registration| {
1528 let participant = registration.participant.upgrade()?;
1529 participant
1530 .read(cx)
1531 .copy_item(registration.registration.document_order)
1532 })
1533 .collect()
1534 }
1535
1536 #[cfg(test)]
1537 fn selected_text(&self, cx: &mut App) -> String {
1538 resolve_copy_items(self.copy_items(cx), cx)
1539 }
1540
1541 fn selects_text(&self, cx: &App) -> bool {
1545 self.snapshot().is_some()
1546 && self.participants.borrow().values().any(|registration| {
1547 registration
1548 .participant
1549 .upgrade()
1550 .is_some_and(|participant| {
1551 let participant = participant.read(cx);
1552 project_ranges(participant.snapshot, &participant.runs.borrow())
1553 .ranges()
1554 .iter()
1555 .any(|range| range.as_ref().is_some_and(|range| !range.is_empty()))
1556 })
1557 })
1558 }
1559
1560 pub fn has_selection(&self, cx: &App) -> bool {
1562 self.snapshot().is_some()
1563 || self.participants.borrow().values().any(|registration| {
1564 registration
1565 .participant
1566 .upgrade()
1567 .is_some_and(|participant| participant.read(cx).local_selection)
1568 })
1569 }
1570
1571 pub fn snapshot(&self) -> Option<TextSelectionSnapshot> {
1573 if !self.did_hit_text {
1574 return None;
1575 }
1576 let anchor_endpoint = self.anchor.as_ref()?;
1577 let cursor_endpoint = self.cursor.as_ref()?;
1578 let anchor = anchor_endpoint.resolve(&self.participants.borrow())?;
1579 let cursor = cursor_endpoint.resolve(&self.participants.borrow())?;
1580 (anchor != cursor).then(|| {
1581 TextSelectionSnapshot::new(anchor_endpoint.snapshot(), cursor_endpoint.snapshot())
1582 .with_selecting(self.is_selecting)
1583 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
1584 })
1585 }
1586
1587 #[cfg(test)]
1589 fn is_selecting(&self) -> bool {
1590 self.is_selecting
1591 }
1592
1593 fn prepare_for_mouse_down(&mut self, extend: bool, cx: &mut App) -> Vec<ClearHandler> {
1594 let pending_extension_anchor = extend.then(|| self.anchor.clone()).flatten();
1595 self.stop_anchor_auto_scroll(cx);
1596 self.anchor = None;
1597 self.cursor = None;
1598 self.pending_extension_anchor = None;
1599 self.is_selecting = false;
1600 self.did_hit_text = false;
1601 if self.touch.reset() {
1602 self.touch_changed(cx);
1603 }
1604 self.prune_dead_participants();
1605 let handlers = self
1606 .participants
1607 .borrow()
1608 .values()
1609 .filter_map(|registration| registration.participant.upgrade())
1610 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1611 .collect();
1612 self.pending_extension_anchor = pending_extension_anchor;
1613 handlers
1614 }
1615
1616 fn touch_selection(&self) -> Option<TouchSelectionSnapshot> {
1623 if !self.touch.active {
1624 return None;
1625 }
1626 let mut start: Option<(u64, Bounds<Pixels>, bool)> = None;
1629 let mut end: Option<(u64, Bounds<Pixels>, bool)> = None;
1630 for registration in self.participants.borrow().values() {
1631 let geometry = ®istration.registration;
1632 if geometry.scope != self.active_scope || registration.participant.upgrade().is_none() {
1633 continue;
1634 }
1635 let Some((edge_start, edge_end)) = geometry.selection_edges else {
1636 continue;
1637 };
1638 let order = geometry.document_order;
1639 let viewport = geometry.hitbox.bounds;
1640 if start.is_none_or(|(best, ..)| order < best) {
1641 start = Some((order, edge_start, caret_in_view(edge_start, viewport)));
1642 }
1643 if end.is_none_or(|(best, ..)| order >= best) {
1644 end = Some((order, edge_end, caret_in_view(edge_end, viewport)));
1645 }
1646 }
1647 let edges = match (start, end) {
1648 (Some((_, start, start_visible)), Some((_, end, end_visible))) => {
1649 let edges = (start, end, start_visible, end_visible);
1650 self.touch.last_edges.set(Some(edges));
1651 edges
1652 }
1653 _ if self.touch.drag.is_some() => self.touch.last_edges.get()?,
1654 _ => return None,
1655 };
1656 let (start, end, start_visible, end_visible) = edges;
1657 Some(
1658 TouchSelectionSnapshot::new(start, end)
1659 .with_edge_visible(SelectionEdge::Start, start_visible)
1660 .with_edge_visible(SelectionEdge::End, end_visible)
1661 .with_menu_open(self.touch.menu_open)
1662 .with_dragging(self.touch.drag.map(|drag| drag.edge())),
1663 )
1664 }
1665
1666 fn touch_handles_of(&self, participant: EntityId) -> Vec<(SelectionEdge, Bounds<Pixels>)> {
1671 if !self.touch.active {
1672 return Vec::new();
1673 }
1674 let Some(own) = self.participants.borrow().get(&participant).cloned() else {
1675 return Vec::new();
1676 };
1677 let Some((start, end)) = own.registration.selection_edges else {
1678 return Vec::new();
1679 };
1680 if start == end {
1681 return Vec::new();
1682 }
1683 let order = own.registration.document_order;
1684 let participants = self.participants.borrow();
1685 let others = participants.iter().filter(|(id, registration)| {
1686 **id != participant
1687 && registration.registration.scope == self.active_scope
1688 && registration.registration.selection_edges.is_some()
1689 && registration.participant.upgrade().is_some()
1690 });
1691 let (mut earlier, mut later) = (false, false);
1692 for (_, registration) in others {
1693 let other = registration.registration.document_order;
1694 earlier |= other < order;
1695 later |= other > order;
1696 }
1697 let viewport = own.registration.hitbox.bounds;
1698 let mut handles = Vec::with_capacity(2);
1699 if !earlier && caret_in_view(start, viewport) {
1700 handles.push((SelectionEdge::Start, start));
1701 }
1702 if !later && caret_in_view(end, viewport) {
1703 handles.push((SelectionEdge::End, end));
1704 }
1705 handles
1706 }
1707
1708 fn keep_touch_selection(&mut self, cx: &mut App) {
1710 self.touch.active = self.snapshot().is_some();
1711 self.touch.menu_open = self.touch.active;
1712 self.touch.drag = None;
1713 self.touch_changed(cx);
1714 }
1715
1716 fn register_touch_ui(&mut self, bounds: Bounds<Pixels>) {
1718 self.touch.ui_bounds.push(bounds);
1719 }
1720
1721 fn handles_painted_at(&self, participant: EntityId, bounds: &[Bounds<Pixels>]) -> bool {
1723 self.touch
1724 .handle_bounds
1725 .get(&participant)
1726 .map_or(bounds.is_empty(), |painted| painted == bounds)
1727 }
1728
1729 fn register_handles(&mut self, participant: EntityId, bounds: Vec<Bounds<Pixels>>) {
1732 self.touch.handle_bounds.insert(participant, bounds);
1733 }
1734
1735 fn close_edit_menu(&mut self, cx: &mut App) {
1736 if !self.touch.menu_open {
1737 return;
1738 }
1739 self.touch.menu_open = false;
1740 self.touch_changed(cx);
1741 }
1742
1743 fn edit_menu_on_scroll(&mut self, phase: TouchPhase, cx: &mut App) {
1747 if !self.touch.active || self.touch.drag.is_some() {
1748 return;
1749 }
1750 match phase {
1751 TouchPhase::Ended | TouchPhase::Cancelled => {
1752 if !self.touch.menu_open {
1753 self.touch.menu_open = true;
1754 self.touch_changed(cx);
1755 }
1756 }
1757 _ => self.close_edit_menu(cx),
1758 }
1759 }
1760
1761 fn select_all_touched(&mut self, cx: &mut App) {
1768 if !self.touch.active {
1769 return;
1770 }
1771 let Some((participant, registration)) = self.anchor_registration() else {
1772 return;
1773 };
1774 let (anchor, cursor) = {
1778 let runs = &participant.read(cx).runs.borrow();
1779 let (Some(first), Some(last)) = (
1780 runs.iter().min_by_key(|run| run.document_order),
1781 runs.iter().max_by_key(|run| run.document_order),
1782 ) else {
1783 return;
1784 };
1785 let (Some(start), Some(end)) = (
1786 first.layout.position_for_index(0),
1787 last.layout.position_for_index(last.text.len()),
1788 ) else {
1789 return;
1790 };
1791 let inset = px(1.);
1794 (
1795 point(start.x + inset, start.y + first.layout.line_height() / 2.),
1796 point(end.x - inset, end.y + last.layout.line_height() / 2.),
1797 )
1798 };
1799 let content_key_resolver = participant.read(cx).content_key_resolver.clone();
1800 let to_endpoint = |window_point: Point<Pixels>| {
1801 let content_point =
1802 window_point - registration.bounds.origin - registration.scroll_offset;
1803 SelectionEndpoint {
1804 participant: Some(participant.downgrade()),
1805 point: content_point,
1806 inside: true,
1807 inside_text: true,
1808 content_key: None,
1809 content_key_resolver: content_key_resolver
1810 .clone()
1811 .map(|resolver| (resolver, content_point)),
1812 }
1813 };
1814 self.anchor = Some(to_endpoint(anchor));
1815 self.cursor = Some(to_endpoint(cursor));
1816 self.did_hit_text = true;
1817 self.is_selecting = false;
1818 self.touch.menu_open = true;
1819 self.publish_snapshots(cx);
1820 self.touch_changed(cx);
1821 }
1822
1823 fn begin_edge_drag(
1829 &mut self,
1830 edge: SelectionEdge,
1831 finger: Point<Pixels>,
1832 window: &mut Window,
1833 cx: &mut App,
1834 ) {
1835 let Some(snapshot) = self.touch_selection() else {
1836 return;
1837 };
1838 let handlers = self.prepare_for_mouse_down(false, cx);
1839 dispatch_clear_handlers(handlers, cx);
1840 let held = snapshot.edge(edge.opposite());
1841 let nudge = px(1.);
1844 let anchor_point = match edge {
1845 SelectionEdge::Start => point(held.left() - nudge, held.center().y),
1846 SelectionEdge::End => point(held.left() + nudge, held.center().y),
1847 };
1848 let anchor = self.endpoint(anchor_point, None, cx);
1849 let drag = EdgeDrag::begin(edge, snapshot.edge(edge), finger);
1850 let cursor = self.endpoint(drag.text_position(finger), Some(window), cx);
1851 self.did_hit_text = anchor.inside_text || cursor.inside_text;
1852 self.anchor = Some(anchor);
1853 self.cursor = Some(cursor);
1854 self.is_selecting = true;
1855 self.touch.active = true;
1856 self.touch.menu_open = false;
1857 self.touch.drag = Some(drag);
1858 self.publish_snapshots(cx);
1859 self.touch_changed(cx);
1860 }
1861
1862 fn update_edge_drag(&mut self, finger: Point<Pixels>, window: &Window, cx: &mut Context<Self>) {
1863 let Some(drag) = self.touch.drag else {
1864 return;
1865 };
1866 let before = self.cursor.clone();
1867 self.update_in_window(drag.text_position(finger), window, cx);
1868 if !self.selects_text(cx) {
1871 self.cursor = before;
1872 self.publish_snapshots(cx);
1873 return;
1874 }
1875 if let Some(points) = self
1878 .snapshot()
1879 .and_then(|snapshot| snapshot.window_points())
1880 {
1881 let (anchor, cursor) = (points.anchor(), points.cursor());
1882 let cursor_first = cursor.y < anchor.y || (cursor.y == anchor.y && cursor.x < anchor.x);
1883 let edge = if cursor_first {
1884 SelectionEdge::Start
1885 } else {
1886 SelectionEdge::End
1887 };
1888 if let Some(drag) = self.touch.drag.as_mut() {
1889 drag.set_edge(edge);
1890 }
1891 }
1892 self.touch_changed(cx);
1893 }
1894
1895 fn end_edge_drag(&mut self, cx: &mut App) {
1896 if self.touch.drag.take().is_none() {
1897 return;
1898 }
1899 self.end(cx);
1900 self.keep_touch_selection(cx);
1901 }
1902
1903 fn begin_in_window(
1904 &mut self,
1905 position: Point<Pixels>,
1906 extend: bool,
1907 window: &mut Window,
1908 cx: &mut App,
1909 ) {
1910 self.begin_impl(position, extend, true, Some(window), cx);
1911 }
1912
1913 fn update_in_window(
1914 &mut self,
1915 position: Point<Pixels>,
1916 window: &Window,
1917 cx: &mut Context<Self>,
1918 ) {
1919 if !cx.has_active_drag() {
1920 self.update_impl(position, Some(window), cx);
1921 self.update_auto_scroll(position, window, cx);
1922 }
1923 }
1924
1925 fn select_at(
1926 &mut self,
1927 position: Point<Pixels>,
1928 click_count: usize,
1929 window: &mut Window,
1930 cx: &mut App,
1931 ) {
1932 GlobalState::init(cx);
1933 if GlobalState::is_text_selection_suppressed(cx) {
1934 return;
1935 }
1936 let hit = self.endpoint(position, Some(window), cx);
1937 if !hit.inside_text {
1938 return;
1939 }
1940 let Some(participant) = hit
1941 .participant
1942 .and_then(|participant| participant.upgrade())
1943 else {
1944 return;
1945 };
1946 let points =
1947 points_for_multi_click(&participant.read(cx).runs.borrow(), position, click_count);
1948 let Some((anchor, cursor)) = points else {
1949 return;
1950 };
1951 let Some(registration) = self
1952 .participants
1953 .borrow()
1954 .get(&participant.entity_id())
1955 .cloned()
1956 else {
1957 return;
1958 };
1959 let content_key_resolver = participant.read(cx).content_key_resolver.clone();
1960 let to_endpoint = |point: Point<Pixels>| {
1961 let content_point = point
1962 - registration.registration.bounds.origin
1963 - registration.registration.scroll_offset;
1964 SelectionEndpoint {
1965 participant: Some(participant.downgrade()),
1966 point: content_point,
1967 inside: true,
1968 inside_text: true,
1969 content_key: None,
1970 content_key_resolver: content_key_resolver
1971 .clone()
1972 .map(|resolver| (resolver, content_point)),
1973 }
1974 };
1975 self.anchor = Some(to_endpoint(anchor));
1976 self.cursor = Some(to_endpoint(cursor));
1977 self.did_hit_text = true;
1978 self.is_selecting = false;
1979 participant.update(cx, |state, cx| state.focus(window, cx));
1980 self.publish_snapshots(cx);
1981 }
1982
1983 #[cfg(test)]
1984 fn update_in_window_with_active_drag(
1985 &mut self,
1986 position: Point<Pixels>,
1987 active_drag: bool,
1988 window: &Window,
1989 cx: &mut App,
1990 ) {
1991 if !active_drag {
1992 self.update_impl(position, Some(window), cx);
1993 }
1994 }
1995
1996 fn begin_impl(
1997 &mut self,
1998 position: Point<Pixels>,
1999 extend: bool,
2000 already_prepared: bool,
2001 window: Option<&mut Window>,
2002 cx: &mut App,
2003 ) {
2004 GlobalState::init(cx);
2005 if GlobalState::is_text_selection_suppressed(cx) {
2006 self.pending_extension_anchor = None;
2007 return;
2008 }
2009 let previous_anchor = extend
2010 .then(|| {
2011 self.pending_extension_anchor
2012 .take()
2013 .or_else(|| self.anchor.clone())
2014 })
2015 .flatten()
2016 .filter(|anchor| anchor.resolve(&self.participants.borrow()).is_some());
2017 if !extend && !already_prepared {
2018 self.clear(cx);
2019 }
2020 let endpoint = self.endpoint(position, window.as_deref(), cx);
2021 let focus_participant = endpoint
2022 .inside
2023 .then(|| endpoint.participant.clone())
2024 .flatten();
2025 let anchor = previous_anchor.unwrap_or_else(|| endpoint.clone());
2026 self.anchor = Some(anchor.clone());
2027 self.cursor = Some(endpoint.clone());
2028 self.did_hit_text = anchor.inside_text || endpoint.inside_text;
2029 self.is_selecting = true;
2030 if let Some(participant) = focus_participant.and_then(|participant| participant.upgrade()) {
2031 if let Some(window) = window {
2032 participant.update(cx, |state, cx| state.focus(window, cx));
2033 }
2034 }
2035 self.publish_snapshots(cx);
2036 }
2037
2038 fn update_impl(&mut self, position: Point<Pixels>, window: Option<&Window>, cx: &mut App) {
2039 if !self.is_selecting {
2040 return;
2041 }
2042 let endpoint = self.endpoint(position, window, cx);
2043 self.did_hit_text |= endpoint.inside_text;
2044 self.cursor = Some(endpoint);
2045 if window.is_none() {
2046 self.update_participant_auto_scroll(position, cx);
2047 }
2048 self.publish_snapshots(cx);
2049 }
2050
2051 fn endpoint(
2052 &mut self,
2053 position: Point<Pixels>,
2054 window: Option<&Window>,
2055 cx: &App,
2056 ) -> SelectionEndpoint {
2057 self.prune_dead_participants();
2058 let mut hit: Option<(
2059 WeakEntity<SelectableTextState>,
2060 Rc<TextSelectionRegistration>,
2061 f32,
2062 )> = None;
2063 let mut predecessor: Option<(
2064 WeakEntity<SelectableTextState>,
2065 Rc<TextSelectionRegistration>,
2066 )> = None;
2067 let mut first: Option<(
2068 WeakEntity<SelectableTextState>,
2069 Rc<TextSelectionRegistration>,
2070 )> = None;
2071
2072 for registration in self.participants.borrow().values() {
2073 if registration.registration.scope != self.active_scope
2074 || registration.participant.upgrade().is_none()
2075 {
2076 continue;
2077 }
2078 let participant_geometry = ®istration.registration;
2079 let hovered = window.map_or_else(
2080 || participant_geometry.bounds.contains(&position),
2081 |window| participant_geometry.hitbox.is_hovered(window),
2082 );
2083 if hovered {
2084 let area = f32::from(participant_geometry.bounds.size.width)
2085 * f32::from(participant_geometry.bounds.size.height);
2086 if hit.as_ref().is_none_or(|(_, best, best_area)| {
2087 area < *best_area
2088 || (area == *best_area
2089 && participant_geometry.document_order < best.document_order)
2090 }) {
2091 hit = Some((
2092 registration.participant.clone(),
2093 participant_geometry.clone(),
2094 area,
2095 ));
2096 }
2097 }
2098 if participant_geometry.bounds.top() <= position.y
2099 && predecessor.as_ref().is_none_or(|(_, best)| {
2100 participant_geometry.bounds.top() > best.bounds.top()
2101 || (participant_geometry.bounds.top() == best.bounds.top()
2102 && participant_geometry.document_order < best.document_order)
2103 })
2104 {
2105 predecessor = Some((
2106 registration.participant.clone(),
2107 participant_geometry.clone(),
2108 ));
2109 }
2110 if first.as_ref().is_none_or(|(_, best)| {
2111 participant_geometry.bounds.top() < best.bounds.top()
2112 || (participant_geometry.bounds.top() == best.bounds.top()
2113 && participant_geometry.document_order < best.document_order)
2114 }) {
2115 first = Some((
2116 registration.participant.clone(),
2117 participant_geometry.clone(),
2118 ));
2119 }
2120 }
2121
2122 let selection = hit
2123 .map(|(participant, registration, _)| (participant, registration, true))
2124 .or_else(|| {
2125 predecessor
2126 .or(first)
2127 .map(|(participant, registration)| (participant, registration, false))
2128 });
2129 match selection {
2130 Some((participant, registration, inside)) => {
2131 let point = position - registration.bounds.origin - registration.scroll_offset;
2132 let content_key_resolver = participant.upgrade().and_then(|participant| {
2133 participant
2134 .read(cx)
2135 .content_key_resolver
2136 .clone()
2137 .map(|callback| (callback, point))
2138 });
2139 SelectionEndpoint {
2140 point,
2141 participant: Some(participant),
2142 inside,
2143 inside_text: inside
2144 && registration
2145 .text_bounds
2146 .iter()
2147 .any(|bounds| bounds.contains(&position)),
2148 content_key: None,
2149 content_key_resolver,
2150 }
2151 }
2152 None => SelectionEndpoint {
2153 participant: None,
2154 point: position,
2155 inside: false,
2156 inside_text: false,
2157 content_key: None,
2158 content_key_resolver: None,
2159 },
2160 }
2161 }
2162
2163 fn publish_snapshots(&mut self, cx: &mut App) {
2164 self.prune_dead_participants();
2165 let snapshot = self.snapshot();
2166 let single_participant = self.single_participant();
2167 for (id, registration) in self.participants.borrow().iter() {
2168 let Some(participant) = registration.participant.upgrade() else {
2169 continue;
2170 };
2171 let participant_snapshot = (registration.registration.scope == self.active_scope
2172 && self.participates(*id, registration)
2173 && single_participant.is_none_or(|single| single == *id))
2174 .then_some(snapshot)
2175 .flatten()
2176 .map(|mut snapshot| {
2177 snapshot.coverage = self.coverage_for(*id);
2178 snapshot
2179 });
2180 if participant.read(cx).snapshot == participant_snapshot {
2181 continue;
2182 }
2183 participant.update(cx, |state, cx| state.set_snapshot(participant_snapshot, cx));
2184 }
2185 }
2186
2187 fn coverage_for(&self, id: EntityId) -> TextSelectionCoverage {
2188 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2189 return TextSelectionCoverage::Bounded;
2190 };
2191 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2192 return TextSelectionCoverage::Bounded;
2193 };
2194 if anchor == cursor {
2195 return TextSelectionCoverage::Bounded;
2196 }
2197 let anchor_order = self.participants.borrow()[&anchor]
2198 .registration
2199 .document_order;
2200 let cursor_order = self.participants.borrow()[&cursor]
2201 .registration
2202 .document_order;
2203 if id != anchor && id != cursor {
2204 TextSelectionCoverage::Full
2205 } else if (id == anchor) == (anchor_order < cursor_order) {
2206 TextSelectionCoverage::ToEnd
2207 } else {
2208 TextSelectionCoverage::FromStart
2209 }
2210 }
2211
2212 fn single_participant(&self) -> Option<EntityId> {
2213 let anchor = self.anchor.as_ref()?.entity_id()?;
2214 let cursor = self.cursor.as_ref()?.entity_id()?;
2215 (anchor == cursor).then_some(anchor)
2216 }
2217
2218 fn participates(&self, id: EntityId, registration: &ParticipantRegistration) -> bool {
2219 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2220 return false;
2221 };
2222 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2223 return false;
2224 };
2225 let Some(anchor_registration) = self.participants.borrow().get(&anchor).cloned() else {
2226 return false;
2227 };
2228 let Some(cursor_registration) = self.participants.borrow().get(&cursor).cloned() else {
2229 return false;
2230 };
2231 let start = anchor_registration
2232 .registration
2233 .document_order
2234 .min(cursor_registration.registration.document_order);
2235 let end = anchor_registration
2236 .registration
2237 .document_order
2238 .max(cursor_registration.registration.document_order);
2239 (start..=end).contains(®istration.registration.document_order)
2240 || id == anchor
2241 || id == cursor
2242 }
2243
2244 fn update_auto_scroll(
2245 &mut self,
2246 position: Point<Pixels>,
2247 window: &Window,
2248 cx: &mut Context<Self>,
2249 ) {
2250 if !self.is_selecting {
2253 return;
2254 }
2255 let Some((_, registration)) = self.anchor_registration() else {
2256 return;
2257 };
2258 if registration.self_scroll {
2261 self.auto_scroll.stop();
2262 self.update_participant_auto_scroll(position, cx);
2263 return;
2264 }
2265 let visible_bounds = registration.hitbox.content_mask.bounds;
2270 const HIT_TEST_INSET: Pixels = px(1.);
2272 if visible_bounds.size.width < HIT_TEST_INSET * 2.
2274 || visible_bounds.size.height < HIT_TEST_INSET * 2.
2275 {
2276 self.stop_anchor_auto_scroll(cx);
2277 return;
2278 }
2279 let delta = AutoScroll::compute_delta(position.y, visible_bounds);
2280 let event_position = point(
2281 position.x.clamp(
2282 visible_bounds.left() + HIT_TEST_INSET,
2283 visible_bounds.right() - HIT_TEST_INSET,
2284 ),
2285 position.y.clamp(
2286 visible_bounds.top() + HIT_TEST_INSET,
2287 visible_bounds.bottom() - HIT_TEST_INSET,
2288 ),
2289 );
2290 self.auto_scroll.last_drag_position = Some(event_position);
2291 self.auto_scroll_stall = (None, 0);
2292 let window = window.window_handle();
2293 self.auto_scroll.set(delta, cx, move |delta, state, cx| {
2294 let Some(position) = state.auto_scroll.last_drag_position else {
2295 return;
2296 };
2297 const STALLED_TICKS: u8 = 3;
2301 let geometry = state
2302 .anchor_registration()
2303 .map(|(_, registration)| (registration.bounds, registration.scroll_offset));
2304 let (last, stalled) = &mut state.auto_scroll_stall;
2305 if *last == geometry {
2306 *stalled = stalled.saturating_add(1);
2307 if *stalled >= STALLED_TICKS {
2308 return;
2309 }
2310 } else {
2311 *last = geometry;
2312 *stalled = 0;
2313 }
2314 let window = window;
2315 cx.defer(move |cx| {
2316 _ = window.update(cx, |_, window, cx| {
2317 window.dispatch_event(
2318 ScrollWheelEvent {
2319 position,
2320 delta: ScrollDelta::Pixels(point(px(0.), -delta)),
2321 ..Default::default()
2322 }
2323 .to_platform_input(),
2324 cx,
2325 );
2326 });
2327 });
2328 });
2329 }
2330
2331 fn update_participant_auto_scroll(&self, position: Point<Pixels>, cx: &mut App) {
2334 let Some((participant, registration)) = self.anchor_registration() else {
2335 return;
2336 };
2337 let visible_bounds = registration
2338 .bounds
2339 .intersect(®istration.hitbox.content_mask.bounds);
2340 let delta = if visible_bounds.size.width > px(0.) && visible_bounds.size.height > px(0.) {
2341 AutoScroll::compute_delta(position.y, visible_bounds)
2342 } else {
2343 None
2344 };
2345 participant.update(cx, |state, cx| state.set_auto_scroll(delta, cx));
2346 }
2347
2348 fn stop_anchor_auto_scroll(&mut self, cx: &mut App) {
2349 self.auto_scroll.stop();
2350 let Some(participant) = self.anchor_participant() else {
2351 return;
2352 };
2353 participant.update(cx, |state, cx| state.set_auto_scroll(None, cx));
2354 }
2355
2356 fn anchor_participant(&self) -> Option<Entity<SelectableTextState>> {
2358 self.anchor
2359 .as_ref()
2360 .filter(|anchor| anchor.inside)?
2361 .participant
2362 .as_ref()?
2363 .upgrade()
2364 }
2365
2366 fn anchor_registration(
2368 &self,
2369 ) -> Option<(Entity<SelectableTextState>, Rc<TextSelectionRegistration>)> {
2370 let participant = self.anchor_participant()?;
2371 let registration = self
2372 .participants
2373 .borrow()
2374 .get(&participant.entity_id())?
2375 .clone();
2376 Some((participant, registration.registration.clone()))
2377 }
2378
2379 fn prune_dead_participants(&mut self) {
2380 self.participants
2381 .get_mut()
2382 .retain(|_, registration| registration.participant.upgrade().is_some());
2383 }
2384}
2385
2386#[derive(Default)]
2387struct SelectionStateRegistry(HashMap<gpui::WindowId, WeakEntity<WindowSelectionState>>);
2390
2391impl Global for SelectionStateRegistry {}
2392
2393#[derive(Default)]
2394struct PendingTextSelectionScopes(HashMap<gpui::WindowId, TextSelectionScopeId>);
2395
2396impl Global for PendingTextSelectionScopes {}
2397
2398#[derive(Default)]
2399struct TextSelectionScopeStacks(HashMap<gpui::WindowId, Vec<TextSelectionScopeId>>);
2400
2401impl Global for TextSelectionScopeStacks {}
2402
2403fn push_text_selection_scope(window_id: gpui::WindowId, scope: TextSelectionScopeId, cx: &mut App) {
2404 if !cx.has_global::<TextSelectionScopeStacks>() {
2405 cx.set_global(TextSelectionScopeStacks::default());
2406 }
2407 cx.global_mut::<TextSelectionScopeStacks>()
2408 .0
2409 .entry(window_id)
2410 .or_default()
2411 .push(scope);
2412}
2413
2414fn pop_text_selection_scope(window_id: gpui::WindowId, cx: &mut App) {
2415 let stacks = &mut cx.global_mut::<TextSelectionScopeStacks>().0;
2416 let remove_stack = stacks.get_mut(&window_id).is_some_and(|stack| {
2417 stack.pop();
2418 stack.is_empty()
2419 });
2420 if remove_stack {
2421 stacks.remove(&window_id);
2422 }
2423}
2424
2425fn current_text_selection_scope(
2426 window_id: gpui::WindowId,
2427 cx: &App,
2428) -> Option<TextSelectionScopeId> {
2429 cx.has_global::<TextSelectionScopeStacks>()
2430 .then(|| {
2431 cx.global::<TextSelectionScopeStacks>()
2432 .0
2433 .get(&window_id)
2434 .and_then(|stack| stack.last().copied())
2435 })
2436 .flatten()
2437}
2438
2439fn with_text_selection_scope<T>(
2440 window_id: gpui::WindowId,
2441 scope: TextSelectionScopeId,
2442 cx: &mut App,
2443 callback: impl FnOnce(&mut App) -> T,
2444) -> T {
2445 push_text_selection_scope(window_id, scope, cx);
2446 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(cx)));
2447 pop_text_selection_scope(window_id, cx);
2448 match result {
2449 Ok(result) => result,
2450 Err(payload) => std::panic::resume_unwind(payload),
2451 }
2452}
2453
2454pub struct TextSelection;
2456
2457impl TextSelection {
2458 pub fn is_selectable_at(position: Point<Pixels>, window: &Window, cx: &mut App) -> bool {
2467 live_text_selection_state(window, cx).is_some_and(|state| {
2468 state.update(cx, |state, cx| {
2469 !state.touch.covers(position)
2470 && state.endpoint(position, Some(window), cx).inside_text
2471 })
2472 })
2473 }
2474
2475 pub fn selected_text(window: &mut Window, cx: &mut App) -> String {
2477 let Some(state) = live_text_selection_state(window, cx) else {
2478 return String::new();
2479 };
2480 let items = state.read(cx).copy_items(cx);
2481 resolve_copy_items(items, cx)
2482 }
2483
2484 pub fn has_selection(window: &mut Window, cx: &mut App) -> bool {
2487 live_text_selection_state(window, cx).is_some_and(|state| state.read(cx).has_selection(cx))
2488 }
2489
2490 pub fn clear(window: &mut Window, cx: &mut App) {
2492 if let Some(state) = live_text_selection_state(window, cx) {
2493 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
2494 dispatch_clear_handlers(handlers, cx);
2495 }
2496 }
2497
2498 pub fn clear_for_window(window_id: gpui::WindowId, cx: &mut App) {
2503 clear_window_text_selection(window_id, cx);
2504 }
2505
2506 pub fn end(window: &mut Window, cx: &mut App) {
2508 if let Some(state) = live_text_selection_state(window, cx) {
2509 state.update(cx, |state, cx| state.end(cx));
2510 }
2511 }
2512
2513 pub fn observe_touch_selection(
2517 window: &Window,
2518 cx: &mut App,
2519 callback: impl Fn(&mut App) + 'static,
2520 ) -> Subscription {
2521 let state = WindowSelectionState::acquire(window.window_handle().window_id(), cx);
2522 cx.observe(&state, move |_, cx| callback(cx))
2523 }
2524
2525 pub fn touch_selection(window: &Window, cx: &App) -> Option<TouchSelectionSnapshot> {
2528 WindowSelectionState::existing(window, cx)?
2529 .read(cx)
2530 .touch_selection()
2531 }
2532
2533 pub fn register_touch_ui(bounds: Bounds<Pixels>, window: &Window, cx: &mut App) {
2537 if let Some(state) = WindowSelectionState::existing(window, cx) {
2538 state.update(cx, |state, _| state.register_touch_ui(bounds));
2539 }
2540 }
2541
2542 pub fn close_edit_menu(window: &mut Window, cx: &mut App) {
2544 if let Some(state) = live_text_selection_state(window, cx) {
2545 state.update(cx, |state, cx| state.close_edit_menu(cx));
2546 }
2547 }
2548
2549 pub fn select_all(window: &mut Window, cx: &mut App) {
2552 if let Some(state) = live_text_selection_state(window, cx) {
2553 state.update(cx, |state, cx| state.select_all_touched(cx));
2554 }
2555 }
2556
2557 pub fn begin_edge_drag(
2560 edge: SelectionEdge,
2561 finger: Point<Pixels>,
2562 window: &mut Window,
2563 cx: &mut App,
2564 ) {
2565 if let Some(state) = live_text_selection_state(window, cx) {
2566 state.update(cx, |state, cx| {
2567 state.begin_edge_drag(edge, finger, window, cx)
2568 });
2569 WindowSelectionState::resolve_content_keys(&state, cx);
2570 }
2571 }
2572
2573 pub fn update_edge_drag(finger: Point<Pixels>, window: &mut Window, cx: &mut App) {
2575 if let Some(state) = live_text_selection_state(window, cx) {
2576 state.update(cx, |state, cx| state.update_edge_drag(finger, window, cx));
2577 WindowSelectionState::resolve_content_keys(&state, cx);
2578 }
2579 }
2580
2581 pub fn end_edge_drag(window: &mut Window, cx: &mut App) {
2583 if let Some(state) = live_text_selection_state(window, cx) {
2584 state.update(cx, |state, cx| state.end_edge_drag(cx));
2585 }
2586 }
2587
2588 pub fn activate_scope(scope: TextSelectionScopeId, window: &mut Window, cx: &mut App) {
2590 let Some(state) = WindowSelectionState::existing(window, cx) else {
2591 if !cx.has_global::<PendingTextSelectionScopes>() {
2592 cx.set_global(PendingTextSelectionScopes::default());
2593 }
2594 cx.global_mut::<PendingTextSelectionScopes>()
2595 .0
2596 .insert(window.window_handle().window_id(), scope);
2597 return;
2598 };
2599 if state.read(cx).active_scope == scope {
2600 return;
2601 }
2602 let handlers = state.update(cx, |state, cx| state.set_active_scope_state(scope, cx));
2603 dispatch_clear_handlers(handlers, cx);
2604 }
2605}
2606
2607pub struct TextSelectionLayer;
2612
2613pub(crate) fn text_selection_scope(
2614 scope: TextSelectionScopeId,
2615 element: impl IntoElement,
2616) -> impl IntoElement {
2617 TextSelectionScopeMarker {
2618 scope,
2619 element: element.into_element(),
2620 }
2621}
2622
2623struct TextSelectionScopeMarker<E> {
2624 scope: TextSelectionScopeId,
2625 element: E,
2626}
2627
2628impl<E: Element> IntoElement for TextSelectionScopeMarker<E> {
2629 type Element = Self;
2630
2631 fn into_element(self) -> Self::Element {
2632 self
2633 }
2634}
2635
2636impl<E: Element> Element for TextSelectionScopeMarker<E> {
2637 type RequestLayoutState = E::RequestLayoutState;
2638 type PrepaintState = E::PrepaintState;
2639
2640 fn id(&self) -> Option<ElementId> {
2641 self.element.id()
2642 }
2643
2644 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
2645 self.element.source_location()
2646 }
2647
2648 fn request_layout(
2649 &mut self,
2650 id: Option<&GlobalElementId>,
2651 inspector_id: Option<&InspectorElementId>,
2652 window: &mut Window,
2653 cx: &mut App,
2654 ) -> (LayoutId, Self::RequestLayoutState) {
2655 let window_id = window.window_handle().window_id();
2656 with_text_selection_scope(window_id, self.scope, cx, |cx| {
2657 self.element.request_layout(id, inspector_id, window, cx)
2658 })
2659 }
2660
2661 fn prepaint(
2662 &mut self,
2663 id: Option<&GlobalElementId>,
2664 inspector_id: Option<&InspectorElementId>,
2665 bounds: Bounds<Pixels>,
2666 request_layout: &mut Self::RequestLayoutState,
2667 window: &mut Window,
2668 cx: &mut App,
2669 ) -> Self::PrepaintState {
2670 let window_id = window.window_handle().window_id();
2671 with_text_selection_scope(window_id, self.scope, cx, |cx| {
2672 self.element
2673 .prepaint(id, inspector_id, bounds, request_layout, window, cx)
2674 })
2675 }
2676
2677 fn paint(
2678 &mut self,
2679 id: Option<&GlobalElementId>,
2680 inspector_id: Option<&InspectorElementId>,
2681 bounds: Bounds<Pixels>,
2682 request_layout: &mut Self::RequestLayoutState,
2683 prepaint: &mut Self::PrepaintState,
2684 window: &mut Window,
2685 cx: &mut App,
2686 ) {
2687 let window_id = window.window_handle().window_id();
2688 with_text_selection_scope(window_id, self.scope, cx, |cx| {
2689 self.element.paint(
2690 id,
2691 inspector_id,
2692 bounds,
2693 request_layout,
2694 prepaint,
2695 window,
2696 cx,
2697 );
2698 });
2699 }
2700}
2701
2702#[doc(hidden)]
2703pub struct TextSelectionLayerPrepaintState(Entity<WindowSelectionState>);
2704
2705impl IntoElement for TextSelectionLayer {
2706 type Element = Self;
2707
2708 fn into_element(self) -> Self::Element {
2709 self
2710 }
2711}
2712
2713impl Element for TextSelectionLayer {
2714 type RequestLayoutState = ();
2715 type PrepaintState = TextSelectionLayerPrepaintState;
2716
2717 fn id(&self) -> Option<ElementId> {
2718 Some("window-text-selection".into())
2719 }
2720
2721 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
2722 None
2723 }
2724
2725 fn request_layout(
2726 &mut self,
2727 _: Option<&GlobalElementId>,
2728 _: Option<&InspectorElementId>,
2729 window: &mut Window,
2730 cx: &mut App,
2731 ) -> (LayoutId, Self::RequestLayoutState) {
2732 (window.request_layout(Style::default(), [], cx), ())
2733 }
2734
2735 fn prepaint(
2736 &mut self,
2737 global_id: Option<&GlobalElementId>,
2738 _: Option<&InspectorElementId>,
2739 _: Bounds<Pixels>,
2740 _: &mut Self::RequestLayoutState,
2741 window: &mut Window,
2742 cx: &mut App,
2743 ) -> Self::PrepaintState {
2744 GlobalState::init(cx);
2750 GlobalState::global(cx).begin_selection_frame();
2751 let state = retain_text_selection_state(global_id, window, cx);
2752 if state.read(cx).touch.painted_any() {
2754 state.update(cx, |state, _| state.touch.begin_frame());
2755 }
2756 TextSelectionLayerPrepaintState(state)
2757 }
2758
2759 fn paint(
2760 &mut self,
2761 _: Option<&GlobalElementId>,
2762 _: Option<&InspectorElementId>,
2763 _: Bounds<Pixels>,
2764 _: &mut Self::RequestLayoutState,
2765 state: &mut Self::PrepaintState,
2766 window: &mut Window,
2767 cx: &mut App,
2768 ) {
2769 paint_text_selection(&state.0, window, cx);
2770 }
2771}
2772
2773fn retain_text_selection_state(
2774 global_id: Option<&GlobalElementId>,
2775 window: &mut Window,
2776 cx: &mut App,
2777) -> Entity<WindowSelectionState> {
2778 let window_id = window.window_handle().window_id();
2779 let state = window.with_element_state::<Entity<WindowSelectionState>, _>(
2780 global_id.expect("TextSelection has a stable element id"),
2781 |retained, _| {
2782 let state = retained.unwrap_or_else(|| WindowSelectionState::acquire(window_id, cx));
2783 (state.clone(), state)
2784 },
2785 );
2786 let registered = cx.has_global::<SelectionStateRegistry>()
2789 && cx
2790 .global::<SelectionStateRegistry>()
2791 .0
2792 .get(&window_id)
2793 .is_some_and(|registered| registered.entity_id() == state.entity_id());
2794 if !registered {
2795 if !cx.has_global::<SelectionStateRegistry>() {
2796 cx.set_global(SelectionStateRegistry::default());
2797 }
2798 cx.global_mut::<SelectionStateRegistry>()
2799 .0
2800 .insert(window_id, state.downgrade());
2801 }
2802 state
2803}
2804
2805fn paint_text_selection(state: &Entity<WindowSelectionState>, window: &mut Window, cx: &mut App) {
2806 if state.read(cx).schedule_finish_frame() {
2807 let state = state.downgrade();
2808 window.defer(cx, move |window, cx| {
2809 let Some(state) = state.upgrade() else {
2810 return;
2811 };
2812 if state.read(cx).finish_frame_quietly(cx) {
2813 return;
2814 }
2815 let handlers = state.update(cx, |state, cx| state.finish_frame(cx));
2816 dispatch_clear_handlers(handlers, cx);
2817 let refresh_cursor = state.update(cx, |state, cx| {
2820 if std::mem::take(&mut state.refresh_held_cursor) && state.is_selecting {
2821 let position = window.mouse_position();
2824 let position = state
2825 .touch
2826 .drag
2827 .map_or(position, |drag| drag.text_position(position));
2828 state.update_in_window(position, window, cx);
2829 true
2830 } else {
2831 false
2832 }
2833 });
2834 if refresh_cursor {
2835 WindowSelectionState::resolve_content_keys(&state, cx);
2836 }
2837 });
2838 }
2839
2840 let mouse_down_state = state.downgrade();
2841 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
2842 if event.button != MouseButton::Left {
2843 return;
2844 }
2845 let Some(state) = mouse_down_state.upgrade() else {
2846 return;
2847 };
2848 if state.read(cx).touch.covers(event.position) {
2851 return;
2852 }
2853 if phase.capture() {
2854 GlobalState::init(cx);
2855 GlobalState::reset_text_selection_suppression(cx);
2856 let handlers = state.update(cx, |state, cx| {
2857 if state.mouse_down_prepared {
2858 return Vec::new();
2859 }
2860 state.mouse_down_prepared = true;
2861 state.prepare_for_mouse_down(event.click_count == 1 && event.modifiers.shift, cx)
2862 });
2863 dispatch_clear_handlers(handlers, cx);
2864 } else if event.click_count == 1 {
2865 if GlobalState::is_text_selection_suppressed(cx) {
2866 state.update(cx, |state, _| state.pending_extension_anchor = None);
2867 return;
2868 }
2869 state.update(cx, |state, cx| {
2870 if !state.is_selecting {
2871 state.begin_in_window(event.position, event.modifiers.shift, window, cx)
2872 }
2873 });
2874 WindowSelectionState::resolve_content_keys(&state, cx);
2875 } else if event.click_count >= 2 {
2876 if GlobalState::is_text_selection_suppressed(cx) {
2877 return;
2878 }
2879 let touch = GlobalState::is_touch_press(cx);
2880 state.update(cx, |state, cx| {
2881 state.select_at(event.position, event.click_count, window, cx);
2882 if touch {
2885 state.keep_touch_selection(cx);
2886 }
2887 });
2888 WindowSelectionState::resolve_content_keys(&state, cx);
2889 }
2890 });
2891
2892 window.on_mouse_event(move |event: &TouchDragEvent, phase, _, cx| {
2895 if phase.capture() && event.phase == TouchPhase::Started {
2896 GlobalState::note_touch(cx);
2897 }
2898 });
2899
2900 let long_press_state = state.downgrade();
2903 window.on_mouse_event(move |event: &LongPressEvent, phase, window, cx| {
2904 if !phase.bubble() {
2905 return;
2906 }
2907 let Some(state) = long_press_state.upgrade() else {
2908 return;
2909 };
2910 if event.phase == TouchPhase::Started {
2911 if window.default_prevented()
2912 || state.read(cx).touch.covers(event.start_position)
2913 || !state.update(cx, |state, cx| {
2914 state
2915 .endpoint(event.start_position, Some(window), cx)
2916 .inside_text
2917 })
2918 {
2919 return;
2920 }
2921 GlobalState::init(cx);
2922 GlobalState::reset_text_selection_suppression(cx);
2923 let handlers = state.update(cx, |state, cx| state.prepare_for_mouse_down(false, cx));
2924 dispatch_clear_handlers(handlers, cx);
2925 let selected = state.update(cx, |state, cx| {
2926 state.select_at(event.start_position, 2, window, cx);
2927 state.anchor.is_some()
2928 });
2929 if !selected {
2930 return;
2931 }
2932 window.capture_long_press(&state);
2933 } else if !window.has_long_press_capture(&state) {
2934 return;
2935 } else {
2936 state.update(cx, |state, cx| match event.phase {
2937 TouchPhase::Moved => {
2938 state.is_selecting = true;
2939 state.update_in_window(event.position, window, cx);
2940 }
2941 TouchPhase::Ended | TouchPhase::Cancelled => {
2942 state.end(cx);
2943 state.keep_touch_selection(cx);
2946 }
2947 _ => {}
2948 });
2949 }
2950 window.prevent_default();
2951 cx.stop_propagation();
2952 WindowSelectionState::resolve_content_keys(&state, cx);
2953 });
2954
2955 let drag_state = state.downgrade();
2958 window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
2959 if !phase.bubble() || event.phase == TouchPhase::Started {
2960 return;
2961 }
2962 let Some(state) = drag_state.upgrade() else {
2963 return;
2964 };
2965 if state.read(cx).touch.drag.is_none() {
2966 return;
2967 }
2968 cx.stop_propagation();
2969 state.update(cx, |state, cx| match event.phase {
2970 TouchPhase::Moved => state.update_edge_drag(event.position, window, cx),
2971 _ => state.end_edge_drag(cx),
2972 });
2973 WindowSelectionState::resolve_content_keys(&state, cx);
2974 });
2975 let drag_state = state.downgrade();
2976 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
2977 if phase.bubble()
2978 && event.pressed_button == Some(MouseButton::Left)
2979 && let Some(state) = drag_state.upgrade()
2980 && state.read(cx).touch.drag.is_some()
2981 {
2982 state.update(cx, |state, cx| {
2983 state.update_edge_drag(event.position, window, cx)
2984 });
2985 WindowSelectionState::resolve_content_keys(&state, cx);
2986 }
2987 });
2988 let drag_state = state.downgrade();
2989 window.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| {
2990 if phase.bubble()
2991 && event.button == MouseButton::Left
2992 && let Some(state) = drag_state.upgrade()
2993 {
2994 state.update(cx, |state, cx| state.end_edge_drag(cx));
2995 }
2996 });
2997
2998 let mouse_move_state = state.downgrade();
2999 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
3000 if phase.bubble()
3001 && let Some(state) = mouse_move_state.upgrade()
3002 && state.read(cx).is_selecting
3004 {
3005 state.update(cx, |state, cx| {
3006 if state.touch.drag.is_some() {
3009 return;
3010 }
3011 state.update_in_window(event.position, window, cx)
3012 });
3013 WindowSelectionState::resolve_content_keys(&state, cx);
3014 }
3015 });
3016
3017 let mouse_up_state = state.downgrade();
3018 window.on_mouse_event(move |_: &MouseUpEvent, phase, _, cx| {
3019 if phase.bubble()
3020 && let Some(state) = mouse_up_state.upgrade()
3021 {
3022 state.update(cx, |state, cx| {
3023 state.mouse_down_prepared = false;
3024 state.end(cx)
3025 });
3026 }
3027 });
3028
3029 let scroll_state = state.downgrade();
3030 window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
3031 let Some(state) = scroll_state.upgrade() else {
3032 return;
3033 };
3034 if phase.capture() {
3038 if state.read(cx).touch.active {
3039 state.update(cx, |state, cx| {
3040 state.edit_menu_on_scroll(event.touch_phase, cx)
3041 });
3042 }
3043 return;
3044 }
3045 if phase.bubble() && state.read(cx).is_selecting {
3047 let position = window.mouse_position();
3048 state.update(cx, |state, cx| {
3049 let position = state
3051 .touch
3052 .drag
3053 .map_or(position, |drag| drag.text_position(position));
3054 state.update_in_window(position, window, cx)
3055 });
3056 WindowSelectionState::resolve_content_keys(&state, cx);
3057 }
3058 });
3059}
3060
3061fn live_text_selection_state(
3062 window: &Window,
3063 cx: &mut App,
3064) -> Option<Entity<WindowSelectionState>> {
3065 WindowSelectionState::existing(window, cx)
3066}
3067
3068pub(crate) fn clear_window_text_selection(window_id: gpui::WindowId, cx: &mut App) {
3069 if !cx.has_global::<SelectionStateRegistry>() {
3070 return;
3071 }
3072 let Some(state) = cx
3073 .global::<SelectionStateRegistry>()
3074 .0
3075 .get(&window_id)
3076 .and_then(WeakEntity::upgrade)
3077 else {
3078 return;
3079 };
3080 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
3081 dispatch_clear_handlers(handlers, cx);
3082}
3083
3084#[cfg(test)]
3085mod tests {
3086 use super::*;
3087 use crate::ElementExt as _;
3088 use gpui::{
3089 Bounds, ContentMask, Context, Hitbox, HitboxBehavior, HitboxId, InteractiveElement as _,
3090 IntoElement, ParentElement as _, Render, SharedString, Styled as _, StyledText,
3091 TestAppContext, TextLayout, Window, div, point, prelude::FluentBuilder as _, px, size,
3092 };
3093 use std::{
3094 cell::{Cell, RefCell},
3095 rc::Rc,
3096 };
3097
3098 struct FakeParticipant {
3099 selection: TextSelectionHandle,
3100 }
3101
3102 struct WindowSelectionView {
3103 selection: TextSelectionHandle,
3104 }
3105
3106 struct SelectionElementOnlyView;
3107 struct ToggleSelectionElementView {
3108 enabled: bool,
3109 selection: TextSelectionHandle,
3110 }
3111
3112 struct DoubleSelectionElementView {
3113 selection: TextSelectionHandle,
3114 }
3115
3116 struct WindowOwnedSelectionView {
3117 selection: TextSelectionHandle,
3118 }
3119
3120 struct FirstFrameScopedSelectionView {
3121 selection: TextSelectionHandle,
3122 }
3123
3124 struct PlainRunLayoutView {
3125 texts: Vec<SharedString>,
3126 layouts: Vec<TextLayout>,
3127 }
3128
3129 impl Render for WindowSelectionView {
3130 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3131 div()
3132 }
3133 }
3134
3135 impl Render for SelectionElementOnlyView {
3136 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3137 div()
3138 .size_full()
3139 .child(TextSelectionLayer)
3140 .child(
3141 div()
3142 .size_full()
3143 .on_mouse_down(MouseButton::Left, |_, _, cx| {
3144 GlobalState::suppress_text_selection(cx);
3145 }),
3146 )
3147 }
3148 }
3149
3150 impl Render for ToggleSelectionElementView {
3151 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3152 let selection = self.selection.clone();
3153 div().when(self.enabled, |this| {
3154 this.child(TextSelectionLayer)
3155 .child(div().size_full().on_prepaint(move |bounds, window, cx| {
3156 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3157 selection.register(
3158 TextSelectionRegistration::new(hitbox, bounds)
3159 .with_text_bounds(vec![bounds]),
3160 window,
3161 cx,
3162 );
3163 }))
3164 })
3165 }
3166 }
3167
3168 impl Render for DoubleSelectionElementView {
3169 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3170 let selection = self.selection.clone();
3171 div()
3172 .size_full()
3173 .child(TextSelectionLayer)
3174 .child(TextSelectionLayer)
3175 .on_prepaint(move |bounds, window, cx| {
3176 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3177 selection.register(
3178 TextSelectionRegistration::new(hitbox, bounds)
3179 .with_text_bounds(vec![bounds]),
3180 window,
3181 cx,
3182 );
3183 })
3184 }
3185 }
3186
3187 impl Render for WindowOwnedSelectionView {
3188 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3189 let selection = self.selection.clone();
3190 div()
3191 .size_full()
3192 .child(TextSelectionLayer)
3193 .child(div().size_full().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 FirstFrameScopedSelectionView {
3206 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3207 let scope = TextSelectionScopeId::from_raw(23);
3208 TextSelection::activate_scope(scope, window, cx);
3209 let selection = self.selection.clone();
3210
3211 div().child(TextSelectionLayer).child(
3212 div()
3213 .size_full()
3214 .on_prepaint(move |bounds, window, cx| {
3215 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3216 selection.register(
3217 TextSelectionRegistration::new(hitbox, bounds)
3218 .with_text_bounds(vec![bounds]),
3219 window,
3220 cx,
3221 );
3222 })
3223 .text_selection_scope(scope),
3224 )
3225 }
3226 }
3227
3228 impl Render for PlainRunLayoutView {
3229 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3230 self.layouts.clear();
3231 let children = self
3232 .texts
3233 .iter()
3234 .enumerate()
3235 .map(|(index, text)| {
3236 let text = StyledText::new(text.clone());
3237 self.layouts.push(text.layout().clone());
3238 div().absolute().top(px(index as f32 * 40.)).child(text)
3239 })
3240 .collect::<Vec<_>>();
3241 div().size_full().children(children)
3242 }
3243 }
3244
3245 impl FakeParticipant {
3246 fn new(text: &str, cx: &mut gpui::App) -> Self {
3247 let selection = TextSelectionHandle::new(text, cx);
3248 Self { selection }
3249 }
3250
3251 fn register(
3252 &self,
3253 selection_state: &mut WindowSelectionState,
3254 y: f32,
3255 scope: TextSelectionScopeId,
3256 document_order: u64,
3257 cx: &mut gpui::App,
3258 ) {
3259 let bounds = Bounds::new(point(px(0.), px(y)), size(px(100.), px(10.)));
3260 selection_state.register_participant(
3261 self.selection.clone(),
3262 TextSelectionRegistration::new(
3263 Hitbox {
3264 id: HitboxId::placeholder(),
3265 bounds,
3266 content_mask: ContentMask { bounds },
3267 behavior: HitboxBehavior::Normal,
3268 },
3269 bounds,
3270 )
3271 .with_scope(scope)
3272 .with_document_order(document_order)
3273 .with_text_bounds(vec![bounds]),
3274 cx,
3275 );
3276 }
3277 }
3278
3279 fn laid_out_runs(texts: &[&str], cx: &mut TestAppContext) -> Vec<(SharedString, TextLayout)> {
3280 let texts = texts
3281 .iter()
3282 .map(|text| SharedString::from(*text))
3283 .collect::<Vec<_>>();
3284 let view = cx.add_window({
3285 let texts = texts.clone();
3286 move |_, _| PlainRunLayoutView {
3287 texts,
3288 layouts: Vec::new(),
3289 }
3290 });
3291 cx.update_window(*view, |_, window, cx| {
3292 let _ = window.draw(cx);
3293 })
3294 .unwrap();
3295 let layouts = cx.update(|cx| view.read(cx).unwrap().layouts.clone());
3296 texts.into_iter().zip(layouts).collect()
3297 }
3298
3299 fn plain_snapshot(anchor: Point<Pixels>, cursor: Point<Pixels>) -> TextSelectionSnapshot {
3300 TextSelectionSnapshot::new(
3301 TextSelectionEndpoint::new(None, anchor),
3302 TextSelectionEndpoint::new(None, cursor),
3303 )
3304 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
3305 }
3306
3307 #[gpui::test]
3308 fn scope_stack_is_cleaned_after_panicking_subtree(cx: &mut TestAppContext) {
3309 let window_id = {
3310 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3311 window_cx.update(|window, _| window.window_handle().window_id())
3312 };
3313 let scope = TextSelectionScopeId::from_raw(41);
3314
3315 cx.update(|cx| {
3316 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3317 with_text_selection_scope(window_id, scope, cx, |_| panic!("subtree failed"));
3318 }));
3319
3320 assert!(result.is_err());
3321 assert_eq!(current_text_selection_scope(window_id, cx), None);
3322 });
3323 }
3324
3325 #[gpui::test]
3326 fn reentrant_scope_from_one_window_does_not_pollute_another(cx: &mut TestAppContext) {
3327 let first_window_id = {
3328 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3329 window_cx.update(|window, _| window.window_handle().window_id())
3330 };
3331 let second_window_id = {
3332 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3333 window_cx.update(|window, _| window.window_handle().window_id())
3334 };
3335 let scope = TextSelectionScopeId::from_raw(42);
3336
3337 cx.update(|cx| {
3338 with_text_selection_scope(first_window_id, scope, cx, |cx| {
3339 assert_eq!(current_text_selection_scope(second_window_id, cx), None);
3340 assert_eq!(
3341 current_text_selection_scope(first_window_id, cx),
3342 Some(scope)
3343 );
3344 });
3345 });
3346 }
3347
3348 #[gpui::test]
3349 fn selection_callback_can_reenter_its_selection_state(cx: &mut TestAppContext) {
3350 let called = Rc::new(Cell::new(false));
3351 let called_from_callback = called.clone();
3352 let (selection_state, participant) = cx.update(|cx| {
3353 let selection_state = cx.new(|_| WindowSelectionState::default());
3354 let selection_state_for_callback = selection_state.clone();
3355 let participant = FakeParticipant::new("participant", cx);
3356 participant
3357 .selection
3358 .subscribe(
3359 move |event, cx| {
3360 if matches!(event, TextSelectionEvent::SelectionChanged(Some(_))) {
3361 selection_state_for_callback
3362 .update(cx, |_, _| called_from_callback.set(true));
3363 }
3364 },
3365 cx,
3366 )
3367 .detach();
3368 (selection_state, participant)
3369 });
3370 cx.run_until_parked();
3371 cx.update(|cx| {
3372 selection_state.update(cx, |selection_state, cx| {
3373 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx);
3374 selection_state.begin(point(px(1.), px(1.)), false, cx);
3375 selection_state.update(point(px(20.), px(1.)), cx);
3376 });
3377 });
3378 cx.run_until_parked();
3379 assert!(called.get());
3380 }
3381
3382 #[gpui::test]
3383 fn selection_events_preserve_snapshot_then_clear_order(cx: &mut TestAppContext) {
3384 let observed = Rc::new(RefCell::new(Vec::new()));
3385 let observed_for_callback = observed.clone();
3386 let selection = cx.update(|cx| {
3387 let selection = TextSelectionHandle::new("selection", cx);
3388 selection
3389 .subscribe(
3390 move |event, _| {
3391 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
3392 observed_for_callback.borrow_mut().push(snapshot.is_some());
3393 }
3394 },
3395 cx,
3396 )
3397 .detach();
3398 selection
3399 });
3400 cx.run_until_parked();
3401 cx.update(|cx| {
3402 selection.0.update(cx, |state, cx| {
3403 state.set_snapshot(
3404 Some(plain_snapshot(point(px(1.), px(1.)), point(px(8.), px(1.)))),
3405 cx,
3406 );
3407 state.clear_state(cx);
3408 });
3409 });
3410 cx.run_until_parked();
3411 assert_eq!(&*observed.borrow(), &[true, false]);
3412 }
3413
3414 fn text_run(order: u64, text: SharedString, layout: TextLayout) -> TextSelectionRun {
3415 let bounds = layout.bounds();
3416 TextSelectionRun::new(text, layout, bounds).with_document_order(order)
3417 }
3418
3419 #[gpui::test]
3420 fn public_selection_data_uses_builders_and_readers(cx: &mut TestAppContext) {
3421 let bounds = Bounds::new(point(px(1.), px(2.)), size(px(30.), px(10.)));
3422 let hitbox = Hitbox {
3423 id: HitboxId::placeholder(),
3424 bounds,
3425 content_mask: ContentMask { bounds },
3426 behavior: HitboxBehavior::Normal,
3427 };
3428 let scope = TextSelectionScopeId::from_raw(7);
3429 let endpoint = TextSelectionEndpoint::new(None, bounds.origin)
3430 .with_content_key(TextSelectionContentKey::new(11));
3431 let snapshot = TextSelectionSnapshot::new(endpoint, endpoint)
3432 .with_selecting(true)
3433 .with_window_points(Some(TextSelectionWindowPoints {
3434 anchor: bounds.origin,
3435 cursor: bounds.bottom_right(),
3436 }))
3437 .with_coverage(TextSelectionCoverage::Full);
3438 let registration = TextSelectionRegistration::new(hitbox, bounds)
3439 .with_scroll_offset(point(px(3.), px(4.)))
3440 .with_scope(scope)
3441 .with_document_order(9)
3442 .with_text_bounds(vec![bounds]);
3443
3444 assert_eq!(endpoint.entity_id(), None);
3445 assert_eq!(endpoint.content_point(), bounds.origin);
3446 assert_eq!(
3447 endpoint.content_key(),
3448 Some(TextSelectionContentKey::new(11))
3449 );
3450 assert_eq!(snapshot.anchor(), endpoint);
3451 assert_eq!(snapshot.cursor(), endpoint);
3452 assert!(snapshot.is_selecting());
3453 assert_eq!(snapshot.coverage(), TextSelectionCoverage::Full);
3454 assert_eq!(
3455 snapshot.window_points(),
3456 Some(TextSelectionWindowPoints {
3457 anchor: bounds.origin,
3458 cursor: bounds.bottom_right(),
3459 })
3460 );
3461 assert_eq!(registration.bounds(), bounds);
3462 assert_eq!(registration.scroll_offset(), point(px(3.), px(4.)));
3463 assert_eq!(registration.scope(), scope);
3464 assert_eq!(registration.document_order(), 9);
3465 assert_eq!(registration.text_bounds(), &[bounds]);
3466
3467 let (text, layout) = laid_out_runs(&["aé"], cx).pop().unwrap();
3468 let text_run = TextSelectionRun::new(text.clone(), layout.clone(), layout.bounds())
3469 .with_document_order(3);
3470 assert_eq!(text_run.document_order(), 3);
3471 assert_eq!(text_run.text(), &text);
3472 assert_eq!(text_run.layout().len(), layout.len());
3473 assert_eq!(text_run.bounds(), layout.bounds());
3474
3475 let projection = TextSelectionProjection {
3476 ranges: vec![Some(1..3)],
3477 is_active: true,
3478 };
3479 assert_eq!(projection.ranges(), &[Some(1..3)]);
3480 assert!(projection.is_active());
3481 }
3482
3483 #[gpui::test]
3484 fn selection_handle_is_the_public_adapter_seam(cx: &mut TestAppContext) {
3485 let selected = Rc::new(Cell::new(false));
3486 let selected_from_callback = selected.clone();
3487 cx.update(|cx| {
3488 let selection = TextSelectionHandle::new("initial", cx);
3489 let entity_id = selection.entity_id();
3490 selection.set_fallback_copy_text("updated", cx);
3491 selection.set_local_selection(true, cx);
3492 selection
3493 .subscribe(
3494 move |event, _| {
3495 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
3496 selected_from_callback.set(snapshot.is_some());
3497 }
3498 },
3499 cx,
3500 )
3501 .detach();
3502 selection.focus_with(|_, _| {}, cx);
3503 selection.copy_with(|_| "copied".to_string(), cx);
3504 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(3)), cx);
3505
3506 assert_eq!(selection.entity_id(), entity_id);
3507 assert_eq!(selection.snapshot(cx), None);
3508 assert_eq!(
3509 selection.update_runs(&[], cx),
3510 TextSelectionProjection::default()
3511 );
3512 });
3513 assert!(!selected.get());
3514 }
3515
3516 #[gpui::test]
3517 fn selection_handle_can_subscribe_its_window_to_refresh(cx: &mut TestAppContext) {
3518 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3519 selection: TextSelectionHandle::new("refresh", cx),
3520 });
3521 cx.update(|window, cx| {
3522 let selection = TextSelectionHandle::new("refresh", cx);
3523 selection.refresh_window_on_change(window, cx).detach();
3524 });
3525 }
3526
3527 #[gpui::test]
3528 fn plain_projection_preserves_forward_reversed_and_unicode_ranges(cx: &mut TestAppContext) {
3529 let (text, layout) = laid_out_runs(&["aé🙂z"], cx).pop().unwrap();
3530 let run = text_run(0, text, layout.clone());
3531 let start = layout.position_for_index(1).unwrap();
3532 let end = layout.position_for_index(7).unwrap();
3533
3534 let forward = project_ranges(Some(plain_snapshot(start, end)), std::slice::from_ref(&run));
3535 let reversed = project_ranges(Some(plain_snapshot(end, start)), &[run]);
3536
3537 assert_eq!(forward.ranges(), &[Some(1..7)]);
3538 assert_eq!(reversed.ranges(), &[Some(1..7)]);
3539 assert!(forward.is_active());
3540 assert!(reversed.is_active());
3541 }
3542
3543 #[gpui::test]
3544 fn double_click_expands_a_plain_run_to_the_input_word_boundary(cx: &mut TestAppContext) {
3545 let (text, layout) = laid_out_runs(&["one café, three"], cx).pop().unwrap();
3546 let run = text_run(0, text, layout.clone());
3547 let click = layout.position_for_index(6).unwrap();
3548
3549 let (anchor, cursor) =
3550 points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
3551 let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
3552
3553 assert_eq!(states.ranges(), &[Some(4..9)]);
3554 }
3555
3556 #[gpui::test]
3557 fn multi_click_uses_text_layout_window_coordinates_at_a_nonzero_origin(
3558 cx: &mut TestAppContext,
3559 ) {
3560 let mut runs = laid_out_runs(&["above", "alpha beta"], cx);
3561 let (text, layout) = runs.pop().unwrap();
3562 assert!(layout.bounds().origin.y > px(0.));
3563 let run = text_run(0, text, layout.clone());
3564 let click = layout.position_for_index(7).unwrap();
3565
3566 let (anchor, cursor) =
3567 points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
3568 let projection = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
3569
3570 assert_eq!(projection.ranges(), &[Some(6..10)]);
3571 }
3572
3573 #[gpui::test]
3574 fn triple_click_expands_to_the_input_logical_line_not_the_visual_row(cx: &mut TestAppContext) {
3575 let (text, layout) = laid_out_runs(&["second line"], cx).pop().unwrap();
3576 let run = text_run(0, text, layout.clone());
3577 let click = layout.position_for_index(4).unwrap();
3578
3579 let (anchor, cursor) =
3580 points_for_multi_click(std::slice::from_ref(&run), click, 4).unwrap();
3581 let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
3582
3583 assert_eq!(states.ranges(), &[Some(0..11)]);
3584 assert_eq!(line_range_at("first line\nsecond line\nthird", 15), 11..22);
3585 }
3586
3587 #[gpui::test]
3588 fn plain_projection_spans_multiple_runs_and_leaves_empty_gutters_unselected(
3589 cx: &mut TestAppContext,
3590 ) {
3591 let mut runs = laid_out_runs(&["first", "", "second"], cx);
3592 let (first_text, first_layout) = runs.remove(0);
3593 let (gutter_text, gutter_layout) = runs.remove(0);
3594 let (second_text, second_layout) = runs.remove(0);
3595 let start = first_layout.position_for_index(2).unwrap();
3596 let end = second_layout.position_for_index(3).unwrap();
3597 let states = project_ranges(
3598 Some(plain_snapshot(start, end)),
3599 &[
3600 text_run(2, second_text, second_layout),
3601 text_run(1, gutter_text, gutter_layout),
3602 text_run(0, first_text, first_layout),
3603 ],
3604 );
3605
3606 assert_eq!(states.ranges(), &[Some(0..3), None, Some(2..5)]);
3607 assert!(states.is_active());
3608 }
3609
3610 #[gpui::test]
3611 fn plain_projection_caches_multiple_participant_copies_in_document_order(
3612 cx: &mut TestAppContext,
3613 ) {
3614 let mut runs = laid_out_runs(&["one", "two"], cx);
3615 let (first_text, first_layout) = runs.remove(0);
3616 let (second_text, second_layout) = runs.remove(0);
3617 let snapshot = plain_snapshot(
3618 first_layout.position_for_index(1).unwrap(),
3619 second_layout.position_for_index(2).unwrap(),
3620 );
3621 cx.update(|cx| {
3622 let mut selection_state = WindowSelectionState::default();
3623 let first = FakeParticipant::new("", cx);
3624 let second = FakeParticipant::new("", cx);
3625 first.register(
3626 &mut selection_state,
3627 0.,
3628 TextSelectionScopeId::default(),
3629 1,
3630 cx,
3631 );
3632 second.register(
3633 &mut selection_state,
3634 20.,
3635 TextSelectionScopeId::default(),
3636 0,
3637 cx,
3638 );
3639
3640 first
3641 .selection
3642 .0
3643 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
3644 let projection = first
3645 .selection
3646 .update_runs(&[text_run(0, first_text, first_layout)], cx);
3647 assert_eq!(projection.ranges(), &[Some(1..3)]);
3648 assert!(projection.is_active());
3649 second
3650 .selection
3651 .0
3652 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
3653 let projection = second
3654 .selection
3655 .update_runs(&[text_run(0, second_text, second_layout)], cx);
3656 assert_eq!(projection.ranges(), &[Some(0..2)]);
3657 assert!(projection.is_active());
3658
3659 assert_eq!(selection_state.selected_text(cx), "tw\nne");
3660 });
3661 }
3662
3663 #[gpui::test]
3664 fn plain_projection_invalidates_cached_copy_when_the_snapshot_changes(cx: &mut TestAppContext) {
3665 let (text, layout) = laid_out_runs(&["first"], cx).pop().unwrap();
3666 let first_snapshot = plain_snapshot(
3667 layout.position_for_index(1).unwrap(),
3668 layout.position_for_index(3).unwrap(),
3669 );
3670 let changed_snapshot = plain_snapshot(
3671 layout.position_for_index(3).unwrap(),
3672 layout.position_for_index(5).unwrap(),
3673 );
3674 let run = text_run(0, text, layout);
3675 cx.update(|cx| {
3676 let mut selection_state = WindowSelectionState::default();
3677 let participant = FakeParticipant::new("", cx);
3678 participant.register(
3679 &mut selection_state,
3680 0.,
3681 TextSelectionScopeId::default(),
3682 0,
3683 cx,
3684 );
3685 participant.selection.0.update(cx, |state, cx| {
3686 state.set_snapshot(Some(first_snapshot), cx);
3687 state.update_runs(std::slice::from_ref(&run));
3688 });
3689 assert_eq!(selection_state.selected_text(cx), "ir");
3690
3691 participant.selection.0.update(cx, |state, cx| {
3692 state.set_snapshot(Some(changed_snapshot), cx);
3693 });
3694 assert_eq!(selection_state.selected_text(cx), "");
3695
3696 participant.selection.update_runs(&[run], cx);
3697 assert_eq!(selection_state.selected_text(cx), "st");
3698 selection_state.clear(cx);
3699 participant.selection.set_local_selection(true, cx);
3700 assert_eq!(selection_state.selected_text(cx), "");
3701 });
3702 }
3703
3704 #[gpui::test]
3705 fn plain_projection_orders_cached_runs_by_frame_order_not_input_order(cx: &mut TestAppContext) {
3706 let mut runs = laid_out_runs(&["one", "two"], cx);
3707 let (first_text, first_layout) = runs.remove(0);
3708 let (second_text, second_layout) = runs.remove(0);
3709 let snapshot = plain_snapshot(
3710 first_layout.position_for_index(1).unwrap(),
3711 second_layout.position_for_index(2).unwrap(),
3712 );
3713 cx.update(|cx| {
3714 let mut selection_state = WindowSelectionState::default();
3715 let participant = FakeParticipant::new("", cx);
3716 participant.register(
3717 &mut selection_state,
3718 0.,
3719 TextSelectionScopeId::default(),
3720 0,
3721 cx,
3722 );
3723 participant.selection.0.update(cx, |state, cx| {
3724 state.set_snapshot(Some(snapshot), cx);
3725 state.update_runs(&[
3726 text_run(1, first_text, first_layout),
3727 text_run(0, second_text, second_layout),
3728 ]);
3729 });
3730
3731 assert_eq!(selection_state.selected_text(cx), "twne");
3732 });
3733 }
3734
3735 #[gpui::test]
3736 fn plain_projection_safely_rejects_a_text_layout_length_mismatch(cx: &mut TestAppContext) {
3737 let (_, layout) = laid_out_runs(&["short"], cx).pop().unwrap();
3738 let start = layout.position_for_index(0).unwrap();
3739 let end = layout.position_for_index(5).unwrap();
3740 let states = project_ranges(
3741 Some(plain_snapshot(start, end)),
3742 &[text_run(0, SharedString::from("longer"), layout)],
3743 );
3744
3745 assert_eq!(states.ranges(), &[None]);
3746 assert!(states.is_active());
3747 }
3748
3749 #[gpui::test]
3750 fn begin_update_and_end_publish_a_cross_participant_selection(cx: &mut TestAppContext) {
3751 cx.update(|cx| {
3752 let mut selection_state = WindowSelectionState::default();
3753 let first = FakeParticipant::new("first", cx);
3754 let second = FakeParticipant::new("second", cx);
3755 first.register(
3756 &mut selection_state,
3757 0.,
3758 TextSelectionScopeId::default(),
3759 0,
3760 cx,
3761 );
3762 second.register(
3763 &mut selection_state,
3764 20.,
3765 TextSelectionScopeId::default(),
3766 1,
3767 cx,
3768 );
3769
3770 selection_state.begin(point(px(1.), px(1.)), false, cx);
3771 selection_state.update(point(px(1.), px(25.)), cx);
3772 assert!(selection_state.has_selection(cx));
3773 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
3774
3775 selection_state.end(cx);
3776 assert!(!selection_state.is_selecting());
3777 });
3778 }
3779
3780 #[gpui::test]
3781 fn shift_extension_keeps_its_original_anchor_when_reversed(cx: &mut TestAppContext) {
3782 cx.update(|cx| {
3783 let mut selection_state = WindowSelectionState::default();
3784 let participant = FakeParticipant::new("participant", cx);
3785 participant.register(
3786 &mut selection_state,
3787 0.,
3788 TextSelectionScopeId::default(),
3789 0,
3790 cx,
3791 );
3792
3793 selection_state.begin(point(px(2.), px(2.)), false, cx);
3794 selection_state.end(cx);
3795 selection_state.begin(point(px(8.), px(2.)), true, cx);
3796 selection_state.end(cx);
3797 let first_anchor = selection_state.snapshot().unwrap().anchor();
3798
3799 selection_state.begin(point(px(0.), px(2.)), true, cx);
3800 selection_state.end(cx);
3801 let reversed = selection_state.snapshot().unwrap();
3802 assert_eq!(reversed.anchor(), first_anchor);
3803 assert!(reversed.cursor().content_point().x < reversed.anchor().content_point().x);
3804 });
3805 }
3806
3807 #[gpui::test]
3808 fn content_key_resolver_runs_outside_the_window_state_lease(cx: &mut TestAppContext) {
3809 cx.update(|cx| {
3810 let state = cx.new(|_| WindowSelectionState::default());
3811 let participant = FakeParticipant::new("virtual", cx);
3812 let state_for_callback = state.clone();
3813 participant.selection.resolve_content_key_with(
3814 move |_, cx| {
3815 let _ = state_for_callback.read(cx).snapshot();
3816 Some(TextSelectionContentKey::new(7))
3817 },
3818 cx,
3819 );
3820 state.update(cx, |state, cx| {
3821 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
3822 state.begin(point(px(1.), px(1.)), false, cx);
3823 state.update(point(px(8.), px(1.)), cx);
3824 });
3825
3826 WindowSelectionState::resolve_content_keys(&state, cx);
3827
3828 assert_eq!(
3829 state.read(cx).snapshot().unwrap().cursor().content_key(),
3830 Some(TextSelectionContentKey::new(7))
3831 );
3832 });
3833 }
3834
3835 #[gpui::test]
3836 fn active_dnd_does_not_move_a_text_selection_cursor(cx: &mut TestAppContext) {
3837 let window = cx.add_window(|_, cx| WindowSelectionView {
3838 selection: TextSelectionHandle::new("unused", cx),
3839 });
3840 window
3841 .update(cx, |_, window, cx| {
3842 let mut state = WindowSelectionState::default();
3843 let participant = FakeParticipant::new("participant", cx);
3844 participant.register(&mut state, 0., TextSelectionScopeId::default(), 0, cx);
3845 state.begin(point(px(1.), px(1.)), false, cx);
3846 let before = state.cursor.as_ref().unwrap().point;
3847 state.update_in_window_with_active_drag(point(px(80.), px(1.)), true, window, cx);
3848 assert_eq!(state.cursor.as_ref().unwrap().point, before);
3849 })
3850 .unwrap();
3851 }
3852
3853 #[gpui::test]
3854 fn shift_extension_falls_back_when_the_anchor_participant_was_swept(cx: &mut TestAppContext) {
3855 cx.update(|cx| {
3856 let mut selection_state = WindowSelectionState::default();
3857 let first = FakeParticipant::new("first", cx);
3858 let second = FakeParticipant::new("second", cx);
3859 first.register(
3860 &mut selection_state,
3861 0.,
3862 TextSelectionScopeId::default(),
3863 0,
3864 cx,
3865 );
3866 selection_state.begin(point(px(1.), px(1.)), false, cx);
3867 selection_state.update(point(px(8.), px(1.)), cx);
3868 selection_state.end(cx);
3869
3870 selection_state.finish_frame(cx);
3871 selection_state.finish_frame(cx);
3872 second.register(
3873 &mut selection_state,
3874 20.,
3875 TextSelectionScopeId::default(),
3876 1,
3877 cx,
3878 );
3879 selection_state.begin(point(px(1.), px(21.)), true, cx);
3880 selection_state.update(point(px(8.), px(21.)), cx);
3881 selection_state.end(cx);
3882
3883 assert_eq!(selection_state.selected_text(cx), "second");
3884 });
3885 }
3886
3887 #[gpui::test]
3888 fn scope_and_suppression_prevent_unrelated_participants_from_participating(
3889 cx: &mut TestAppContext,
3890 ) {
3891 cx.update(|cx| {
3892 let mut selection_state = WindowSelectionState::default();
3893 let base = FakeParticipant::new("base", cx);
3894 let modal = FakeParticipant::new("modal", cx);
3895 base.register(
3896 &mut selection_state,
3897 0.,
3898 TextSelectionScopeId::default(),
3899 0,
3900 cx,
3901 );
3902 modal.register(&mut selection_state, 20., TextSelectionScopeId(1), 1, cx);
3903
3904 selection_state.set_active_scope(TextSelectionScopeId(1), cx);
3905 selection_state.begin(point(px(1.), px(21.)), false, cx);
3906 selection_state.update(point(px(8.), px(21.)), cx);
3907 selection_state.end(cx);
3908 assert_eq!(selection_state.selected_text(cx), "modal");
3909
3910 selection_state.clear(cx);
3911 GlobalState::init(cx);
3912 GlobalState::suppress_text_selection(cx);
3913 selection_state.begin(point(px(1.), px(21.)), false, cx);
3914 selection_state.update(point(px(8.), px(21.)), cx);
3915 assert!(!selection_state.has_selection(cx));
3916 });
3917 }
3918
3919 #[gpui::test]
3920 fn dead_participants_are_pruned_and_empty_selection_falls_back_safely(cx: &mut TestAppContext) {
3921 let selection_state = cx.update(|cx| {
3922 let selection_state = cx.new(|_| WindowSelectionState::default());
3923 let participant = FakeParticipant::new("gone", cx);
3924 selection_state.update(cx, |selection_state, cx| {
3925 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx)
3926 });
3927 selection_state
3928 });
3929 cx.update(|cx| {
3930 selection_state.update(cx, |selection_state, cx| {
3931 selection_state.begin(point(px(1.), px(1.)), false, cx);
3932 selection_state.update(point(px(8.), px(1.)), cx);
3933 selection_state.end(cx);
3934
3935 assert_eq!(selection_state.selected_text(cx), "");
3936 assert!(!selection_state.has_selection(cx));
3937 });
3938 });
3939 }
3940
3941 #[gpui::test]
3942 fn text_selection_namespace_reports_copies_ends_and_clears_selection(cx: &mut TestAppContext) {
3943 let (view, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3944 selection: TextSelectionHandle::new("copied", cx),
3945 });
3946 cx.update(|window, cx| {
3947 let selection = view.read(cx).selection.clone();
3948 let selection_state = WindowSelectionState::ensure(window, cx);
3949 selection_state.update(cx, |selection_state, cx| {
3950 FakeParticipant { selection }.register(
3951 selection_state,
3952 0.,
3953 TextSelectionScopeId::default(),
3954 0,
3955 cx,
3956 );
3957 selection_state.begin(point(px(1.), px(1.)), false, cx);
3958 selection_state.update(point(px(8.), px(1.)), cx);
3959 });
3960
3961 assert!(TextSelection::has_selection(window, cx));
3962 assert_eq!(TextSelection::selected_text(window, cx), "copied");
3963 TextSelection::end(window, cx);
3964 assert!(TextSelection::has_selection(window, cx));
3965 TextSelection::clear(window, cx);
3966 assert!(!TextSelection::has_selection(window, cx));
3967 assert_eq!(TextSelection::selected_text(window, cx), "");
3968 });
3969 }
3970
3971 #[gpui::test]
3972 fn two_windows_isolate_selection_copy_clear_and_release_ownership(cx: &mut TestAppContext) {
3973 let first = cx.add_window(|_, cx| WindowOwnedSelectionView {
3974 selection: TextSelectionHandle::new("first", cx),
3975 });
3976 let second = cx.add_window(|_, cx| WindowOwnedSelectionView {
3977 selection: TextSelectionHandle::new("second", cx),
3978 });
3979 let first_selection = cx.update(|cx| first.read(cx).unwrap().selection.clone());
3980 let second_selection = cx.update(|cx| second.read(cx).unwrap().selection.clone());
3981
3982 let first_state = cx
3983 .update_window(*first, |_, window, cx| {
3984 let _ = window.draw(cx);
3985 first_selection.set_local_selection(true, cx);
3986 assert_eq!(TextSelection::selected_text(window, cx), "first");
3987 WindowSelectionState::existing(window, cx)
3988 .unwrap()
3989 .downgrade()
3990 })
3991 .unwrap();
3992 cx.update_window(*second, |_, window, cx| {
3993 let _ = window.draw(cx);
3994 second_selection.set_local_selection(true, cx);
3995 assert_eq!(TextSelection::selected_text(window, cx), "second");
3996 })
3997 .unwrap();
3998
3999 cx.update_window(*first, |_, window, cx| {
4000 TextSelection::clear(window, cx);
4001 assert_eq!(TextSelection::selected_text(window, cx), "");
4002 })
4003 .unwrap();
4004 cx.update_window(*second, |_, window, cx| {
4005 assert_eq!(TextSelection::selected_text(window, cx), "second");
4006 })
4007 .unwrap();
4008
4009 cx.update_window(*first, |_, window, _| window.remove_window())
4010 .unwrap();
4011 cx.run_until_parked();
4012
4013 assert!(first_state.upgrade().is_none());
4014 cx.update_window(*second, |_, window, cx| {
4015 assert_eq!(TextSelection::selected_text(window, cx), "second");
4016 })
4017 .unwrap();
4018 cx.update(|cx| {
4019 assert_eq!(cx.global::<SelectionStateRegistry>().0.len(), 1);
4020 });
4021 }
4022
4023 #[gpui::test]
4024 fn copy_callback_can_reenter_window_and_handle_selection(cx: &mut TestAppContext) {
4025 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
4026 cx.update(|window, cx| {
4027 let _ = window.draw(cx);
4028 let state = WindowSelectionState::existing(window, cx).unwrap();
4029 let selection = TextSelectionHandle::new("fallback", cx);
4030 let state_for_copy = state.clone();
4031 let selection_for_copy = selection.clone();
4032 selection.copy_with(
4033 move |cx: &mut App| {
4034 state_for_copy.update(cx, |state, _| {
4035 assert!(state.snapshot().is_some());
4036 });
4037 assert!(selection_for_copy.snapshot(cx).is_some());
4038 selection_for_copy.set_fallback_copy_text("reentered", cx);
4039 "reentrant copy".to_string()
4040 },
4041 cx,
4042 );
4043 state.update(cx, |state, cx| {
4044 FakeParticipant {
4045 selection: selection.clone(),
4046 }
4047 .register(state, 0., TextSelectionScopeId::default(), 0, cx);
4048 state.begin(point(px(1.), px(1.)), false, cx);
4049 state.update(point(px(8.), px(1.)), cx);
4050 state.end(cx);
4051 });
4052
4053 assert_eq!(TextSelection::selected_text(window, cx), "reentrant copy");
4054 });
4055 }
4056
4057 #[gpui::test]
4058 fn cross_participant_selection_excludes_participants_outside_its_document_interval(
4059 cx: &mut TestAppContext,
4060 ) {
4061 cx.update(|cx| {
4062 let mut selection_state = WindowSelectionState::default();
4063 let first = FakeParticipant::new("first", cx);
4064 let second = FakeParticipant::new("second", cx);
4065 let third = FakeParticipant::new("third", cx);
4066 first.register(
4067 &mut selection_state,
4068 0.,
4069 TextSelectionScopeId::default(),
4070 0,
4071 cx,
4072 );
4073 second.register(
4074 &mut selection_state,
4075 20.,
4076 TextSelectionScopeId::default(),
4077 1,
4078 cx,
4079 );
4080 third.register(
4081 &mut selection_state,
4082 40.,
4083 TextSelectionScopeId::default(),
4084 2,
4085 cx,
4086 );
4087
4088 selection_state.begin(point(px(1.), px(1.)), false, cx);
4089 selection_state.update(point(px(1.), px(25.)), cx);
4090 selection_state.end(cx);
4091
4092 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
4093 assert!(third.selection.snapshot(cx).is_none());
4094 });
4095 }
4096
4097 #[gpui::test]
4098 fn changing_scope_clears_the_previous_scope_selection(cx: &mut TestAppContext) {
4099 cx.update(|cx| {
4100 let mut selection_state = WindowSelectionState::default();
4101 let base = FakeParticipant::new("base", cx);
4102 let modal = FakeParticipant::new("modal", cx);
4103 base.register(
4104 &mut selection_state,
4105 0.,
4106 TextSelectionScopeId::default(),
4107 0,
4108 cx,
4109 );
4110 modal.register(
4111 &mut selection_state,
4112 20.,
4113 TextSelectionScopeId::from_raw(1),
4114 1,
4115 cx,
4116 );
4117
4118 selection_state.begin(point(px(1.), px(1.)), false, cx);
4119 selection_state.update(point(px(8.), px(1.)), cx);
4120 selection_state.end(cx);
4121 selection_state.set_active_scope(TextSelectionScopeId::from_raw(1), cx);
4122
4123 assert!(!selection_state.has_selection(cx));
4124 assert!(base.selection.snapshot(cx).is_none());
4125 });
4126 }
4127
4128 #[gpui::test]
4129 fn blank_only_drag_never_publishes_or_copies_selection(cx: &mut TestAppContext) {
4130 cx.update(|cx| {
4131 let mut selection_state = WindowSelectionState::default();
4132 let participant = FakeParticipant::new("participant", cx);
4133 participant.register(
4134 &mut selection_state,
4135 0.,
4136 TextSelectionScopeId::default(),
4137 0,
4138 cx,
4139 );
4140
4141 selection_state.begin(point(px(200.), px(1.)), false, cx);
4142 selection_state.update(point(px(200.), px(8.)), cx);
4143 selection_state.end(cx);
4144
4145 assert!(!selection_state.has_selection(cx));
4146 assert_eq!(selection_state.selected_text(cx), "");
4147 assert!(participant.selection.snapshot(cx).is_none());
4148 });
4149 }
4150
4151 #[gpui::test]
4152 fn stale_live_participants_are_removed_when_the_next_frame_begins(cx: &mut TestAppContext) {
4153 cx.update(|cx| {
4154 let mut selection_state = WindowSelectionState::default();
4155 let participant = FakeParticipant::new("stale", cx);
4156 participant.register(
4157 &mut selection_state,
4158 0.,
4159 TextSelectionScopeId::default(),
4160 0,
4161 cx,
4162 );
4163 selection_state.begin(point(px(1.), px(1.)), false, cx);
4164 selection_state.update(point(px(8.), px(1.)), cx);
4165 selection_state.end(cx);
4166
4167 selection_state.finish_frame(cx);
4168 selection_state.finish_frame(cx);
4169 assert_eq!(selection_state.selected_text(cx), "");
4170 assert!(participant.selection.snapshot(cx).is_none());
4171 });
4172 }
4173
4174 #[gpui::test]
4175 fn clear_stops_anchor_auto_scroll_before_discarding_the_anchor(cx: &mut TestAppContext) {
4176 let commands = Rc::new(RefCell::new(Vec::new()));
4177 let observed = commands.clone();
4178 let (mut selection_state, participant) = cx.update(|cx| {
4179 let selection_state = WindowSelectionState::default();
4180 let participant = FakeParticipant::new("scroll", cx);
4181 participant
4182 .selection
4183 .subscribe(
4184 move |event, _| {
4185 if let TextSelectionEvent::AutoScroll(delta) = event {
4186 observed.borrow_mut().push(*delta);
4187 }
4188 },
4189 cx,
4190 )
4191 .detach();
4192 (selection_state, participant)
4193 });
4194 cx.run_until_parked();
4195 cx.update(|cx| {
4196 participant.register(
4197 &mut selection_state,
4198 0.,
4199 TextSelectionScopeId::default(),
4200 0,
4201 cx,
4202 );
4203
4204 selection_state.begin(point(px(1.), px(1.)), false, cx);
4205 selection_state.update(point(px(1.), px(25.)), cx);
4206 selection_state.clear(cx);
4207 });
4208 cx.run_until_parked();
4209 assert!(commands.borrow().iter().any(Option::is_some));
4210 assert_eq!(commands.borrow().last(), Some(&None));
4211 }
4212
4213 #[gpui::test]
4214 fn drag_auto_scroll_stops_when_the_content_mask_collapses(cx: &mut TestAppContext) {
4215 let window = cx.add_window(|_, cx| WindowSelectionView {
4216 selection: TextSelectionHandle::new("unused", cx),
4217 });
4218 window
4219 .update(cx, |_, window, cx| {
4220 let state = cx.new(|_| WindowSelectionState::default());
4221 let participant = FakeParticipant::new("participant", cx);
4222 state.update(cx, |state, cx| {
4223 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
4224 state.begin(point(px(1.), px(1.)), false, cx);
4225 });
4226 let collapsed = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(0.)));
4229 state.update(cx, |state, cx| {
4230 state.register_participant(
4231 participant.selection.clone(),
4232 TextSelectionRegistration::new(
4233 Hitbox {
4234 id: HitboxId::placeholder(),
4235 bounds: collapsed,
4236 content_mask: ContentMask { bounds: collapsed },
4237 behavior: HitboxBehavior::Normal,
4238 },
4239 collapsed,
4240 )
4241 .with_text_bounds(vec![collapsed]),
4242 cx,
4243 );
4244 state.update_in_window(point(px(1.), px(50.)), window, cx);
4245 assert!(!state.auto_scroll.is_active());
4246 assert!(state.auto_scroll.last_drag_position.is_none());
4247 });
4248 })
4249 .unwrap();
4250 }
4251
4252 #[gpui::test]
4253 fn pointer_moves_after_a_click_do_not_auto_scroll(cx: &mut TestAppContext) {
4254 let window = cx.add_window(|_, cx| WindowSelectionView {
4255 selection: TextSelectionHandle::new("unused", cx),
4256 });
4257 window
4258 .update(cx, |_, window, cx| {
4259 let state = cx.new(|_| WindowSelectionState::default());
4260 let participant = FakeParticipant::new("participant", cx);
4261 state.update(cx, |state, cx| {
4262 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
4263 state.begin(point(px(1.), px(1.)), false, cx);
4265 state.end(cx);
4266 assert!(state.anchor.is_some());
4267
4268 state.update_in_window(point(px(1.), px(50.)), window, cx);
4269 assert!(!state.auto_scroll.is_active());
4270 });
4271 })
4272 .unwrap();
4273 }
4274
4275 #[gpui::test]
4276 fn proxy_endpoints_break_equal_position_ties_by_document_order(cx: &mut TestAppContext) {
4277 cx.update(|cx| {
4278 let mut selection_state = WindowSelectionState::default();
4279 let later = FakeParticipant::new("later", cx);
4280 let earlier = FakeParticipant::new("earlier", cx);
4281 later.register(
4282 &mut selection_state,
4283 0.,
4284 TextSelectionScopeId::default(),
4285 2,
4286 cx,
4287 );
4288 earlier.register(
4289 &mut selection_state,
4290 0.,
4291 TextSelectionScopeId::default(),
4292 1,
4293 cx,
4294 );
4295
4296 selection_state.begin(point(px(1.), px(1.)), false, cx);
4297 selection_state.update(point(px(200.), px(25.)), cx);
4298 let endpoint = selection_state.snapshot().unwrap().cursor();
4299
4300 assert_eq!(endpoint.entity_id(), Some(earlier.selection.entity_id()));
4301 });
4302 }
4303
4304 #[gpui::test]
4305 fn equal_area_hovered_participants_break_ties_by_document_order(cx: &mut TestAppContext) {
4306 cx.update(|cx| {
4307 for _ in 0..64 {
4308 let mut selection_state = WindowSelectionState::default();
4309 let later = FakeParticipant::new("later", cx);
4310 let earliest = FakeParticipant::new("earliest", cx);
4311 let middle = FakeParticipant::new("middle", cx);
4312 later.register(
4313 &mut selection_state,
4314 0.,
4315 TextSelectionScopeId::default(),
4316 30,
4317 cx,
4318 );
4319 earliest.register(
4320 &mut selection_state,
4321 0.,
4322 TextSelectionScopeId::default(),
4323 10,
4324 cx,
4325 );
4326 middle.register(
4327 &mut selection_state,
4328 0.,
4329 TextSelectionScopeId::default(),
4330 20,
4331 cx,
4332 );
4333
4334 selection_state.begin(point(px(1.), px(1.)), false, cx);
4335 selection_state.update(point(px(8.), px(1.)), cx);
4336
4337 assert_eq!(
4338 selection_state.snapshot().unwrap().anchor().entity_id(),
4339 Some(earliest.selection.entity_id())
4340 );
4341 }
4342 });
4343 }
4344
4345 #[gpui::test]
4346 fn text_selection_namespace_is_a_safe_no_op_until_the_element_is_rendered(
4347 cx: &mut TestAppContext,
4348 ) {
4349 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
4350 selection: TextSelectionHandle::new("not enabled", cx),
4351 });
4352 cx.update(|window, cx| {
4353 assert!(!TextSelection::has_selection(window, cx));
4354 assert_eq!(TextSelection::selected_text(window, cx), "");
4355 TextSelection::clear(window, cx);
4356 TextSelection::end(window, cx);
4357 assert!(!TextSelection::has_selection(window, cx));
4358 });
4359 }
4360
4361 #[gpui::test]
4362 fn unit_selection_element_supports_scope_and_registration_on_the_first_frame(
4363 cx: &mut TestAppContext,
4364 ) {
4365 let (view, cx) = cx.add_window_view(|_, cx| FirstFrameScopedSelectionView {
4366 selection: TextSelectionHandle::new("first frame", cx),
4367 });
4368 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
4369
4370 cx.update(|window, cx| {
4371 let _ = window.draw(cx);
4372 let state = WindowSelectionState::existing(window, cx).unwrap();
4373 assert_eq!(
4374 state.read(cx).active_scope,
4375 TextSelectionScopeId::from_raw(23)
4376 );
4377 assert!(
4378 state
4379 .read(cx)
4380 .participants
4381 .borrow()
4382 .contains_key(&selection.entity_id())
4383 );
4384 });
4385 }
4386
4387 #[gpui::test]
4388 fn lazy_registration_does_not_enable_queries_without_the_element(cx: &mut TestAppContext) {
4389 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
4390 selection: TextSelectionHandle::new("registered", cx),
4391 });
4392 cx.update(|window, cx| {
4393 let selection = TextSelectionHandle::new("registered", cx);
4394 selection.set_local_selection(true, cx);
4395 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(20.)));
4396 let hitbox = Hitbox {
4397 id: HitboxId::placeholder(),
4398 bounds,
4399 content_mask: ContentMask { bounds },
4400 behavior: HitboxBehavior::Normal,
4401 };
4402 selection.register(
4403 TextSelectionRegistration::new(hitbox, bounds).with_text_bounds(vec![bounds]),
4404 window,
4405 cx,
4406 );
4407 assert_eq!(TextSelection::selected_text(window, cx), "");
4408 assert!(!TextSelection::has_selection(window, cx));
4409 TextSelection::clear(window, cx);
4410 assert_eq!(TextSelection::selected_text(window, cx), "");
4411 });
4412 }
4413
4414 #[cfg(feature = "gpui-fast")]
4417 struct SelectionStateReader {
4418 renders: Rc<Cell<usize>>,
4419 }
4420
4421 #[cfg(feature = "gpui-fast")]
4422 impl Render for SelectionStateReader {
4423 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4424 self.renders.set(self.renders.get() + 1);
4425 if let Some(state) = WindowSelectionState::existing(window, cx) {
4426 let _ = state.read(cx).participants.borrow().len();
4427 }
4428 div().size_full()
4429 }
4430 }
4431
4432 #[cfg(feature = "gpui-fast")]
4435 struct ReaderBesideSelectionView {
4436 text: gpui::Entity<ToggleSelectionElementView>,
4437 reader: gpui::Entity<SelectionStateReader>,
4438 }
4439
4440 #[cfg(feature = "gpui-fast")]
4441 impl Render for ReaderBesideSelectionView {
4442 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4443 div()
4444 .size_full()
4445 .child(self.reader.clone())
4446 .child(self.text.clone())
4447 }
4448 }
4449
4450 #[cfg(feature = "gpui-fast")]
4454 #[gpui::test]
4455 fn redrawing_selectable_text_leaves_its_readers_retained(cx: &mut TestAppContext) {
4456 let renders = Rc::new(Cell::new(0));
4457 let (view, cx) = cx.add_window_view(|_, cx| ReaderBesideSelectionView {
4458 text: cx.new(|cx| ToggleSelectionElementView {
4459 enabled: true,
4460 selection: TextSelectionHandle::new("text", cx),
4461 }),
4462 reader: cx.new(|_| SelectionStateReader {
4463 renders: renders.clone(),
4464 }),
4465 });
4466 let text = cx.update(|_, cx| view.read(cx).text.clone());
4467 let redraw = |cx: &mut gpui::VisualTestContext| {
4468 text.update(cx, |_, cx| cx.notify());
4469 cx.update(|window, cx| {
4470 let _ = window.draw(cx);
4471 window.simulate_next_frame(cx);
4472 });
4473 cx.run_until_parked();
4474 };
4475 redraw(cx);
4476 redraw(cx);
4477 let before = renders.get();
4478 for _ in 0..4 {
4479 redraw(cx);
4480 }
4481 assert_eq!(renders.get(), before, "the reader rendered again");
4482 }
4483
4484 #[gpui::test]
4485 fn retained_selection_state_releases_and_does_not_resurrect_selection(cx: &mut TestAppContext) {
4486 let (view, cx) = cx.add_window_view(|_, cx| ToggleSelectionElementView {
4487 enabled: true,
4488 selection: TextSelectionHandle::new("local", cx),
4489 });
4490 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
4491 cx.update(|window, cx| {
4492 let _ = window.draw(cx);
4493 selection.set_local_selection(true, cx);
4494 assert!(TextSelection::has_selection(window, cx));
4495
4496 window.simulate_next_frame(cx);
4497 assert!(TextSelection::has_selection(window, cx));
4498 let _ = window.draw(cx);
4499 assert!(TextSelection::has_selection(window, cx));
4500 });
4501 view.update(cx, |view, cx| {
4502 view.enabled = false;
4503 cx.notify();
4504 });
4505 cx.update(|window, cx| {
4506 let _ = window.draw(cx);
4507 });
4508 cx.update(|window, cx| {
4509 window.simulate_next_frame(cx);
4510 });
4511 cx.update(|window, cx| {
4512 window.simulate_next_frame(cx);
4513 });
4514 cx.run_until_parked();
4515 cx.update(|window, cx| {
4516 assert!(!TextSelection::has_selection(window, cx));
4517 assert_eq!(TextSelection::selected_text(window, cx), "");
4518 assert!(!selection.has_local_selection(cx));
4519 TextSelection::clear(window, cx);
4520 });
4521
4522 view.update(cx, |view, cx| {
4523 view.enabled = true;
4524 cx.notify();
4525 });
4526 cx.update(|window, cx| {
4527 let _ = window.draw(cx);
4528 assert!(!TextSelection::has_selection(window, cx));
4529 assert_eq!(TextSelection::selected_text(window, cx), "");
4530 });
4531 }
4532
4533 #[gpui::test]
4534 fn mounted_selection_element_does_not_keep_an_idle_frame_queue_alive(cx: &mut TestAppContext) {
4535 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
4536 cx.update(|window, cx| {
4537 let _ = window.draw(cx);
4538 assert_eq!(window.simulate_next_frame(cx), 0);
4539 assert_eq!(window.simulate_next_frame(cx), 0);
4540 assert!(live_text_selection_state(window, cx).is_some());
4541 });
4542 }
4543
4544 #[gpui::test]
4545 fn selection_element_initializes_suppression_and_respects_bubble_suppression(
4546 cx: &mut TestAppContext,
4547 ) {
4548 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
4549 cx.update(|window, cx| {
4550 let _ = window.draw(cx);
4551 });
4552 cx.simulate_mouse_down(
4553 point(px(1.), px(1.)),
4554 MouseButton::Left,
4555 gpui::Modifiers::default(),
4556 );
4557 cx.simulate_mouse_up(
4558 point(px(1.), px(1.)),
4559 MouseButton::Left,
4560 gpui::Modifiers::default(),
4561 );
4562 cx.update(|window, cx| {
4563 assert!(GlobalState::is_text_selection_suppressed(cx));
4564 assert!(!TextSelection::has_selection(window, cx));
4565 });
4566 }
4567
4568 #[gpui::test]
4569 fn frame_sweep_keeps_a_participant_registered_before_the_selection_element_paints(
4570 cx: &mut TestAppContext,
4571 ) {
4572 cx.update(|cx| {
4573 let mut selection_state = WindowSelectionState::default();
4574 let participant = FakeParticipant::new("painted first", cx);
4575 participant.register(
4576 &mut selection_state,
4577 0.,
4578 TextSelectionScopeId::default(),
4579 0,
4580 cx,
4581 );
4582 selection_state.begin(point(px(1.), px(1.)), false, cx);
4583 selection_state.update(point(px(8.), px(1.)), cx);
4584 selection_state.end(cx);
4585
4586 selection_state.finish_frame(cx);
4587
4588 assert_eq!(selection_state.selected_text(cx), "painted first");
4589 assert!(participant.selection.snapshot(cx).is_some());
4590 });
4591 }
4592
4593 #[gpui::test]
4594 fn two_selection_elements_schedule_only_one_post_frame_sweep(cx: &mut TestAppContext) {
4595 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
4596 selection: TextSelectionHandle::new("once", cx),
4597 });
4598 cx.update(|window, cx| {
4599 let selection_state = WindowSelectionState::ensure(window, cx);
4600 let selection = view.read(cx).selection.clone();
4601 selection_state.update(cx, |selection_state, cx| {
4602 FakeParticipant { selection }.register(
4603 selection_state,
4604 0.,
4605 TextSelectionScopeId::default(),
4606 0,
4607 cx,
4608 );
4609 selection_state.begin(point(px(1.), px(1.)), false, cx);
4610 selection_state.update(point(px(8.), px(1.)), cx);
4611 selection_state.end(cx);
4612 });
4613
4614 let _ = window.draw(cx);
4615 window.simulate_next_frame(cx);
4616
4617 let items = selection_state.read(cx).copy_items(cx);
4618 assert_eq!(resolve_copy_items(items, cx), "once");
4619 });
4620 }
4621
4622 #[gpui::test]
4623 fn duplicate_selection_elements_gate_real_pointer_gestures_and_reentrant_clear(
4624 cx: &mut TestAppContext,
4625 ) {
4626 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
4627 selection: TextSelectionHandle::new("once", cx),
4628 });
4629 let clear_count = Rc::new(Cell::new(0));
4630 cx.update(|window, cx| {
4631 let state = WindowSelectionState::ensure(window, cx);
4632 let state_for_clear = state.clone();
4633 let count = clear_count.clone();
4634 let selection = view.read(cx).selection.clone();
4635 selection
4636 .subscribe(
4637 move |event, cx| {
4638 if matches!(event, TextSelectionEvent::Cleared) {
4639 count.set(count.get() + 1);
4640 let _ = state_for_clear.read(cx).snapshot();
4641 }
4642 },
4643 cx,
4644 )
4645 .detach();
4646 let _ = window.draw(cx);
4647 });
4648
4649 cx.simulate_mouse_down(
4650 point(px(10.), px(10.)),
4651 MouseButton::Left,
4652 gpui::Modifiers::default(),
4653 );
4654 cx.simulate_mouse_up(
4655 point(px(10.), px(10.)),
4656 MouseButton::Left,
4657 gpui::Modifiers::default(),
4658 );
4659 cx.simulate_mouse_down(
4660 point(px(70.), px(10.)),
4661 MouseButton::Left,
4662 gpui::Modifiers {
4663 shift: true,
4664 ..Default::default()
4665 },
4666 );
4667 cx.simulate_mouse_up(
4668 point(px(70.), px(10.)),
4669 MouseButton::Left,
4670 gpui::Modifiers::default(),
4671 );
4672 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
4673
4674 cx.simulate_mouse_down(
4675 point(px(15.), px(10.)),
4676 MouseButton::Left,
4677 gpui::Modifiers::default(),
4678 );
4679 cx.simulate_mouse_move(
4680 point(px(85.), px(10.)),
4681 Some(MouseButton::Left),
4682 gpui::Modifiers::default(),
4683 );
4684 cx.simulate_mouse_up(
4685 point(px(85.), px(10.)),
4686 MouseButton::Left,
4687 gpui::Modifiers::default(),
4688 );
4689 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
4690 assert_eq!(clear_count.get(), 3);
4691 }
4692
4693 #[gpui::test]
4694 fn selection_layer_handles_real_double_and_triple_click_events(cx: &mut TestAppContext) {
4695 let (text, layout) = laid_out_runs(&["alpha beta"], cx).pop().unwrap();
4696 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
4697 selection: TextSelectionHandle::new("", cx),
4698 });
4699 cx.update(|window, cx| {
4700 let _ = window.draw(cx);
4701 let selection = view.read(cx).selection.clone();
4702 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(17)), cx);
4703 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
4704 });
4705
4706 let position = layout.position_for_index(7).unwrap();
4707 cx.simulate_event(MouseDownEvent {
4708 position,
4709 modifiers: gpui::Modifiers::default(),
4710 button: MouseButton::Left,
4711 click_count: 2,
4712 first_mouse: false,
4713 });
4714 cx.simulate_event(MouseUpEvent {
4715 position,
4716 modifiers: gpui::Modifiers::default(),
4717 button: MouseButton::Left,
4718 click_count: 2,
4719 });
4720 cx.update(|window, cx| {
4721 let selection = view.read(cx).selection.clone();
4722 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
4723 assert_eq!(TextSelection::selected_text(window, cx), "beta");
4724 let snapshot = selection.snapshot(cx).unwrap();
4725 assert_eq!(
4726 snapshot.anchor().content_key(),
4727 Some(TextSelectionContentKey::new(17))
4728 );
4729 assert_eq!(
4730 snapshot.cursor().content_key(),
4731 Some(TextSelectionContentKey::new(17))
4732 );
4733 });
4734
4735 cx.simulate_event(MouseDownEvent {
4736 position,
4737 modifiers: gpui::Modifiers::default(),
4738 button: MouseButton::Left,
4739 click_count: 3,
4740 first_mouse: false,
4741 });
4742 cx.simulate_event(MouseUpEvent {
4743 position,
4744 modifiers: gpui::Modifiers::default(),
4745 button: MouseButton::Left,
4746 click_count: 3,
4747 });
4748 cx.update(|window, cx| {
4749 let selection = view.read(cx).selection.clone();
4750 selection.update_runs(&[text_run(0, text, layout)], cx);
4751 assert_eq!(TextSelection::selected_text(window, cx), "alpha beta");
4752 });
4753 }
4754}