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