1use std::{
2 cell::{Cell, RefCell},
3 collections::HashMap,
4 ops::Range,
5 rc::Rc,
6 sync::atomic::{AtomicU64, Ordering},
7};
8
9use gpui::{
10 App, AppContext as _, Bounds, Context, Element, ElementId, Entity, EntityId, EventEmitter,
11 Global, GlobalElementId, Half, Hitbox, HitboxBehavior, Hsla, InputEvent as _,
12 InspectorElementId, IntoElement, LayoutId, LongPressEvent, MouseButton, MouseDownEvent,
13 MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollDelta, ScrollEvent, SharedString, Style,
14 Subscription, TextLayout, TouchDragEvent, TouchPhase, WeakEntity, Window, point, px,
15};
16
17use crate::text_boundary::{line_range_at, word_range_at};
18use crate::touch_selection::{
19 EdgeDrag, SelectionEdge, TouchHandle, TouchSelectionSnapshot, caret_in_view, caret_line_box,
20};
21use crate::{AutoScroll, GlobalState};
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
25pub struct TextSelectionScopeId(u64);
26
27impl TextSelectionScopeId {
28 pub fn new() -> Self {
33 static NEXT_SCOPE_ID: AtomicU64 = AtomicU64::new(1);
34 let value = NEXT_SCOPE_ID
35 .try_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
36 value.checked_add(1)
37 })
38 .expect("text selection scope identifiers exhausted");
39 Self(value)
40 }
41
42 #[cfg(test)]
43 const fn from_raw(value: u64) -> Self {
44 Self(value)
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct TextSelectionContentKey(u64);
51
52impl TextSelectionContentKey {
53 pub const fn new(value: u64) -> Self {
55 Self(value)
56 }
57
58 pub const fn value(self) -> u64 {
60 self.0
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct TextSelectionEndpoint {
67 entity_id: Option<EntityId>,
68 point: Point<Pixels>,
69 content_key: Option<TextSelectionContentKey>,
70}
71
72impl TextSelectionEndpoint {
73 pub(crate) const fn new(entity_id: Option<EntityId>, point: Point<Pixels>) -> Self {
75 Self {
76 entity_id,
77 point,
78 content_key: None,
79 }
80 }
81
82 pub(crate) const fn with_content_key(mut self, content_key: TextSelectionContentKey) -> Self {
84 self.content_key = Some(content_key);
85 self
86 }
87
88 pub const fn entity_id(&self) -> Option<EntityId> {
90 self.entity_id
91 }
92
93 pub const fn content_point(&self) -> Point<Pixels> {
95 self.point
96 }
97
98 pub const fn content_key(&self) -> Option<TextSelectionContentKey> {
100 self.content_key
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct TextSelectionWindowPoints {
107 anchor: Point<Pixels>,
108 cursor: Point<Pixels>,
109}
110
111impl TextSelectionWindowPoints {
112 pub const fn anchor(&self) -> Point<Pixels> {
114 self.anchor
115 }
116
117 pub const fn cursor(&self) -> Point<Pixels> {
119 self.cursor
120 }
121}
122
123#[derive(Clone, Copy, Debug, PartialEq)]
125pub struct TextSelectionSnapshot {
126 anchor: TextSelectionEndpoint,
127 cursor: TextSelectionEndpoint,
128 is_selecting: bool,
129 window_points: Option<TextSelectionWindowPoints>,
130 coverage: TextSelectionCoverage,
131}
132
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
135pub enum TextSelectionCoverage {
136 #[default]
138 Bounded,
139 FromStart,
141 ToEnd,
143 Full,
145}
146
147impl TextSelectionSnapshot {
148 pub(crate) const fn new(anchor: TextSelectionEndpoint, cursor: TextSelectionEndpoint) -> Self {
150 Self {
151 anchor,
152 cursor,
153 is_selecting: false,
154 window_points: None,
155 coverage: TextSelectionCoverage::Bounded,
156 }
157 }
158
159 pub(crate) const fn with_selecting(mut self, is_selecting: bool) -> Self {
161 self.is_selecting = is_selecting;
162 self
163 }
164
165 pub(crate) const fn with_window_points(
167 mut self,
168 window_points: Option<TextSelectionWindowPoints>,
169 ) -> Self {
170 self.window_points = window_points;
171 self
172 }
173
174 #[cfg(test)]
176 pub(crate) const fn with_coverage(mut self, coverage: TextSelectionCoverage) -> Self {
177 self.coverage = coverage;
178 self
179 }
180
181 pub const fn anchor(&self) -> TextSelectionEndpoint {
183 self.anchor
184 }
185
186 pub const fn cursor(&self) -> TextSelectionEndpoint {
188 self.cursor
189 }
190
191 pub const fn is_selecting(&self) -> bool {
193 self.is_selecting
194 }
195
196 pub const fn window_points(&self) -> Option<TextSelectionWindowPoints> {
198 self.window_points
199 }
200
201 pub const fn coverage(&self) -> TextSelectionCoverage {
203 self.coverage
204 }
205}
206
207struct RenderedMarker;
215
216pub struct TextSelectionRegistration {
218 hitbox: Hitbox,
219 bounds: Bounds<Pixels>,
220 scroll_offset: Point<Pixels>,
221 scope: TextSelectionScopeId,
222 document_order: u64,
223 paint_order: bool,
224 text_bounds: Vec<Bounds<Pixels>>,
225 self_scroll: bool,
226 selection_edges: Option<(Bounds<Pixels>, Bounds<Pixels>)>,
227 rendered: Option<WeakEntity<RenderedMarker>>,
228}
229
230impl TextSelectionRegistration {
231 pub fn new(hitbox: Hitbox, bounds: Bounds<Pixels>) -> Self {
233 Self {
234 hitbox,
235 bounds,
236 scroll_offset: Point::default(),
237 scope: TextSelectionScopeId::default(),
238 document_order: 0,
239 paint_order: false,
240 text_bounds: Vec::new(),
241 self_scroll: false,
242 selection_edges: None,
243 rendered: None,
244 }
245 }
246
247 pub fn with_rendered_element(
254 mut self,
255 participant: &TextSelectionHandle,
256 window: &mut Window,
257 cx: &mut App,
258 ) -> Self {
259 self.rendered = Some(
260 window
261 .use_keyed_state(
262 ElementId::NamedInteger(
263 "text-selection-participant".into(),
264 participant.entity_id().as_u64(),
265 ),
266 cx,
267 |_, _| RenderedMarker,
268 )
269 .downgrade(),
270 );
271 self
272 }
273
274 fn is_rendered(&self) -> bool {
277 self.rendered
278 .as_ref()
279 .is_some_and(|marker| marker.upgrade().is_some())
280 }
281
282 pub(crate) fn with_self_scroll(mut self, self_scroll: bool) -> Self {
287 self.self_scroll = self_scroll;
288 self
289 }
290
291 pub fn with_scroll_offset(mut self, scroll_offset: Point<Pixels>) -> Self {
293 self.scroll_offset = scroll_offset;
294 self
295 }
296
297 pub fn with_scope(mut self, scope: TextSelectionScopeId) -> Self {
299 self.scope = scope;
300 self
301 }
302
303 pub fn with_document_order(mut self, document_order: u64) -> Self {
305 self.document_order = document_order;
306 self
307 }
308
309 pub(crate) fn in_paint_order(mut self) -> Self {
312 self.paint_order = true;
313 self
314 }
315
316 pub fn with_text_bounds(mut self, text_bounds: Vec<Bounds<Pixels>>) -> Self {
318 self.text_bounds = text_bounds;
319 self
320 }
321
322 pub fn with_selection_edges(mut self, start: Bounds<Pixels>, end: Bounds<Pixels>) -> Self {
326 self.selection_edges = Some((start, end));
327 self
328 }
329
330 pub fn hitbox(&self) -> &Hitbox {
332 &self.hitbox
333 }
334
335 pub const fn bounds(&self) -> Bounds<Pixels> {
337 self.bounds
338 }
339
340 pub const fn scroll_offset(&self) -> Point<Pixels> {
342 self.scroll_offset
343 }
344
345 pub const fn scope(&self) -> TextSelectionScopeId {
347 self.scope
348 }
349
350 pub const fn document_order(&self) -> u64 {
352 self.document_order
353 }
354
355 pub fn text_bounds(&self) -> &[Bounds<Pixels>] {
357 &self.text_bounds
358 }
359
360 pub const fn selection_edges(&self) -> Option<(Bounds<Pixels>, Bounds<Pixels>)> {
362 self.selection_edges
363 }
364}
365
366#[derive(Clone)]
368pub struct TextSelectionRun {
369 document_order: u64,
371 text: SharedString,
373 layout: TextLayout,
375 bounds: Bounds<Pixels>,
377 triple_click: TripleClickSelection,
379}
380
381#[derive(Clone, Copy)]
382enum TripleClickSelection {
383 LogicalLine,
384 Paragraph,
385 Row(Bounds<Pixels>),
386}
387
388impl TextSelectionRun {
389 pub fn new(text: impl Into<SharedString>, layout: TextLayout, bounds: Bounds<Pixels>) -> Self {
391 Self {
392 document_order: 0,
393 text: text.into(),
394 layout,
395 bounds,
396 triple_click: TripleClickSelection::LogicalLine,
397 }
398 }
399
400 pub const fn with_document_order(mut self, document_order: u64) -> Self {
402 self.document_order = document_order;
403 self
404 }
405
406 pub(crate) const fn with_paragraph(mut self, row: Option<Bounds<Pixels>>) -> Self {
409 self.triple_click = match row {
410 Some(row) => TripleClickSelection::Row(row),
411 None => TripleClickSelection::Paragraph,
412 };
413 self
414 }
415
416 pub const fn document_order(&self) -> u64 {
418 self.document_order
419 }
420
421 pub fn text(&self) -> &SharedString {
423 &self.text
424 }
425
426 pub fn layout(&self) -> &TextLayout {
428 &self.layout
429 }
430
431 pub const fn bounds(&self) -> Bounds<Pixels> {
433 self.bounds
434 }
435}
436
437#[derive(Clone, Debug, Default, PartialEq, Eq)]
439pub struct TextSelectionProjection {
440 ranges: Vec<Option<Range<usize>>>,
442 is_active: bool,
444}
445
446impl TextSelectionProjection {
447 pub fn ranges(&self) -> &[Option<Range<usize>>] {
449 &self.ranges
450 }
451
452 pub const fn is_active(&self) -> bool {
454 self.is_active
455 }
456}
457
458fn project_ranges(
464 snapshot: Option<TextSelectionSnapshot>,
465 runs: &[TextSelectionRun],
466) -> TextSelectionProjection {
467 let Some(snapshot) = snapshot else {
468 return TextSelectionProjection {
469 ranges: vec![None; runs.len()],
470 is_active: false,
471 };
472 };
473 let Some(window_points) = snapshot.window_points() else {
474 return TextSelectionProjection {
475 ranges: vec![None; runs.len()],
476 is_active: true,
477 };
478 };
479
480 TextSelectionProjection {
481 ranges: runs
482 .iter()
483 .map(|run| selection_range_for_run(run, window_points.anchor, window_points.cursor))
484 .collect(),
485 is_active: true,
486 }
487}
488
489fn selection_range_for_run(
490 run: &TextSelectionRun,
491 selection_start: Point<Pixels>,
492 selection_end: Point<Pixels>,
493) -> Option<Range<usize>> {
494 if run.text.len() != run.layout.len() {
495 return None;
496 }
497
498 if run.text.is_empty() {
499 return None;
500 }
501
502 let line_height = run.layout.line_height();
503 let (rows_top, rows_bottom) = text_rows_extent(&run.layout, line_height);
508 let band_top = selection_start.y.min(selection_end.y);
509 let band_bottom = selection_start.y.max(selection_end.y);
510 if rows_bottom <= band_top || rows_top > band_bottom {
511 return None;
512 }
513 if band_top < rows_top && band_bottom >= rows_bottom {
514 return Some(0..run.text.len());
515 }
516
517 let mut range = None;
518 for (offset, character) in run.text.char_indices() {
519 let next_offset = offset + character.len_utf8();
520 let Some(position) = run.layout.position_for_index(offset) else {
521 continue;
522 };
523
524 let char_width = run
525 .layout
526 .position_for_index(next_offset)
527 .filter(|next| next.y == position.y)
528 .map_or_else(|| line_height.half(), |next| next.x - position.x);
529
530 if point_in_selection_band(
531 position,
532 char_width,
533 selection_start,
534 selection_end,
535 line_height,
536 ) {
537 range.get_or_insert(offset..offset).end = next_offset;
538 }
539 }
540 range
541}
542
543pub(crate) fn text_rows_extent(text_layout: &TextLayout, line_height: Pixels) -> (Pixels, Pixels) {
552 let top = text_layout.bounds().top();
553 let layout_line_height = text_layout.line_height();
554 let lines = text_layout.line_layouts();
555 let mut last_line_top = top;
556 for line in lines.iter().take(lines.len().saturating_sub(1)) {
557 last_line_top += line.size(layout_line_height).height;
558 }
559 let last_row_top = lines.last().map_or(top, |line| {
560 last_line_top + line.wrap_boundaries.len() as f32 * layout_line_height
561 });
562 (top, last_row_top + line_height)
563}
564
565fn carets_for_multi_click(
568 runs: &[TextSelectionRun],
569 position: Point<Pixels>,
570 click_count: usize,
571) -> Option<(Bounds<Pixels>, Bounds<Pixels>)> {
572 let run = runs.iter().find(|run| run.bounds.contains(&position))?;
573 if run.text.len() != run.layout.len() {
574 return None;
575 }
576 let offset = run.layout.index_for_position(position).ok()?;
577 unit_carets(run, offset, click_count)
578}
579
580fn unit_carets(
583 run: &TextSelectionRun,
584 offset: usize,
585 click_count: usize,
586) -> Option<(Bounds<Pixels>, Bounds<Pixels>)> {
587 let range = match click_count {
588 2 => word_range_at(&run.text, offset)?,
589 3.. => match run.triple_click {
590 TripleClickSelection::LogicalLine => line_range_at(&run.text, offset),
591 TripleClickSelection::Paragraph => 0..run.text.len(),
592 TripleClickSelection::Row(row) => {
593 return Some((
594 caret_line_box(row.origin, row.size.height),
595 caret_line_box(row.top_right(), row.size.height),
596 ));
597 }
598 },
599 _ => return None,
600 };
601 if range.is_empty() {
602 return None;
603 }
604 let line_height = run.layout.line_height();
605 Some((
606 caret_line_box(run.layout.position_for_index(range.start)?, line_height),
607 caret_line_box(run.layout.position_for_index(range.end)?, line_height),
608 ))
609}
610
611fn point_in_selection_band(
612 position: Point<Pixels>,
613 char_width: Pixels,
614 selection_start: Point<Pixels>,
615 selection_end: Point<Pixels>,
616 line_height: Pixels,
617) -> bool {
618 let point_in_line =
619 |point: Point<Pixels>| point.y >= position.y && point.y < position.y + line_height;
620 let top = selection_start.y.min(selection_end.y);
621 let bottom = selection_start.y.max(selection_end.y);
622 let x = position.x + char_width.half();
623
624 if position.y + line_height <= top || position.y > bottom {
625 return false;
626 }
627
628 if point_in_line(selection_start) && point_in_line(selection_end) {
629 let left = selection_start.x.min(selection_end.x);
630 let right = selection_start.x.max(selection_end.x);
631 return x >= left && x <= right;
632 }
633
634 let (top_point, bottom_point) = if selection_start.y < selection_end.y {
635 (selection_start, selection_end)
636 } else {
637 (selection_end, selection_start)
638 };
639 if point_in_line(top_point) {
640 x >= top_point.x
641 } else if point_in_line(bottom_point) {
642 x <= bottom_point.x
643 } else {
644 true
645 }
646}
647
648type FocusCallback = Rc<dyn Fn(&mut Window, &mut App)>;
649type ClearHandler = Rc<dyn Fn(&mut App)>;
650type CopyCallback = Rc<dyn Fn(&mut App) -> String>;
651type ContentKeyResolver = Rc<dyn Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey>>;
652
653#[derive(Clone, Copy, Debug, PartialEq)]
655pub enum TextSelectionEvent {
656 SelectionChanged(Option<TextSelectionSnapshot>),
658 AutoScroll(Option<Pixels>),
660 Cleared,
662 TouchSelectionChanged,
667}
668
669struct CopyItem {
670 document_order: u64,
671 callback: Option<CopyCallback>,
672 fallback: String,
673}
674
675fn resolve_copy_items(mut items: Vec<CopyItem>, cx: &mut App) -> String {
676 items.sort_by_key(|item| item.document_order);
677 items
678 .into_iter()
679 .map(|item| {
680 item.callback
681 .map(|callback| callback(cx))
682 .unwrap_or(item.fallback)
683 })
684 .filter(|text| !text.trim().is_empty())
685 .collect::<Vec<_>>()
686 .join("\n")
687}
688
689fn dispatch_clear_handlers(handlers: Vec<ClearHandler>, cx: &mut App) {
690 for handler in handlers {
691 handler(cx);
692 }
693}
694
695fn clear_departed_participants(
696 participants: Vec<Entity<SelectableTextState>>,
697 cx: &mut App,
698) -> Vec<ClearHandler> {
699 participants
700 .into_iter()
701 .filter_map(|participant| {
702 participant
703 .read(cx)
704 .has_selection_to_clear()
705 .then(|| participant.update(cx, |state, cx| state.clear_state(cx)))
706 .flatten()
707 })
708 .collect()
709}
710
711struct SelectableTextState {
712 fallback_copy_text: String,
713 window_selection: RefCell<Option<WeakEntity<WindowSelectionState>>>,
714 projected_copy_text: RefCell<Option<String>>,
717 runs: RefCell<Vec<TextSelectionRun>>,
719 local_selection: bool,
720 snapshot: Option<TextSelectionSnapshot>,
721 on_focus: Option<FocusCallback>,
722 clear: Option<ClearHandler>,
723 copy: Option<CopyCallback>,
724 content_key_resolver: Option<ContentKeyResolver>,
725}
726
727impl EventEmitter<TextSelectionEvent> for SelectableTextState {}
728
729impl SelectableTextState {
730 fn new(fallback_copy_text: impl Into<String>) -> Self {
731 Self {
732 fallback_copy_text: fallback_copy_text.into(),
733 window_selection: RefCell::new(None),
734 projected_copy_text: RefCell::new(None),
735 runs: RefCell::new(Vec::new()),
736 local_selection: false,
737 snapshot: None,
738 on_focus: None,
739 clear: None,
740 copy: None,
741 content_key_resolver: None,
742 }
743 }
744
745 fn snapshot(&self) -> Option<TextSelectionSnapshot> {
747 self.snapshot
748 }
749
750 fn set_fallback_copy_text(&mut self, text: impl Into<String>) {
752 self.fallback_copy_text = text.into();
753 *self.projected_copy_text.get_mut() = None;
754 }
755
756 fn set_local_selection(&mut self, active: bool) {
758 self.local_selection = active;
759 }
760
761 fn update_runs(&self, runs: &[TextSelectionRun]) -> TextSelectionProjection {
768 *self.runs.borrow_mut() = runs.to_vec();
769 let states = project_ranges(self.snapshot, runs);
770 let mut selected_runs = runs
771 .iter()
772 .zip(states.ranges())
773 .enumerate()
774 .filter_map(|(index, (run, state))| {
775 state.as_ref().map(|range| {
776 debug_assert!(run.text.is_char_boundary(range.start));
777 debug_assert!(run.text.is_char_boundary(range.end));
778 (
779 run.document_order,
780 index,
781 run.text[range.clone()].to_string(),
782 )
783 })
784 })
785 .collect::<Vec<_>>();
786 selected_runs.sort_by_key(|(order, index, _)| (*order, *index));
787 *self.projected_copy_text.borrow_mut() =
788 Some(selected_runs.into_iter().map(|(_, _, text)| text).collect());
789 states
790 }
791
792 fn set_focus_handler(&mut self, callback: impl Fn(&mut Window, &mut App) + 'static) {
794 self.on_focus = Some(Rc::new(callback));
795 }
796
797 fn clear_with(&mut self, callback: impl Fn(&mut App) + 'static) {
798 self.clear = Some(Rc::new(callback));
799 }
800
801 fn copy_with(&mut self, callback: impl Fn(&mut App) -> String + 'static) {
803 self.copy = Some(Rc::new(callback));
804 }
805
806 fn resolve_content_key_with(
808 &mut self,
809 callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
810 ) {
811 self.content_key_resolver = Some(Rc::new(callback));
812 }
813
814 fn set_snapshot_quietly(&mut self, snapshot: Option<TextSelectionSnapshot>) -> bool {
815 if self.snapshot == snapshot {
816 return false;
817 }
818 self.snapshot = snapshot;
819 *self.projected_copy_text.get_mut() = None;
820 true
821 }
822
823 fn set_snapshot(&mut self, snapshot: Option<TextSelectionSnapshot>, cx: &mut Context<Self>) {
824 if self.set_snapshot_quietly(snapshot) {
825 cx.emit(TextSelectionEvent::SelectionChanged(snapshot));
826 }
827 }
828
829 fn has_selection_to_clear(&self) -> bool {
832 self.snapshot.is_some() || self.local_selection
833 }
834
835 fn clear_state(&mut self, cx: &mut Context<Self>) -> Option<ClearHandler> {
836 self.snapshot = None;
837 *self.projected_copy_text.get_mut() = None;
838 self.local_selection = false;
839 cx.emit(TextSelectionEvent::Cleared);
840 cx.emit(TextSelectionEvent::SelectionChanged(None));
841 self.clear.clone()
842 }
843
844 fn set_auto_scroll(&self, delta: Option<Pixels>, cx: &mut Context<Self>) {
845 cx.emit(TextSelectionEvent::AutoScroll(delta));
846 }
847
848 fn focus(&self, window: &mut Window, cx: &mut App) {
849 if let Some(callback) = self.on_focus.clone() {
850 window.defer(cx, move |window, cx| callback(window, cx));
851 }
852 }
853
854 fn copy_item(&self, document_order: u64) -> Option<CopyItem> {
855 (self.snapshot.is_some() || self.local_selection).then(|| CopyItem {
856 document_order,
857 callback: self.copy.clone(),
858 fallback: self
859 .projected_copy_text
860 .borrow()
861 .clone()
862 .unwrap_or_else(|| self.fallback_copy_text.clone()),
863 })
864 }
865}
866
867#[derive(Default)]
869pub struct TouchHandleLayout {
870 hitboxes: Vec<(SelectionEdge, Hitbox)>,
871}
872
873#[derive(Clone)]
875pub struct TextSelectionHandle(Entity<SelectableTextState>);
876
877impl TextSelectionHandle {
878 pub fn new(fallback_copy_text: impl Into<String>, cx: &mut App) -> Self {
880 Self(cx.new(|_| SelectableTextState::new(fallback_copy_text)))
881 }
882
883 pub fn entity_id(&self) -> EntityId {
885 self.0.entity_id()
886 }
887
888 pub fn snapshot(&self, cx: &App) -> Option<TextSelectionSnapshot> {
890 self.0.read(cx).snapshot()
891 }
892
893 pub(crate) fn record_table_cell(&self, bounds: Bounds<Pixels>, cx: &App) {
895 let Some(state) = self.window_selection(cx) else {
896 return;
897 };
898 let state = state.read(cx);
899 if state.touch_select_all {
900 return;
901 }
902 let Some(snapshot) = self.snapshot(cx) else {
903 return;
904 };
905 if snapshot.anchor().entity_id() == Some(self.entity_id())
906 && snapshot
907 .window_points()
908 .is_some_and(|points| bounds.contains(&points.anchor()))
909 {
910 state
911 .table_cell_anchor
912 .set(Some((snapshot.anchor(), bounds)));
913 }
914 }
915
916 pub(crate) fn table_cell_selection(&self, cx: &App) -> Option<Bounds<Pixels>> {
918 let state = self.window_selection(cx)?;
919 let state = state.read(cx);
920 if state.touch_select_all {
921 return None;
922 }
923 let (anchor, bounds) = state.table_cell_anchor.get()?;
924 (self.snapshot(cx)?.anchor() == anchor).then_some(bounds)
925 }
926
927 pub(crate) fn clear_table_cell_selection(&self, cx: &App) {
929 if let Some(state) = self.window_selection(cx) {
930 let state = state.read(cx);
931 if state
932 .table_cell_anchor
933 .get()
934 .is_some_and(|(endpoint, _)| endpoint.entity_id() == Some(self.entity_id()))
935 {
936 state.table_cell_anchor.set(None);
937 }
938 }
939 }
940
941 fn window_selection(&self, cx: &App) -> Option<Entity<WindowSelectionState>> {
942 self.0
943 .read(cx)
944 .window_selection
945 .borrow()
946 .as_ref()?
947 .upgrade()
948 }
949
950 pub fn set_fallback_copy_text(&self, text: impl Into<String>, cx: &mut App) {
952 self.0
953 .update(cx, |state, _| state.set_fallback_copy_text(text));
954 }
955
956 pub fn set_local_selection(&self, active: bool, cx: &mut App) {
958 self.0
959 .update(cx, |state, _| state.set_local_selection(active));
960 }
961
962 pub fn has_local_selection(&self, cx: &App) -> bool {
964 self.0.read(cx).local_selection
965 }
966
967 pub fn register(
969 &self,
970 mut registration: TextSelectionRegistration,
971 window: &mut Window,
972 cx: &mut App,
973 ) {
974 if let Some(scope) = current_text_selection_scope(window.window_handle().window_id(), cx) {
975 registration.scope = scope;
976 }
977 let Some(state) = WindowSelectionState::existing(window, cx) else {
978 return;
979 };
980 *self.0.read(cx).window_selection.borrow_mut() = Some(state.downgrade());
981 state.read(cx).register_quietly(self, registration);
982 let snapshot = state.read(cx).participant_snapshot(self.entity_id());
983 if self.snapshot(cx) != snapshot {
984 self.0.update(cx, |participant, _| {
988 participant.set_snapshot_quietly(snapshot);
989 });
990 state
991 .read(cx)
992 .draw_changed
993 .borrow_mut()
994 .insert(self.entity_id(), self.downgrade());
995 }
996 schedule_selection_frame(&state, window, cx);
997 }
998
999 pub fn update_runs(&self, runs: &[TextSelectionRun], cx: &mut App) -> TextSelectionProjection {
1001 self.0.read(cx).update_runs(runs)
1002 }
1003
1004 pub(crate) fn projected_copy_text(&self, cx: &App) -> Option<String> {
1007 self.0
1008 .read(cx)
1009 .copy_item(0)
1010 .filter(|item| item.callback.is_none())
1011 .map(|item| item.fallback)
1012 }
1013
1014 pub(crate) fn set_hit_test_runs(&self, runs: &[TextSelectionRun], cx: &mut App) {
1016 *self.0.read(cx).runs.borrow_mut() = runs.to_vec();
1017 }
1018
1019 pub(crate) fn hold_multi_click(
1024 &self,
1025 start: Bounds<Pixels>,
1026 end: Bounds<Pixels>,
1027 click_count: usize,
1028 cx: &mut App,
1029 ) {
1030 let Some(state) = self.window_selection(cx) else {
1031 return;
1032 };
1033 state.update(cx, |state, cx| {
1034 if let Some(unit) = state.unit_of(&self.0, start, end, click_count, cx) {
1035 state.hold_unit(unit, cx);
1036 }
1037 });
1038 }
1039
1040 pub fn subscribe(
1042 &self,
1043 mut callback: impl FnMut(&TextSelectionEvent, &mut App) + 'static,
1044 cx: &mut App,
1045 ) -> Subscription {
1046 cx.subscribe(&self.0, move |_, event, cx| callback(event, cx))
1047 }
1048
1049 #[must_use = "retain the subscription or explicitly detach it"]
1051 pub fn refresh_window_on_change(&self, window: &Window, cx: &mut App) -> Subscription {
1052 let window = window.window_handle();
1053 self.subscribe(
1054 move |event, cx| {
1055 if matches!(event, TextSelectionEvent::SelectionChanged(_)) {
1056 _ = window.update(cx, |_, window, _| window.refresh());
1057 }
1058 },
1059 cx,
1060 )
1061 }
1062
1063 pub fn focus_with(&self, callback: impl Fn(&mut Window, &mut App) + 'static, cx: &mut App) {
1065 self.0
1066 .update(cx, |state, _| state.set_focus_handler(callback));
1067 }
1068
1069 pub fn clear_with(&self, callback: impl Fn(&mut App) + 'static, cx: &mut App) {
1071 self.0.update(cx, |state, _| state.clear_with(callback));
1072 }
1073
1074 pub fn copy_with(&self, callback: impl Fn(&mut App) -> String + 'static, cx: &mut App) {
1076 self.0.update(cx, |state, _| state.copy_with(callback));
1077 }
1078
1079 pub fn prepaint_touch_handles(&self, window: &mut Window, cx: &App) -> TouchHandleLayout {
1084 let Some(state) = WindowSelectionState::existing(window, cx) else {
1085 return TouchHandleLayout::default();
1086 };
1087 let hitboxes = state
1088 .read(cx)
1089 .touch_handles_of(self.entity_id())
1090 .into_iter()
1091 .map(|(edge, caret)| {
1092 let hitbox = window.insert_hitbox(
1093 TouchHandle::hit_bounds(edge, caret),
1094 HitboxBehavior::BlockMouse,
1095 );
1096 (edge, hitbox)
1097 })
1098 .collect();
1099 TouchHandleLayout { hitboxes }
1100 }
1101
1102 pub fn paint_touch_handles(
1110 &self,
1111 layout: &TouchHandleLayout,
1112 color: Hsla,
1113 window: &mut Window,
1114 cx: &mut App,
1115 ) {
1116 let Some(state) = WindowSelectionState::existing(window, cx) else {
1117 return;
1118 };
1119 for (edge, caret) in state.read(cx).touch_handles_of(self.entity_id()) {
1120 TouchHandle::paint(edge, caret, color, window);
1121 }
1122 let bounds: Vec<Bounds<Pixels>> = layout
1123 .hitboxes
1124 .iter()
1125 .map(|(_, hitbox)| hitbox.bounds)
1126 .collect();
1127 if !state.read(cx).handles_painted_at(self.entity_id(), &bounds) {
1128 state.update(cx, |state, _| {
1129 state.register_handles(self.entity_id(), bounds)
1130 });
1131 }
1132 for (edge, hitbox) in &layout.hitboxes {
1133 let edge = *edge;
1134 window.on_mouse_event({
1137 let hitbox = hitbox.clone();
1138 let state = state.downgrade();
1139 move |event: &TouchDragEvent, phase, window, cx| {
1140 if !phase.bubble()
1141 || event.phase != TouchPhase::Started
1142 || window.default_prevented()
1143 || !hitbox.is_hovered(window)
1144 {
1145 return;
1146 }
1147 let Some(state) = state.upgrade() else {
1148 return;
1149 };
1150 window.prevent_default();
1151 cx.stop_propagation();
1152 state.update(cx, |state, cx| {
1153 state.begin_edge_drag(edge, event.position, window, cx)
1154 });
1155 WindowSelectionState::resolve_content_keys(&state, cx);
1156 }
1157 });
1158 window.on_mouse_event({
1160 let hitbox = hitbox.clone();
1161 let state = state.downgrade();
1162 move |event: &MouseDownEvent, phase, window, cx| {
1163 if !phase.bubble()
1164 || event.button != MouseButton::Left
1165 || !hitbox.is_hovered(window)
1166 {
1167 return;
1168 }
1169 let Some(state) = state.upgrade() else {
1170 return;
1171 };
1172 cx.stop_propagation();
1173 state.update(cx, |state, cx| {
1174 state.begin_edge_drag(edge, event.position, window, cx)
1175 });
1176 WindowSelectionState::resolve_content_keys(&state, cx);
1177 }
1178 });
1179 }
1180 }
1181
1182 pub fn resolve_content_key_with(
1184 &self,
1185 callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
1186 cx: &mut App,
1187 ) {
1188 self.0
1189 .update(cx, |state, _| state.resolve_content_key_with(callback));
1190 }
1191
1192 fn downgrade(&self) -> WeakEntity<SelectableTextState> {
1193 self.0.downgrade()
1194 }
1195}
1196
1197#[derive(Clone)]
1198struct ParticipantRegistration {
1199 participant: WeakEntity<SelectableTextState>,
1200 registration: Rc<TextSelectionRegistration>,
1201 document_order: u64,
1202 generation: u64,
1203}
1204
1205#[derive(Clone)]
1206struct SelectionEndpoint {
1207 participant: Option<WeakEntity<SelectableTextState>>,
1208 point: Point<Pixels>,
1209 inside: bool,
1210 inside_text: bool,
1211 content_key: Option<TextSelectionContentKey>,
1212 content_key_resolver: Option<(ContentKeyResolver, Point<Pixels>)>,
1213}
1214
1215impl SelectionEndpoint {
1216 fn snapshot(&self) -> TextSelectionEndpoint {
1217 let snapshot = TextSelectionEndpoint::new(self.entity_id(), self.point);
1218 if let Some(content_key) = self.content_key {
1219 snapshot.with_content_key(content_key)
1220 } else {
1221 snapshot
1222 }
1223 }
1224
1225 fn resolve(
1226 &self,
1227 participants: &HashMap<EntityId, ParticipantRegistration>,
1228 ) -> Option<Point<Pixels>> {
1229 let participant = self.participant.as_ref()?;
1230 let registration = participants.get(&participant.entity_id())?;
1231 participant.upgrade()?;
1232 Some(
1233 self.point
1234 + registration.registration.scroll_offset
1235 + registration.registration.bounds.origin,
1236 )
1237 }
1238
1239 fn entity_id(&self) -> Option<EntityId> {
1240 self.participant
1241 .as_ref()
1242 .map(|participant| participant.entity_id())
1243 }
1244
1245 fn moved(&self, delta: Point<Pixels>) -> Self {
1248 let point = self.point + delta;
1249 Self {
1250 point,
1251 content_key: None,
1252 content_key_resolver: self
1253 .content_key_resolver
1254 .clone()
1255 .map(|(resolver, _)| (resolver, point)),
1256 ..self.clone()
1257 }
1258 }
1259}
1260
1261#[derive(Clone)]
1265struct SelectionUnit {
1266 click_count: usize,
1267 start: SelectionEndpoint,
1268 end: SelectionEndpoint,
1269 row_heights: (Pixels, Pixels),
1271}
1272
1273#[derive(Default)]
1279struct TouchSelection {
1280 active: bool,
1282 menu_open: bool,
1283 drag: Option<EdgeDrag>,
1284 ui_bounds: Vec<Bounds<Pixels>>,
1288 previous_ui_bounds: Vec<Bounds<Pixels>>,
1289 handle_bounds: HashMap<EntityId, Vec<Bounds<Pixels>>>,
1293 last_edges: std::cell::Cell<Option<(Bounds<Pixels>, Bounds<Pixels>, bool, bool)>>,
1298}
1299
1300impl TouchSelection {
1301 fn covers(&self, position: Point<Pixels>) -> bool {
1302 self.active
1303 && self
1304 .ui_bounds
1305 .iter()
1306 .chain(&self.previous_ui_bounds)
1307 .chain(self.handle_bounds.values().flatten())
1308 .any(|bounds| bounds.contains(&position))
1309 }
1310
1311 fn painted_any(&self) -> bool {
1314 !self.ui_bounds.is_empty() || !self.previous_ui_bounds.is_empty()
1315 }
1316
1317 fn begin_frame(&mut self) {
1319 self.previous_ui_bounds = std::mem::take(&mut self.ui_bounds);
1320 }
1321
1322 fn reset(&mut self) -> bool {
1324 let had = self.active;
1325 self.handle_bounds.clear();
1326 self.active = false;
1327 self.menu_open = false;
1328 self.drag = None;
1329 self.last_edges.set(None);
1330 had
1331 }
1332}
1333
1334#[derive(Default)]
1337struct ParticipantPaintOrder {
1338 next: u64,
1339 painted: Vec<EntityId>,
1340 last: Option<u64>,
1341 dirty: bool,
1342 refresh_requested: bool,
1343}
1344
1345#[derive(Default)]
1347struct WindowSelectionState {
1348 participants: RefCell<HashMap<EntityId, ParticipantRegistration>>,
1352 active_scope: TextSelectionScopeId,
1353 table_cell_anchor: Cell<Option<(TextSelectionEndpoint, Bounds<Pixels>)>>,
1355 touch_select_all: bool,
1356 anchor: Option<SelectionEndpoint>,
1357 cursor: Option<SelectionEndpoint>,
1358 pending_extension_anchor: Option<SelectionEndpoint>,
1359 unit: Option<SelectionUnit>,
1360 is_selecting: bool,
1361 did_hit_text: bool,
1362 frame_generation: Cell<u64>,
1363 finish_frame_scheduled: Cell<bool>,
1364 layer_painted: Cell<bool>,
1365 paint_order: RefCell<ParticipantPaintOrder>,
1366 draw_changed: RefCell<HashMap<EntityId, WeakEntity<SelectableTextState>>>,
1369 refresh_held_cursor: Cell<bool>,
1370 touch_edges_moved: Cell<bool>,
1371 #[cfg(test)]
1372 snapshot_visits: Cell<usize>,
1373 mouse_down_prepared: bool,
1374 auto_scroll: AutoScroll,
1375 auto_scroll_stall: (Option<(Bounds<Pixels>, Point<Pixels>)>, u8),
1380 touch: TouchSelection,
1381 entity_id: Option<EntityId>,
1384}
1385
1386impl WindowSelectionState {
1387 fn resolve_content_keys(state: &Entity<Self>, cx: &mut App) {
1388 let pending = state.read_with(cx, |state, _| {
1389 [
1390 state
1391 .anchor
1392 .as_ref()
1393 .and_then(|endpoint| endpoint.content_key_resolver.clone()),
1394 state
1395 .cursor
1396 .as_ref()
1397 .and_then(|endpoint| endpoint.content_key_resolver.clone()),
1398 ]
1399 });
1400 if pending.iter().all(Option::is_none) && !state.read(cx).is_active() {
1403 return;
1404 }
1405 let resolved =
1406 pending.map(|pending| pending.and_then(|(callback, point)| callback(point, cx)));
1407 state.update(cx, |state, cx| {
1408 if let (Some(endpoint), Some(key)) = (state.anchor.as_mut(), resolved[0]) {
1409 endpoint.content_key = Some(key);
1410 endpoint.content_key_resolver = None;
1411 }
1412 if let (Some(endpoint), Some(key)) = (state.cursor.as_mut(), resolved[1]) {
1413 endpoint.content_key = Some(key);
1414 endpoint.content_key_resolver = None;
1415 }
1416 state.publish_snapshots(cx);
1417 });
1418 }
1419 fn acquire(window_id: gpui::WindowId, cx: &mut App) -> Entity<Self> {
1420 if !cx.has_global::<SelectionStateRegistry>() {
1421 cx.set_global(SelectionStateRegistry::default());
1422 }
1423 if let Some(state) = cx
1424 .global::<SelectionStateRegistry>()
1425 .0
1426 .get(&window_id)
1427 .and_then(WeakEntity::upgrade)
1428 {
1429 return state;
1430 }
1431
1432 let active_scope = if cx.has_global::<PendingTextSelectionScopes>() {
1433 cx.global_mut::<PendingTextSelectionScopes>()
1434 .0
1435 .remove(&window_id)
1436 .unwrap_or_default()
1437 } else {
1438 TextSelectionScopeId::default()
1439 };
1440
1441 let state = cx.new(move |cx| {
1442 let entity_id = cx.entity_id();
1443 cx.on_release(move |state: &mut WindowSelectionState, cx| {
1444 let handlers = state.clear_state(cx);
1445 if cx.has_global::<SelectionStateRegistry>() {
1446 let registry = &mut cx.global_mut::<SelectionStateRegistry>().0;
1447 if registry
1448 .get(&window_id)
1449 .is_some_and(|state| state.entity_id() == entity_id)
1450 {
1451 registry.remove(&window_id);
1452 }
1453 }
1454 if !handlers.is_empty() {
1455 cx.defer(move |cx| dispatch_clear_handlers(handlers, cx));
1456 }
1457 })
1458 .detach();
1459 Self {
1460 active_scope,
1461 entity_id: Some(entity_id),
1462 ..Self::default()
1463 }
1464 });
1465 cx.global_mut::<SelectionStateRegistry>()
1466 .0
1467 .insert(window_id, state.downgrade());
1468 state
1469 }
1470
1471 #[cfg(test)]
1472 fn ensure(window: &Window, cx: &mut App) -> Entity<Self> {
1473 Self::acquire(window.window_handle().window_id(), cx)
1474 }
1475
1476 fn existing(window: &Window, cx: &App) -> Option<Entity<Self>> {
1477 if !cx.has_global::<SelectionStateRegistry>() {
1478 return None;
1479 }
1480 cx.global::<SelectionStateRegistry>()
1481 .0
1482 .get(&window.window_handle().window_id())
1483 .and_then(WeakEntity::upgrade)
1484 }
1485
1486 #[cfg(test)]
1488 fn set_active_scope(&mut self, scope: TextSelectionScopeId, cx: &mut App) {
1489 let handlers = self.set_active_scope_state(scope, cx);
1490 dispatch_clear_handlers(handlers, cx);
1491 }
1492
1493 fn set_active_scope_state(
1494 &mut self,
1495 scope: TextSelectionScopeId,
1496 cx: &mut App,
1497 ) -> Vec<ClearHandler> {
1498 if self.active_scope == scope {
1499 return Vec::new();
1500 }
1501 let handlers = self.clear_state(cx);
1502 self.active_scope = scope;
1503 self.publish_snapshots(cx);
1504 handlers
1505 }
1506
1507 #[cfg(test)]
1523 fn finish_frame(&mut self, cx: &mut App) -> Vec<ClearHandler> {
1524 let stale = self.stale_participants(true);
1525 let mut handlers = if self.endpoint_departed(&stale) {
1526 self.clear_state(cx)
1527 } else {
1528 Vec::new()
1529 };
1530 let departed = self.remove_stale_participants(stale);
1531 handlers.extend(clear_departed_participants(departed, cx));
1532 self.finish_registrations();
1533 self.publish_snapshots(cx);
1534 handlers
1535 }
1536
1537 fn stale_participants(
1538 &self,
1539 full_frame: bool,
1540 ) -> Vec<(EntityId, WeakEntity<SelectableTextState>)> {
1541 let generation = self.frame_generation.get();
1542 self.participants
1543 .borrow()
1544 .iter()
1545 .filter_map(|(id, participant)| {
1546 let registration = &participant.registration;
1547 let missed_frame = participant.generation != generation
1548 && !registration.is_rendered()
1549 && (full_frame || registration.rendered.is_some());
1550 (missed_frame || participant.participant.upgrade().is_none())
1551 .then(|| (*id, participant.participant.clone()))
1552 })
1553 .collect()
1554 }
1555
1556 fn endpoint_departed(&self, stale: &[(EntityId, WeakEntity<SelectableTextState>)]) -> bool {
1557 stale.iter().any(|(id, _)| {
1558 [
1559 self.anchor.as_ref(),
1560 self.cursor.as_ref(),
1561 self.pending_extension_anchor.as_ref(),
1562 ]
1563 .into_iter()
1564 .flatten()
1565 .chain(self.unit.iter().flat_map(|unit| [&unit.start, &unit.end]))
1566 .any(|endpoint| endpoint.entity_id() == Some(*id))
1567 })
1568 }
1569
1570 fn remove_stale_participants(
1571 &self,
1572 stale: Vec<(EntityId, WeakEntity<SelectableTextState>)>,
1573 ) -> Vec<Entity<SelectableTextState>> {
1574 let mut participants = self.participants.borrow_mut();
1575 stale
1576 .into_iter()
1577 .filter_map(|(id, participant)| {
1578 participants.remove(&id);
1579 participant.upgrade()
1580 })
1581 .collect()
1582 }
1583
1584 fn finish_registrations(&self) -> bool {
1588 let generation = self.frame_generation.get();
1589 let mut participants = self.participants.borrow_mut();
1590 let mut order = self.paint_order.borrow_mut();
1591 let complete = participants.values().all(|participant| {
1592 !participant.registration.paint_order || participant.generation == generation
1593 });
1594 let refresh = if complete {
1595 let mut next = 0;
1596 for id in order.painted.drain(..) {
1597 if let Some(participant) = participants.get_mut(&id)
1598 && participant.registration.paint_order
1599 {
1600 next += 1;
1601 participant.document_order = next;
1602 }
1603 }
1604 order.next = next;
1605 order.dirty = false;
1606 order.refresh_requested = false;
1607 false
1608 } else {
1609 let refresh = order.dirty && !order.refresh_requested;
1610 order.refresh_requested |= refresh;
1611 order.painted.clear();
1612 refresh
1613 };
1614 order.last = None;
1615 self.finish_frame_scheduled.set(false);
1616 self.layer_painted.set(false);
1617 self.frame_generation.set(generation.wrapping_add(1));
1618 refresh
1619 }
1620
1621 fn schedule_finish_frame(&self) -> bool {
1622 !self.finish_frame_scheduled.replace(true)
1623 }
1624
1625 fn is_active(&self) -> bool {
1628 self.anchor.is_some()
1629 || self.cursor.is_some()
1630 || self.pending_extension_anchor.is_some()
1631 || self.is_selecting
1632 || self.refresh_held_cursor.get()
1633 || self.touch.active
1634 }
1635
1636 fn register_quietly(
1640 &self,
1641 selection: &TextSelectionHandle,
1642 registration: TextSelectionRegistration,
1643 ) {
1644 let mut participants = self.participants.borrow_mut();
1645 let previous = participants.get(&selection.entity_id());
1646 if self.is_selecting
1647 && registration.self_scroll
1648 && self.anchor.as_ref().and_then(SelectionEndpoint::entity_id)
1649 == Some(selection.entity_id())
1650 && previous.is_some_and(|previous| {
1651 previous.registration.scroll_offset != registration.scroll_offset
1652 || previous.registration.bounds != registration.bounds
1653 })
1654 {
1655 self.refresh_held_cursor.set(true);
1656 }
1657 if self.touch.active
1660 && previous.is_none_or(|previous| {
1661 previous.registration.selection_edges != registration.selection_edges
1662 })
1663 {
1664 self.touch_edges_moved.set(true);
1665 }
1666 let generation = self.frame_generation.get();
1667 let document_order = if registration.paint_order {
1668 let mut order = self.paint_order.borrow_mut();
1669 let previous = previous.filter(|previous| previous.registration.paint_order);
1670 let number = previous.map_or_else(
1671 || {
1672 order.next += 1;
1673 order.dirty = true;
1674 order.next
1675 },
1676 |previous| previous.document_order,
1677 );
1678 if previous.is_some_and(|previous| {
1682 previous
1683 .registration
1684 .rendered
1685 .as_ref()
1686 .map(|marker| marker.entity_id())
1687 != registration
1688 .rendered
1689 .as_ref()
1690 .map(|marker| marker.entity_id())
1691 }) {
1692 order.dirty = true;
1693 }
1694 let inverted = order.last.is_some_and(|last| last > number);
1695 order.dirty |= inverted;
1696 order.last = Some(number);
1697 order.painted.push(selection.entity_id());
1699 number
1700 } else {
1701 registration.document_order
1702 };
1703 participants.insert(
1704 selection.entity_id(),
1705 ParticipantRegistration {
1706 participant: selection.downgrade(),
1707 registration: Rc::new(registration),
1708 document_order,
1709 generation,
1710 },
1711 );
1712 }
1713
1714 #[cfg(test)]
1715 fn register_participant(
1716 &mut self,
1717 selection: TextSelectionHandle,
1718 registration: TextSelectionRegistration,
1719 cx: &mut App,
1720 ) {
1721 self.register_quietly(&selection, registration);
1722 self.publish_snapshots(cx);
1723 }
1724
1725 #[cfg(test)]
1727 fn begin(&mut self, position: Point<Pixels>, extend: bool, cx: &mut App) {
1728 self.begin_impl(position, extend, false, None, cx);
1729 }
1730
1731 #[cfg(test)]
1733 fn update(&mut self, position: Point<Pixels>, cx: &mut App) {
1734 self.update_impl(position, None, cx);
1735 }
1736
1737 pub fn end(&mut self, cx: &mut App) {
1739 self.pending_extension_anchor = None;
1740 if !self.is_selecting {
1741 return;
1742 }
1743 self.is_selecting = false;
1744 if !self.did_hit_text {
1745 self.anchor = None;
1746 self.cursor = None;
1747 }
1748 self.stop_anchor_auto_scroll(cx);
1749 self.publish_snapshots(cx);
1750 }
1751
1752 pub fn clear(&mut self, cx: &mut App) {
1754 let handlers = self.clear_state(cx);
1755 dispatch_clear_handlers(handlers, cx);
1756 }
1757
1758 fn clear_state(&mut self, cx: &mut App) -> Vec<ClearHandler> {
1759 self.draw_changed.get_mut().clear();
1760 self.table_cell_anchor.set(None);
1761 self.touch_select_all = false;
1762 self.stop_anchor_auto_scroll(cx);
1763 self.anchor = None;
1764 self.cursor = None;
1765 self.pending_extension_anchor = None;
1766 self.unit = None;
1767 self.is_selecting = false;
1768 self.did_hit_text = false;
1769 if self.touch.reset() {
1770 self.touch_changed(cx);
1771 }
1772 self.prune_dead_participants();
1773 self.participants
1774 .borrow()
1775 .values()
1776 .filter_map(|registration| registration.participant.upgrade())
1777 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1778 .collect()
1779 }
1780
1781 fn touch_changed(&self, cx: &mut App) {
1794 let participants = self
1795 .participants
1796 .borrow()
1797 .values()
1798 .filter_map(|registration| registration.participant.upgrade())
1799 .filter(|participant| participant.read(cx).snapshot.is_some())
1800 .collect::<Vec<_>>();
1801 let entity_id = self.entity_id;
1802 cx.defer(move |cx| {
1803 for participant in participants {
1804 participant.update(cx, |_, cx| {
1805 cx.emit(TextSelectionEvent::TouchSelectionChanged)
1806 });
1807 }
1808 if let Some(entity_id) = entity_id {
1809 cx.notify(entity_id);
1810 }
1811 });
1812 }
1813
1814 fn copy_items(&self, cx: &App) -> Vec<CopyItem> {
1815 self.participants
1816 .borrow()
1817 .values()
1818 .filter_map(|registration| {
1819 let participant = registration.participant.upgrade()?;
1820 participant.read(cx).copy_item(registration.document_order)
1821 })
1822 .collect()
1823 }
1824
1825 #[cfg(test)]
1826 fn selected_text(&self, cx: &mut App) -> String {
1827 resolve_copy_items(self.copy_items(cx), cx)
1828 }
1829
1830 fn selects_text(&self, cx: &App) -> bool {
1834 self.snapshot().is_some()
1835 && self.participants.borrow().values().any(|registration| {
1836 registration
1837 .participant
1838 .upgrade()
1839 .is_some_and(|participant| {
1840 let participant = participant.read(cx);
1841 project_ranges(participant.snapshot, &participant.runs.borrow())
1842 .ranges()
1843 .iter()
1844 .any(|range| range.as_ref().is_some_and(|range| !range.is_empty()))
1845 })
1846 })
1847 }
1848
1849 pub fn has_selection(&self, cx: &App) -> bool {
1851 self.snapshot().is_some()
1852 || self.participants.borrow().values().any(|registration| {
1853 registration
1854 .participant
1855 .upgrade()
1856 .is_some_and(|participant| participant.read(cx).local_selection)
1857 })
1858 }
1859
1860 pub fn snapshot(&self) -> Option<TextSelectionSnapshot> {
1862 if !self.did_hit_text {
1863 return None;
1864 }
1865 let anchor_endpoint = self.anchor.as_ref()?;
1866 let cursor_endpoint = self.cursor.as_ref()?;
1867 let anchor = anchor_endpoint.resolve(&self.participants.borrow())?;
1868 let cursor = cursor_endpoint.resolve(&self.participants.borrow())?;
1869 (anchor != cursor).then(|| {
1870 TextSelectionSnapshot::new(anchor_endpoint.snapshot(), cursor_endpoint.snapshot())
1871 .with_selecting(self.is_selecting)
1872 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
1873 })
1874 }
1875
1876 #[cfg(test)]
1878 fn is_selecting(&self) -> bool {
1879 self.is_selecting
1880 }
1881
1882 fn prepare_for_mouse_down(&mut self, extend: bool, cx: &mut App) -> Vec<ClearHandler> {
1883 let pending_extension_anchor = extend.then(|| self.anchor.clone()).flatten();
1884 let unit = extend.then(|| self.unit.take()).flatten();
1885 if !extend {
1886 self.table_cell_anchor.set(None);
1887 self.touch_select_all = false;
1888 }
1889 self.stop_anchor_auto_scroll(cx);
1890 self.anchor = None;
1891 self.cursor = None;
1892 self.pending_extension_anchor = None;
1893 self.unit = unit;
1894 self.is_selecting = false;
1895 self.did_hit_text = false;
1896 if self.touch.reset() {
1897 self.touch_changed(cx);
1898 }
1899 self.prune_dead_participants();
1900 let handlers = self
1901 .participants
1902 .borrow()
1903 .values()
1904 .filter_map(|registration| registration.participant.upgrade())
1905 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1906 .collect();
1907 self.pending_extension_anchor = pending_extension_anchor;
1908 handlers
1909 }
1910
1911 fn touch_selection(&self) -> Option<TouchSelectionSnapshot> {
1918 if !self.touch.active {
1919 return None;
1920 }
1921 let mut start: Option<(u64, Bounds<Pixels>, bool)> = None;
1924 let mut end: Option<(u64, Bounds<Pixels>, bool)> = None;
1925 for registration in self.participants.borrow().values() {
1926 let geometry = ®istration.registration;
1927 if geometry.scope != self.active_scope || registration.participant.upgrade().is_none() {
1928 continue;
1929 }
1930 let Some((edge_start, edge_end)) = geometry.selection_edges else {
1931 continue;
1932 };
1933 let order = registration.document_order;
1934 let viewport = geometry.hitbox.bounds;
1935 if start.is_none_or(|(best, ..)| order < best) {
1936 start = Some((order, edge_start, caret_in_view(edge_start, viewport)));
1937 }
1938 if end.is_none_or(|(best, ..)| order >= best) {
1939 end = Some((order, edge_end, caret_in_view(edge_end, viewport)));
1940 }
1941 }
1942 let edges = match (start, end) {
1943 (Some((_, start, start_visible)), Some((_, end, end_visible))) => {
1944 let edges = (start, end, start_visible, end_visible);
1945 self.touch.last_edges.set(Some(edges));
1946 edges
1947 }
1948 _ if self.touch.drag.is_some() => self.touch.last_edges.get()?,
1949 _ => return None,
1950 };
1951 let (start, end, start_visible, end_visible) = edges;
1952 Some(
1953 TouchSelectionSnapshot::new(start, end)
1954 .with_edge_visible(SelectionEdge::Start, start_visible)
1955 .with_edge_visible(SelectionEdge::End, end_visible)
1956 .with_menu_open(self.touch.menu_open)
1957 .with_dragging(self.touch.drag.map(|drag| drag.edge())),
1958 )
1959 }
1960
1961 fn touch_handles_of(&self, participant: EntityId) -> Vec<(SelectionEdge, Bounds<Pixels>)> {
1966 if !self.touch.active {
1967 return Vec::new();
1968 }
1969 let Some(own) = self.participants.borrow().get(&participant).cloned() else {
1970 return Vec::new();
1971 };
1972 let Some((start, end)) = own.registration.selection_edges else {
1973 return Vec::new();
1974 };
1975 if start == end {
1976 return Vec::new();
1977 }
1978 let order = own.document_order;
1979 let participants = self.participants.borrow();
1980 let others = participants.iter().filter(|(id, registration)| {
1981 **id != participant
1982 && registration.registration.scope == self.active_scope
1983 && registration.registration.selection_edges.is_some()
1984 && registration.participant.upgrade().is_some()
1985 });
1986 let (mut earlier, mut later) = (false, false);
1987 for (_, registration) in others {
1988 let other = registration.document_order;
1989 earlier |= other < order;
1990 later |= other > order;
1991 }
1992 let viewport = own.registration.hitbox.bounds;
1993 let mut handles = Vec::with_capacity(2);
1994 if !earlier && caret_in_view(start, viewport) {
1995 handles.push((SelectionEdge::Start, start));
1996 }
1997 if !later && caret_in_view(end, viewport) {
1998 handles.push((SelectionEdge::End, end));
1999 }
2000 handles
2001 }
2002
2003 fn keep_touch_selection(&mut self, cx: &mut App) {
2005 self.touch.active = self.snapshot().is_some();
2006 self.touch.menu_open = self.touch.active;
2007 self.touch.drag = None;
2008 self.touch_changed(cx);
2009 }
2010
2011 fn register_touch_ui(&mut self, bounds: Bounds<Pixels>) {
2013 self.touch.ui_bounds.push(bounds);
2014 }
2015
2016 fn handles_painted_at(&self, participant: EntityId, bounds: &[Bounds<Pixels>]) -> bool {
2018 self.touch
2019 .handle_bounds
2020 .get(&participant)
2021 .map_or(bounds.is_empty(), |painted| painted == bounds)
2022 }
2023
2024 fn register_handles(&mut self, participant: EntityId, bounds: Vec<Bounds<Pixels>>) {
2027 self.touch.handle_bounds.insert(participant, bounds);
2028 }
2029
2030 fn close_edit_menu(&mut self, cx: &mut App) {
2031 if !self.touch.menu_open {
2032 return;
2033 }
2034 self.touch.menu_open = false;
2035 self.touch_changed(cx);
2036 }
2037
2038 fn edit_menu_on_scroll(&mut self, phase: TouchPhase, cx: &mut App) {
2042 if !self.touch.active || self.touch.drag.is_some() {
2043 return;
2044 }
2045 match phase {
2046 TouchPhase::Ended | TouchPhase::Cancelled => {
2047 if !self.touch.menu_open {
2048 self.touch.menu_open = true;
2049 self.touch_changed(cx);
2050 }
2051 }
2052 _ => self.close_edit_menu(cx),
2053 }
2054 }
2055
2056 fn select_all_touched(&mut self, cx: &mut App) {
2063 if !self.touch.active {
2064 return;
2065 }
2066 let Some((participant, registration)) = self.anchor_registration() else {
2067 return;
2068 };
2069 let (anchor, cursor) = {
2073 let runs = &participant.read(cx).runs.borrow();
2074 let (Some(first), Some(last)) = (
2075 runs.iter().min_by_key(|run| run.document_order),
2076 runs.iter().max_by_key(|run| run.document_order),
2077 ) else {
2078 return;
2079 };
2080 let (Some(start), Some(end)) = (
2081 first.layout.position_for_index(0),
2082 last.layout.position_for_index(last.text.len()),
2083 ) else {
2084 return;
2085 };
2086 let inset = px(1.);
2089 (
2090 point(start.x + inset, start.y + first.layout.line_height() / 2.),
2091 point(end.x - inset, end.y + last.layout.line_height() / 2.),
2092 )
2093 };
2094 let content_key_resolver = participant.read(cx).content_key_resolver.clone();
2095 let to_endpoint = |window_point: Point<Pixels>| {
2096 let content_point =
2097 window_point - registration.bounds.origin - registration.scroll_offset;
2098 SelectionEndpoint {
2099 participant: Some(participant.downgrade()),
2100 point: content_point,
2101 inside: true,
2102 inside_text: true,
2103 content_key: None,
2104 content_key_resolver: content_key_resolver
2105 .clone()
2106 .map(|resolver| (resolver, content_point)),
2107 }
2108 };
2109 self.table_cell_anchor.set(None);
2110 self.touch_select_all = true;
2111 self.anchor = Some(to_endpoint(anchor));
2112 self.cursor = Some(to_endpoint(cursor));
2113 self.did_hit_text = true;
2114 self.is_selecting = false;
2115 self.touch.menu_open = true;
2116 self.publish_snapshots(cx);
2117 self.touch_changed(cx);
2118 }
2119
2120 fn begin_edge_drag(
2126 &mut self,
2127 edge: SelectionEdge,
2128 finger: Point<Pixels>,
2129 window: &mut Window,
2130 cx: &mut App,
2131 ) {
2132 let Some(snapshot) = self.touch_selection() else {
2133 return;
2134 };
2135 let handlers = self.prepare_for_mouse_down(false, cx);
2136 dispatch_clear_handlers(handlers, cx);
2137 let held = snapshot.edge(edge.opposite());
2138 let nudge = px(1.);
2141 let anchor_point = match edge {
2142 SelectionEdge::Start => point(held.left() - nudge, held.center().y),
2143 SelectionEdge::End => point(held.left() + nudge, held.center().y),
2144 };
2145 let anchor = self.endpoint(anchor_point, None, cx);
2146 let drag = EdgeDrag::begin(edge, snapshot.edge(edge), finger);
2147 let cursor = self.endpoint(drag.text_position(finger), Some(window), cx);
2148 self.did_hit_text = anchor.inside_text || cursor.inside_text;
2149 self.anchor = Some(anchor);
2150 self.cursor = Some(cursor);
2151 self.is_selecting = true;
2152 self.touch.active = true;
2153 self.touch.menu_open = false;
2154 self.touch.drag = Some(drag);
2155 self.publish_snapshots(cx);
2156 self.touch_changed(cx);
2157 }
2158
2159 fn update_edge_drag(&mut self, finger: Point<Pixels>, window: &Window, cx: &mut Context<Self>) {
2160 let Some(drag) = self.touch.drag else {
2161 return;
2162 };
2163 let before = self.cursor.clone();
2164 self.update_in_window(drag.text_position(finger), window, cx);
2165 if !self.selects_text(cx) {
2168 self.cursor = before;
2169 self.publish_snapshots(cx);
2170 return;
2171 }
2172 if let Some(points) = self
2175 .snapshot()
2176 .and_then(|snapshot| snapshot.window_points())
2177 {
2178 let (anchor, cursor) = (points.anchor(), points.cursor());
2179 let cursor_first = cursor.y < anchor.y || (cursor.y == anchor.y && cursor.x < anchor.x);
2180 let edge = if cursor_first {
2181 SelectionEdge::Start
2182 } else {
2183 SelectionEdge::End
2184 };
2185 if let Some(drag) = self.touch.drag.as_mut() {
2186 drag.set_edge(edge);
2187 }
2188 }
2189 self.touch_changed(cx);
2190 }
2191
2192 fn end_edge_drag(&mut self, cx: &mut App) {
2193 if self.touch.drag.take().is_none() {
2194 return;
2195 }
2196 self.end(cx);
2197 self.keep_touch_selection(cx);
2198 }
2199
2200 fn begin_in_window(
2201 &mut self,
2202 position: Point<Pixels>,
2203 extend: bool,
2204 window: &mut Window,
2205 cx: &mut App,
2206 ) {
2207 self.begin_impl(position, extend, true, Some(window), cx);
2208 }
2209
2210 fn update_in_window(
2211 &mut self,
2212 position: Point<Pixels>,
2213 window: &Window,
2214 cx: &mut Context<Self>,
2215 ) {
2216 if !cx.has_active_drag() {
2217 self.update_impl(position, Some(window), cx);
2218 self.update_auto_scroll(position, window, cx);
2219 }
2220 }
2221
2222 fn select_at(
2224 &mut self,
2225 position: Point<Pixels>,
2226 click_count: usize,
2227 window: &mut Window,
2228 cx: &mut App,
2229 ) -> Option<SelectionUnit> {
2230 GlobalState::init(cx);
2231 if GlobalState::is_text_selection_suppressed(cx) {
2232 return None;
2233 }
2234 let hit = self.endpoint(position, Some(window), cx);
2235 if !hit.inside_text {
2236 return None;
2237 }
2238 let participant = hit.participant?.upgrade()?;
2239 let (start, end) =
2240 carets_for_multi_click(&participant.read(cx).runs.borrow(), position, click_count)?;
2241 let unit = self.unit_of(&participant, start, end, click_count, cx)?;
2242 self.anchor = Some(unit.start.clone());
2243 self.cursor = Some(unit.end.clone());
2244 self.did_hit_text = true;
2245 self.is_selecting = false;
2246 participant.update(cx, |state, cx| state.focus(window, cx));
2247 self.publish_snapshots(cx);
2248 Some(unit)
2249 }
2250
2251 fn unit_of(
2254 &self,
2255 participant: &Entity<SelectableTextState>,
2256 start: Bounds<Pixels>,
2257 end: Bounds<Pixels>,
2258 click_count: usize,
2259 cx: &App,
2260 ) -> Option<SelectionUnit> {
2261 let registration = self
2262 .participants
2263 .borrow()
2264 .get(&participant.entity_id())?
2265 .registration
2266 .clone();
2267 let content_key_resolver = participant.read(cx).content_key_resolver.clone();
2268 let to_endpoint = |point: Point<Pixels>| {
2269 let content_point = point - registration.bounds.origin - registration.scroll_offset;
2270 SelectionEndpoint {
2271 participant: Some(participant.downgrade()),
2272 point: content_point,
2273 inside: true,
2274 inside_text: true,
2275 content_key: None,
2276 content_key_resolver: content_key_resolver
2277 .clone()
2278 .map(|resolver| (resolver, content_point)),
2279 }
2280 };
2281 Some(SelectionUnit {
2282 click_count,
2283 start: to_endpoint(start.origin),
2284 end: to_endpoint(end.origin),
2285 row_heights: (start.size.height, end.size.height),
2286 })
2287 }
2288
2289 fn hold_unit(&mut self, unit: SelectionUnit, cx: &mut App) {
2293 self.unit = Some(unit);
2294 self.is_selecting = true;
2295 self.publish_snapshots(cx);
2296 }
2297
2298 fn extend_unit(
2302 &mut self,
2303 unit: &SelectionUnit,
2304 position: Point<Pixels>,
2305 endpoint: SelectionEndpoint,
2306 cx: &App,
2307 ) {
2308 let (start, end) = {
2309 let participants = self.participants.borrow();
2310 (
2311 unit.start.resolve(&participants),
2312 unit.end.resolve(&participants),
2313 )
2314 };
2315 let (Some(start), Some(end)) = (start, end) else {
2316 self.did_hit_text |= endpoint.inside_text;
2317 self.cursor = Some(endpoint);
2318 return;
2319 };
2320 let (start_height, end_height) = unit.row_heights;
2321 let before =
2322 position.y < start.y || (position.y < start.y + start_height && position.x < start.x);
2323 let after = position.y >= end.y + end_height || (position.y >= end.y && position.x > end.x);
2324 if !before && !after {
2325 if self.anchor.is_some() {
2328 self.anchor = Some(unit.start.clone());
2329 self.cursor = Some(unit.end.clone());
2330 self.did_hit_text = true;
2331 }
2332 return;
2333 }
2334 let (edge, anchor) = if before {
2335 (SelectionEdge::Start, &unit.end)
2336 } else {
2337 (SelectionEdge::End, &unit.start)
2338 };
2339 let cursor = Self::unit_edge_at(&endpoint, position, unit.click_count, edge, cx);
2340 self.anchor = Some(anchor.clone());
2341 self.cursor = Some(cursor.unwrap_or(endpoint));
2342 self.did_hit_text = true;
2343 }
2344
2345 fn unit_edge_at(
2348 endpoint: &SelectionEndpoint,
2349 position: Point<Pixels>,
2350 click_count: usize,
2351 edge: SelectionEdge,
2352 cx: &App,
2353 ) -> Option<SelectionEndpoint> {
2354 if !endpoint.inside {
2355 return None;
2356 }
2357 let participant = endpoint.participant.as_ref()?.upgrade()?;
2358 let runs = participant.read(cx).runs.borrow();
2359 let run = runs.iter().find(|run| run.bounds.contains(&position))?;
2360 if run.text.len() != run.layout.len() {
2361 return None;
2362 }
2363 let offset = run
2365 .layout
2366 .index_for_position(position)
2367 .unwrap_or_else(|closest| closest);
2368 let (start, end) = unit_carets(run, offset, click_count)?;
2369 let caret = match edge {
2370 SelectionEdge::Start => start,
2371 SelectionEdge::End => end,
2372 };
2373 Some(endpoint.moved(caret.origin - position))
2374 }
2375
2376 #[cfg(test)]
2377 fn update_in_window_with_active_drag(
2378 &mut self,
2379 position: Point<Pixels>,
2380 active_drag: bool,
2381 window: &Window,
2382 cx: &mut App,
2383 ) {
2384 if !active_drag {
2385 self.update_impl(position, Some(window), cx);
2386 }
2387 }
2388
2389 fn begin_impl(
2390 &mut self,
2391 position: Point<Pixels>,
2392 extend: bool,
2393 already_prepared: bool,
2394 window: Option<&mut Window>,
2395 cx: &mut App,
2396 ) {
2397 GlobalState::init(cx);
2398 if GlobalState::is_text_selection_suppressed(cx) {
2399 self.pending_extension_anchor = None;
2400 return;
2401 }
2402 let previous_anchor = extend
2403 .then(|| {
2404 self.pending_extension_anchor
2405 .take()
2406 .or_else(|| self.anchor.clone())
2407 })
2408 .flatten()
2409 .filter(|anchor| anchor.resolve(&self.participants.borrow()).is_some());
2410 if !extend && !already_prepared {
2411 self.clear(cx);
2412 }
2413 let endpoint = self.endpoint(position, window.as_deref(), cx);
2414 let focus_participant = endpoint
2415 .inside
2416 .then(|| endpoint.participant.clone())
2417 .flatten();
2418 let anchor = previous_anchor.unwrap_or_else(|| endpoint.clone());
2419 self.anchor = Some(anchor.clone());
2420 self.cursor = Some(endpoint.clone());
2421 self.did_hit_text = anchor.inside_text || endpoint.inside_text;
2422 self.is_selecting = true;
2423 if let Some(unit) = extend.then(|| self.unit.clone()).flatten() {
2425 self.extend_unit(&unit, position, endpoint, cx);
2426 }
2427 if let Some(participant) = focus_participant.and_then(|participant| participant.upgrade()) {
2428 if let Some(window) = window {
2429 participant.update(cx, |state, cx| state.focus(window, cx));
2430 }
2431 }
2432 self.publish_snapshots(cx);
2433 }
2434
2435 fn update_impl(&mut self, position: Point<Pixels>, window: Option<&Window>, cx: &mut App) {
2436 if !self.is_selecting {
2437 return;
2438 }
2439 let endpoint = self.endpoint(position, window, cx);
2440 match self.unit.clone() {
2441 Some(unit) => self.extend_unit(&unit, position, endpoint, cx),
2442 None => {
2443 self.did_hit_text |= endpoint.inside_text;
2444 self.cursor = Some(endpoint);
2445 }
2446 }
2447 if window.is_none() {
2448 self.update_participant_auto_scroll(position, cx);
2449 }
2450 self.publish_snapshots(cx);
2451 }
2452
2453 fn endpoint(
2454 &mut self,
2455 position: Point<Pixels>,
2456 window: Option<&Window>,
2457 cx: &App,
2458 ) -> SelectionEndpoint {
2459 self.prune_dead_participants();
2460 let mut hit: Option<(ParticipantRegistration, f32)> = None;
2461 let mut predecessor: Option<ParticipantRegistration> = None;
2462 let mut first: Option<ParticipantRegistration> = None;
2463
2464 for registration in self.participants.borrow().values() {
2465 if registration.registration.scope != self.active_scope
2466 || registration.participant.upgrade().is_none()
2467 {
2468 continue;
2469 }
2470 let participant_geometry = ®istration.registration;
2471 let hovered = window.map_or_else(
2472 || participant_geometry.bounds.contains(&position),
2473 |window| participant_geometry.hitbox.is_hovered(window),
2474 );
2475 if hovered {
2476 let area = f32::from(participant_geometry.bounds.size.width)
2477 * f32::from(participant_geometry.bounds.size.height);
2478 if hit.as_ref().is_none_or(|(best, best_area)| {
2479 area < *best_area
2480 || (area == *best_area && registration.document_order < best.document_order)
2481 }) {
2482 hit = Some((registration.clone(), area));
2483 }
2484 }
2485 if participant_geometry.bounds.top() <= position.y
2486 && predecessor.as_ref().is_none_or(|best| {
2487 participant_geometry.bounds.top() > best.registration.bounds.top()
2488 || (participant_geometry.bounds.top() == best.registration.bounds.top()
2489 && registration.document_order < best.document_order)
2490 })
2491 {
2492 predecessor = Some(registration.clone());
2493 }
2494 if first.as_ref().is_none_or(|best| {
2495 participant_geometry.bounds.top() < best.registration.bounds.top()
2496 || (participant_geometry.bounds.top() == best.registration.bounds.top()
2497 && registration.document_order < best.document_order)
2498 }) {
2499 first = Some(registration.clone());
2500 }
2501 }
2502
2503 let selection = hit.map(|(participant, _)| (participant, true)).or_else(|| {
2504 predecessor
2505 .or(first)
2506 .map(|participant| (participant, false))
2507 });
2508 match selection {
2509 Some((participant, inside)) => {
2510 let registration = participant.registration;
2511 let participant = participant.participant;
2512 let point = position - registration.bounds.origin - registration.scroll_offset;
2513 let content_key_resolver = participant.upgrade().and_then(|participant| {
2514 participant
2515 .read(cx)
2516 .content_key_resolver
2517 .clone()
2518 .map(|callback| (callback, point))
2519 });
2520 SelectionEndpoint {
2521 point,
2522 participant: Some(participant),
2523 inside,
2524 inside_text: inside
2525 && registration
2526 .text_bounds
2527 .iter()
2528 .any(|bounds| bounds.contains(&position)),
2529 content_key: None,
2530 content_key_resolver,
2531 }
2532 }
2533 None => SelectionEndpoint {
2534 participant: None,
2535 point: position,
2536 inside: false,
2537 inside_text: false,
2538 content_key: None,
2539 content_key_resolver: None,
2540 },
2541 }
2542 }
2543
2544 fn publish_snapshots(&mut self, cx: &mut App) {
2545 self.prune_dead_participants();
2546 for (participant, snapshot) in self.changed_snapshots(cx) {
2547 self.draw_changed.get_mut().remove(&participant.entity_id());
2548 participant.update(cx, |state, cx| state.set_snapshot(snapshot, cx));
2549 }
2550 }
2551
2552 fn participant_snapshot(&self, id: EntityId) -> Option<TextSelectionSnapshot> {
2553 #[cfg(test)]
2554 self.snapshot_visits.set(self.snapshot_visits.get() + 1);
2555
2556 let participants = self.participants.borrow();
2557 let registration = participants.get(&id)?;
2558 (registration.registration.scope == self.active_scope
2559 && self.participates(id, registration)
2560 && self.single_participant().is_none_or(|single| single == id))
2561 .then(|| self.snapshot())
2562 .flatten()
2563 .map(|mut snapshot| {
2564 snapshot.coverage = self.coverage_for(id);
2565 snapshot
2566 })
2567 }
2568
2569 fn changed_snapshots(
2570 &self,
2571 cx: &App,
2572 ) -> Vec<(Entity<SelectableTextState>, Option<TextSelectionSnapshot>)> {
2573 self.participants
2574 .borrow()
2575 .iter()
2576 .filter_map(|(id, registration)| {
2577 let participant = registration.participant.upgrade()?;
2578 let snapshot = self.participant_snapshot(*id);
2579 (participant.read(cx).snapshot != snapshot).then_some((participant, snapshot))
2580 })
2581 .collect()
2582 }
2583
2584 fn coverage_for(&self, id: EntityId) -> TextSelectionCoverage {
2585 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2586 return TextSelectionCoverage::Bounded;
2587 };
2588 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2589 return TextSelectionCoverage::Bounded;
2590 };
2591 if anchor == cursor {
2592 return TextSelectionCoverage::Bounded;
2593 }
2594 let anchor_order = self.participants.borrow()[&anchor].document_order;
2595 let cursor_order = self.participants.borrow()[&cursor].document_order;
2596 if id != anchor && id != cursor {
2597 TextSelectionCoverage::Full
2598 } else if (id == anchor) == (anchor_order < cursor_order) {
2599 TextSelectionCoverage::ToEnd
2600 } else {
2601 TextSelectionCoverage::FromStart
2602 }
2603 }
2604
2605 fn single_participant(&self) -> Option<EntityId> {
2606 let anchor = self.anchor.as_ref()?.entity_id()?;
2607 let cursor = self.cursor.as_ref()?.entity_id()?;
2608 (anchor == cursor).then_some(anchor)
2609 }
2610
2611 fn participates(&self, id: EntityId, registration: &ParticipantRegistration) -> bool {
2612 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2613 return false;
2614 };
2615 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
2616 return false;
2617 };
2618 let Some(anchor_registration) = self.participants.borrow().get(&anchor).cloned() else {
2619 return false;
2620 };
2621 let Some(cursor_registration) = self.participants.borrow().get(&cursor).cloned() else {
2622 return false;
2623 };
2624 let start = anchor_registration
2625 .document_order
2626 .min(cursor_registration.document_order);
2627 let end = anchor_registration
2628 .document_order
2629 .max(cursor_registration.document_order);
2630 (start..=end).contains(®istration.document_order) || id == anchor || id == cursor
2631 }
2632
2633 fn update_auto_scroll(
2634 &mut self,
2635 position: Point<Pixels>,
2636 window: &Window,
2637 cx: &mut Context<Self>,
2638 ) {
2639 if !self.is_selecting {
2642 return;
2643 }
2644 let Some((_, registration)) = self.anchor_registration() else {
2645 return;
2646 };
2647 if registration.self_scroll {
2650 self.auto_scroll.stop();
2651 self.update_participant_auto_scroll(position, cx);
2652 return;
2653 }
2654 let visible_bounds = registration.hitbox.content_mask.bounds;
2659 const HIT_TEST_INSET: Pixels = px(1.);
2661 if visible_bounds.size.width < HIT_TEST_INSET * 2.
2663 || visible_bounds.size.height < HIT_TEST_INSET * 2.
2664 {
2665 self.stop_anchor_auto_scroll(cx);
2666 return;
2667 }
2668 let delta = AutoScroll::compute_delta(position.y, visible_bounds);
2669 let event_position = point(
2670 position.x.clamp(
2671 visible_bounds.left() + HIT_TEST_INSET,
2672 visible_bounds.right() - HIT_TEST_INSET,
2673 ),
2674 position.y.clamp(
2675 visible_bounds.top() + HIT_TEST_INSET,
2676 visible_bounds.bottom() - HIT_TEST_INSET,
2677 ),
2678 );
2679 self.auto_scroll.last_drag_position = Some(event_position);
2680 self.auto_scroll_stall = (None, 0);
2681 let window = window.window_handle();
2682 self.auto_scroll.set(delta, cx, move |delta, state, cx| {
2683 let Some(position) = state.auto_scroll.last_drag_position else {
2684 return;
2685 };
2686 const STALLED_TICKS: u8 = 3;
2690 let geometry = state
2691 .anchor_registration()
2692 .map(|(_, registration)| (registration.bounds, registration.scroll_offset));
2693 let (last, stalled) = &mut state.auto_scroll_stall;
2694 if *last == geometry {
2695 *stalled = stalled.saturating_add(1);
2696 if *stalled >= STALLED_TICKS {
2697 return;
2698 }
2699 } else {
2700 *last = geometry;
2701 *stalled = 0;
2702 }
2703 let window = window;
2704 cx.defer(move |cx| {
2705 _ = window.update(cx, |_, window, cx| {
2706 window.dispatch_event(
2707 ScrollEvent {
2708 position,
2709 delta: ScrollDelta::Pixels(point(px(0.), -delta)),
2710 ..Default::default()
2711 }
2712 .to_platform_input(),
2713 cx,
2714 );
2715 });
2716 });
2717 });
2718 }
2719
2720 fn update_participant_auto_scroll(&self, position: Point<Pixels>, cx: &mut App) {
2723 let Some((participant, registration)) = self.anchor_registration() else {
2724 return;
2725 };
2726 let visible_bounds = registration
2727 .bounds
2728 .intersect(®istration.hitbox.content_mask.bounds);
2729 let delta = if visible_bounds.size.width > px(0.) && visible_bounds.size.height > px(0.) {
2730 AutoScroll::compute_delta(position.y, visible_bounds)
2731 } else {
2732 None
2733 };
2734 participant.update(cx, |state, cx| state.set_auto_scroll(delta, cx));
2735 }
2736
2737 fn stop_anchor_auto_scroll(&mut self, cx: &mut App) {
2738 self.auto_scroll.stop();
2739 let Some(participant) = self.anchor_participant() else {
2740 return;
2741 };
2742 participant.update(cx, |state, cx| state.set_auto_scroll(None, cx));
2743 }
2744
2745 fn anchor_participant(&self) -> Option<Entity<SelectableTextState>> {
2747 self.anchor
2748 .as_ref()
2749 .filter(|anchor| anchor.inside)?
2750 .participant
2751 .as_ref()?
2752 .upgrade()
2753 }
2754
2755 fn anchor_registration(
2757 &self,
2758 ) -> Option<(Entity<SelectableTextState>, Rc<TextSelectionRegistration>)> {
2759 let participant = self.anchor_participant()?;
2760 let registration = self
2761 .participants
2762 .borrow()
2763 .get(&participant.entity_id())?
2764 .clone();
2765 Some((participant, registration.registration.clone()))
2766 }
2767
2768 fn prune_dead_participants(&mut self) {
2769 self.participants
2770 .get_mut()
2771 .retain(|_, registration| registration.participant.upgrade().is_some());
2772 }
2773}
2774
2775#[derive(Default)]
2776struct SelectionStateRegistry(HashMap<gpui::WindowId, WeakEntity<WindowSelectionState>>);
2779
2780impl Global for SelectionStateRegistry {}
2781
2782#[derive(Default)]
2783struct PendingTextSelectionScopes(HashMap<gpui::WindowId, TextSelectionScopeId>);
2784
2785impl Global for PendingTextSelectionScopes {}
2786
2787#[derive(Default)]
2788struct TextSelectionScopeStacks(HashMap<gpui::WindowId, Vec<TextSelectionScopeId>>);
2789
2790impl Global for TextSelectionScopeStacks {}
2791
2792fn push_text_selection_scope(window_id: gpui::WindowId, scope: TextSelectionScopeId, cx: &mut App) {
2793 if !cx.has_global::<TextSelectionScopeStacks>() {
2794 cx.set_global(TextSelectionScopeStacks::default());
2795 }
2796 cx.global_mut::<TextSelectionScopeStacks>()
2797 .0
2798 .entry(window_id)
2799 .or_default()
2800 .push(scope);
2801}
2802
2803fn pop_text_selection_scope(window_id: gpui::WindowId, cx: &mut App) {
2804 let stacks = &mut cx.global_mut::<TextSelectionScopeStacks>().0;
2805 let remove_stack = stacks.get_mut(&window_id).is_some_and(|stack| {
2806 stack.pop();
2807 stack.is_empty()
2808 });
2809 if remove_stack {
2810 stacks.remove(&window_id);
2811 }
2812}
2813
2814fn current_text_selection_scope(
2815 window_id: gpui::WindowId,
2816 cx: &App,
2817) -> Option<TextSelectionScopeId> {
2818 cx.has_global::<TextSelectionScopeStacks>()
2819 .then(|| {
2820 cx.global::<TextSelectionScopeStacks>()
2821 .0
2822 .get(&window_id)
2823 .and_then(|stack| stack.last().copied())
2824 })
2825 .flatten()
2826}
2827
2828fn with_text_selection_scope<T>(
2829 window_id: gpui::WindowId,
2830 scope: TextSelectionScopeId,
2831 cx: &mut App,
2832 callback: impl FnOnce(&mut App) -> T,
2833) -> T {
2834 push_text_selection_scope(window_id, scope, cx);
2835 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(cx)));
2836 pop_text_selection_scope(window_id, cx);
2837 match result {
2838 Ok(result) => result,
2839 Err(payload) => std::panic::resume_unwind(payload),
2840 }
2841}
2842
2843pub struct TextSelection;
2845
2846impl TextSelection {
2847 pub fn is_selectable_at(position: Point<Pixels>, window: &Window, cx: &mut App) -> bool {
2856 live_text_selection_state(window, cx).is_some_and(|state| {
2857 state.update(cx, |state, cx| {
2858 !state.touch.covers(position)
2859 && state.endpoint(position, Some(window), cx).inside_text
2860 })
2861 })
2862 }
2863
2864 pub fn selected_text(window: &mut Window, cx: &mut App) -> String {
2866 let Some(state) = live_text_selection_state(window, cx) else {
2867 return String::new();
2868 };
2869 let items = state.read(cx).copy_items(cx);
2870 resolve_copy_items(items, cx)
2871 }
2872
2873 pub fn has_selection(window: &mut Window, cx: &mut App) -> bool {
2876 live_text_selection_state(window, cx).is_some_and(|state| state.read(cx).has_selection(cx))
2877 }
2878
2879 pub fn clear(window: &mut Window, cx: &mut App) {
2881 if let Some(state) = live_text_selection_state(window, cx) {
2882 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
2883 dispatch_clear_handlers(handlers, cx);
2884 }
2885 }
2886
2887 pub fn clear_for_window(window_id: gpui::WindowId, cx: &mut App) {
2892 clear_window_text_selection(window_id, cx);
2893 }
2894
2895 pub fn end(window: &mut Window, cx: &mut App) {
2897 if let Some(state) = live_text_selection_state(window, cx) {
2898 state.update(cx, |state, cx| state.end(cx));
2899 }
2900 }
2901
2902 pub fn observe_touch_selection(
2906 window: &Window,
2907 cx: &mut App,
2908 callback: impl Fn(&mut App) + 'static,
2909 ) -> Subscription {
2910 let state = WindowSelectionState::acquire(window.window_handle().window_id(), cx);
2911 cx.observe(&state, move |_, cx| callback(cx))
2912 }
2913
2914 pub fn touch_selection(window: &Window, cx: &App) -> Option<TouchSelectionSnapshot> {
2917 WindowSelectionState::existing(window, cx)?
2918 .read(cx)
2919 .touch_selection()
2920 }
2921
2922 pub fn register_touch_ui(bounds: Bounds<Pixels>, window: &Window, cx: &mut App) {
2926 if let Some(state) = WindowSelectionState::existing(window, cx) {
2927 state.update(cx, |state, _| state.register_touch_ui(bounds));
2928 }
2929 }
2930
2931 pub fn close_edit_menu(window: &mut Window, cx: &mut App) {
2933 if let Some(state) = live_text_selection_state(window, cx) {
2934 state.update(cx, |state, cx| state.close_edit_menu(cx));
2935 }
2936 }
2937
2938 pub fn select_all(window: &mut Window, cx: &mut App) {
2941 if let Some(state) = live_text_selection_state(window, cx) {
2942 state.update(cx, |state, cx| state.select_all_touched(cx));
2943 }
2944 }
2945
2946 pub fn begin_edge_drag(
2949 edge: SelectionEdge,
2950 finger: Point<Pixels>,
2951 window: &mut Window,
2952 cx: &mut App,
2953 ) {
2954 if let Some(state) = live_text_selection_state(window, cx) {
2955 state.update(cx, |state, cx| {
2956 state.begin_edge_drag(edge, finger, window, cx)
2957 });
2958 WindowSelectionState::resolve_content_keys(&state, cx);
2959 }
2960 }
2961
2962 pub fn update_edge_drag(finger: Point<Pixels>, window: &mut Window, cx: &mut App) {
2964 if let Some(state) = live_text_selection_state(window, cx) {
2965 state.update(cx, |state, cx| state.update_edge_drag(finger, window, cx));
2966 WindowSelectionState::resolve_content_keys(&state, cx);
2967 }
2968 }
2969
2970 pub fn end_edge_drag(window: &mut Window, cx: &mut App) {
2972 if let Some(state) = live_text_selection_state(window, cx) {
2973 state.update(cx, |state, cx| state.end_edge_drag(cx));
2974 }
2975 }
2976
2977 pub fn activate_scope(scope: TextSelectionScopeId, window: &mut Window, cx: &mut App) {
2979 let Some(state) = WindowSelectionState::existing(window, cx) else {
2980 if !cx.has_global::<PendingTextSelectionScopes>() {
2981 cx.set_global(PendingTextSelectionScopes::default());
2982 }
2983 cx.global_mut::<PendingTextSelectionScopes>()
2984 .0
2985 .insert(window.window_handle().window_id(), scope);
2986 return;
2987 };
2988 if state.read(cx).active_scope == scope {
2989 return;
2990 }
2991 let handlers = state.update(cx, |state, cx| state.set_active_scope_state(scope, cx));
2992 dispatch_clear_handlers(handlers, cx);
2993 }
2994}
2995
2996pub struct TextSelectionLayer;
3001
3002pub(crate) fn text_selection_scope(
3003 scope: TextSelectionScopeId,
3004 element: impl IntoElement,
3005) -> impl IntoElement {
3006 TextSelectionScopeMarker {
3007 scope,
3008 element: element.into_element(),
3009 }
3010}
3011
3012struct TextSelectionScopeMarker<E> {
3013 scope: TextSelectionScopeId,
3014 element: E,
3015}
3016
3017impl<E: Element> IntoElement for TextSelectionScopeMarker<E> {
3018 type Element = Self;
3019
3020 fn into_element(self) -> Self::Element {
3021 self
3022 }
3023}
3024
3025impl<E: Element> Element for TextSelectionScopeMarker<E> {
3026 type RequestLayoutState = E::RequestLayoutState;
3027 type PrepaintState = E::PrepaintState;
3028
3029 fn id(&self) -> Option<ElementId> {
3030 self.element.id()
3031 }
3032
3033 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
3034 self.element.source_location()
3035 }
3036
3037 fn request_layout(
3038 &mut self,
3039 id: Option<&GlobalElementId>,
3040 inspector_id: Option<&InspectorElementId>,
3041 window: &mut Window,
3042 cx: &mut App,
3043 ) -> (LayoutId, Self::RequestLayoutState) {
3044 let window_id = window.window_handle().window_id();
3045 with_text_selection_scope(window_id, self.scope, cx, |cx| {
3046 self.element.request_layout(id, inspector_id, window, cx)
3047 })
3048 }
3049
3050 fn prepaint(
3051 &mut self,
3052 id: Option<&GlobalElementId>,
3053 inspector_id: Option<&InspectorElementId>,
3054 bounds: Bounds<Pixels>,
3055 request_layout: &mut Self::RequestLayoutState,
3056 window: &mut Window,
3057 cx: &mut App,
3058 ) -> Self::PrepaintState {
3059 let window_id = window.window_handle().window_id();
3060 with_text_selection_scope(window_id, self.scope, cx, |cx| {
3061 self.element
3062 .prepaint(id, inspector_id, bounds, request_layout, window, cx)
3063 })
3064 }
3065
3066 fn paint(
3067 &mut self,
3068 id: Option<&GlobalElementId>,
3069 inspector_id: Option<&InspectorElementId>,
3070 bounds: Bounds<Pixels>,
3071 request_layout: &mut Self::RequestLayoutState,
3072 prepaint: &mut Self::PrepaintState,
3073 window: &mut Window,
3074 cx: &mut App,
3075 ) {
3076 let window_id = window.window_handle().window_id();
3077 with_text_selection_scope(window_id, self.scope, cx, |cx| {
3078 self.element.paint(
3079 id,
3080 inspector_id,
3081 bounds,
3082 request_layout,
3083 prepaint,
3084 window,
3085 cx,
3086 );
3087 });
3088 }
3089}
3090
3091#[doc(hidden)]
3092pub struct TextSelectionLayerPrepaintState(Entity<WindowSelectionState>);
3093
3094impl IntoElement for TextSelectionLayer {
3095 type Element = Self;
3096
3097 fn into_element(self) -> Self::Element {
3098 self
3099 }
3100}
3101
3102impl Element for TextSelectionLayer {
3103 type RequestLayoutState = ();
3104 type PrepaintState = TextSelectionLayerPrepaintState;
3105
3106 fn id(&self) -> Option<ElementId> {
3107 Some("window-text-selection".into())
3108 }
3109
3110 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
3111 None
3112 }
3113
3114 fn request_layout(
3115 &mut self,
3116 _: Option<&GlobalElementId>,
3117 _: Option<&InspectorElementId>,
3118 window: &mut Window,
3119 cx: &mut App,
3120 ) -> (LayoutId, Self::RequestLayoutState) {
3121 (window.request_layout(Style::default(), [], cx), ())
3122 }
3123
3124 fn prepaint(
3125 &mut self,
3126 global_id: Option<&GlobalElementId>,
3127 _: Option<&InspectorElementId>,
3128 _: Bounds<Pixels>,
3129 _: &mut Self::RequestLayoutState,
3130 window: &mut Window,
3131 cx: &mut App,
3132 ) -> Self::PrepaintState {
3133 GlobalState::init(cx);
3134 let state = retain_text_selection_state(global_id, window, cx);
3135 state.read(cx).layer_painted.set(true);
3136 if state.read(cx).touch.painted_any() {
3138 state.update(cx, |state, _| state.touch.begin_frame());
3139 }
3140 TextSelectionLayerPrepaintState(state)
3141 }
3142
3143 fn paint(
3144 &mut self,
3145 _: Option<&GlobalElementId>,
3146 _: Option<&InspectorElementId>,
3147 _: Bounds<Pixels>,
3148 _: &mut Self::RequestLayoutState,
3149 state: &mut Self::PrepaintState,
3150 window: &mut Window,
3151 cx: &mut App,
3152 ) {
3153 paint_text_selection(&state.0, window, cx);
3154 }
3155}
3156
3157fn retain_text_selection_state(
3158 global_id: Option<&GlobalElementId>,
3159 window: &mut Window,
3160 cx: &mut App,
3161) -> Entity<WindowSelectionState> {
3162 let window_id = window.window_handle().window_id();
3163 let state = window.with_element_state::<Entity<WindowSelectionState>, _>(
3164 global_id.expect("TextSelection has a stable element id"),
3165 |retained, _| {
3166 let state = retained.unwrap_or_else(|| WindowSelectionState::acquire(window_id, cx));
3167 (state.clone(), state)
3168 },
3169 );
3170 let registered = cx.has_global::<SelectionStateRegistry>()
3173 && cx
3174 .global::<SelectionStateRegistry>()
3175 .0
3176 .get(&window_id)
3177 .is_some_and(|registered| registered.entity_id() == state.entity_id());
3178 if !registered {
3179 if !cx.has_global::<SelectionStateRegistry>() {
3180 cx.set_global(SelectionStateRegistry::default());
3181 }
3182 cx.global_mut::<SelectionStateRegistry>()
3183 .0
3184 .insert(window_id, state.downgrade());
3185 }
3186 state
3187}
3188
3189fn schedule_selection_frame(
3190 state: &Entity<WindowSelectionState>,
3191 window: &mut Window,
3192 cx: &mut App,
3193) {
3194 if state.read(cx).schedule_finish_frame() {
3195 let state = state.downgrade();
3196 window.defer(cx, move |window, cx| {
3197 let Some(state) = state.upgrade() else {
3198 return;
3199 };
3200 let stale = state
3201 .read(cx)
3202 .stale_participants(state.read(cx).layer_painted.get());
3203 let mut handlers = if state.read(cx).endpoint_departed(&stale) {
3206 state.update(cx, |state, cx| state.clear_state(cx))
3207 } else {
3208 Vec::new()
3209 };
3210 let departed = state.read(cx).remove_stale_participants(stale);
3211 handlers.extend(clear_departed_participants(departed, cx));
3212 let refresh_order = state.read(cx).finish_registrations();
3213 let mut changes = state
3214 .read(cx)
3215 .changed_snapshots(cx)
3216 .into_iter()
3217 .map(|(participant, snapshot)| (participant.entity_id(), (participant, snapshot)))
3218 .collect::<HashMap<_, _>>();
3219 let drawn = std::mem::take(&mut *state.read(cx).draw_changed.borrow_mut());
3220 for (id, participant) in drawn {
3221 if let Some(participant) = participant.upgrade() {
3222 changes.entry(id).or_insert_with(|| {
3223 let snapshot = participant.read(cx).snapshot;
3224 (participant, snapshot)
3225 });
3226 }
3227 }
3228 for (_, (participant, snapshot)) in changes {
3229 participant.update(cx, |participant, cx| {
3230 participant.set_snapshot_quietly(snapshot);
3231 cx.emit(TextSelectionEvent::SelectionChanged(snapshot));
3232 });
3233 }
3234 dispatch_clear_handlers(handlers, cx);
3235 if refresh_order {
3236 window.refresh();
3240 }
3241 if state.read(cx).touch_edges_moved.replace(false) {
3242 state.update(cx, |state, cx| state.touch_changed(cx));
3243 }
3244 if state.read(cx).refresh_held_cursor.replace(false) && state.read(cx).is_selecting {
3247 state.update(cx, |state, cx| {
3248 let position = window.mouse_position();
3251 let position = state
3252 .touch
3253 .drag
3254 .map_or(position, |drag| drag.text_position(position));
3255 state.update_in_window(position, window, cx);
3256 });
3257 WindowSelectionState::resolve_content_keys(&state, cx);
3258 }
3259 });
3260 }
3261}
3262
3263fn paint_text_selection(state: &Entity<WindowSelectionState>, window: &mut Window, cx: &mut App) {
3264 schedule_selection_frame(state, window, cx);
3265
3266 let mouse_down_state = state.downgrade();
3267 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
3268 if event.button != MouseButton::Left {
3269 return;
3270 }
3271 let Some(state) = mouse_down_state.upgrade() else {
3272 return;
3273 };
3274 if state.read(cx).touch.covers(event.position) {
3277 return;
3278 }
3279 if phase.capture() {
3280 GlobalState::init(cx);
3281 GlobalState::reset_text_selection_suppression(cx);
3282 let handlers = state.update(cx, |state, cx| {
3283 if state.mouse_down_prepared {
3284 return Vec::new();
3285 }
3286 state.mouse_down_prepared = true;
3287 state.prepare_for_mouse_down(event.click_count == 1 && event.modifiers.shift, cx)
3288 });
3289 dispatch_clear_handlers(handlers, cx);
3290 } else if event.click_count == 1 {
3291 if GlobalState::is_text_selection_suppressed(cx) {
3292 state.update(cx, |state, _| state.pending_extension_anchor = None);
3293 return;
3294 }
3295 state.update(cx, |state, cx| {
3296 if !state.is_selecting {
3297 state.begin_in_window(event.position, event.modifiers.shift, window, cx)
3298 }
3299 });
3300 WindowSelectionState::resolve_content_keys(&state, cx);
3301 } else if event.click_count >= 2 {
3302 if GlobalState::is_text_selection_suppressed(cx) {
3303 return;
3304 }
3305 let touch = GlobalState::is_touch_press(cx);
3306 state.update(cx, |state, cx| {
3307 let unit = state.select_at(event.position, event.click_count, window, cx);
3308 if touch {
3311 state.keep_touch_selection(cx);
3312 } else if let Some(unit) = unit {
3313 state.hold_unit(unit, cx);
3314 }
3315 });
3316 WindowSelectionState::resolve_content_keys(&state, cx);
3317 }
3318 });
3319
3320 window.on_mouse_event(move |event: &TouchDragEvent, phase, _, cx| {
3323 if phase.capture() && event.phase == TouchPhase::Started {
3324 GlobalState::note_touch(cx);
3325 }
3326 });
3327
3328 let long_press_state = state.downgrade();
3331 window.on_mouse_event(move |event: &LongPressEvent, phase, window, cx| {
3332 if !phase.bubble() {
3333 return;
3334 }
3335 let Some(state) = long_press_state.upgrade() else {
3336 return;
3337 };
3338 if event.phase == TouchPhase::Started {
3339 if window.default_prevented()
3340 || state.read(cx).touch.covers(event.start_position)
3341 || !state.update(cx, |state, cx| {
3342 state
3343 .endpoint(event.start_position, Some(window), cx)
3344 .inside_text
3345 })
3346 {
3347 return;
3348 }
3349 GlobalState::init(cx);
3350 GlobalState::reset_text_selection_suppression(cx);
3351 let handlers = state.update(cx, |state, cx| state.prepare_for_mouse_down(false, cx));
3352 dispatch_clear_handlers(handlers, cx);
3353 let selected = state.update(cx, |state, cx| {
3354 state.select_at(event.start_position, 2, window, cx);
3355 state.anchor.is_some()
3356 });
3357 if !selected {
3358 return;
3359 }
3360 window.capture_long_press(&state);
3361 } else if !window.has_long_press_capture(&state) {
3362 return;
3363 } else {
3364 state.update(cx, |state, cx| match event.phase {
3365 TouchPhase::Moved => {
3366 state.is_selecting = true;
3367 state.update_in_window(event.position, window, cx);
3368 }
3369 TouchPhase::Ended | TouchPhase::Cancelled => {
3370 state.end(cx);
3371 state.keep_touch_selection(cx);
3374 }
3375 _ => {}
3376 });
3377 }
3378 window.prevent_default();
3379 cx.stop_propagation();
3380 WindowSelectionState::resolve_content_keys(&state, cx);
3381 });
3382
3383 let drag_state = state.downgrade();
3386 window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
3387 if !phase.bubble() || event.phase == TouchPhase::Started {
3388 return;
3389 }
3390 let Some(state) = drag_state.upgrade() else {
3391 return;
3392 };
3393 if state.read(cx).touch.drag.is_none() {
3394 return;
3395 }
3396 cx.stop_propagation();
3397 state.update(cx, |state, cx| match event.phase {
3398 TouchPhase::Moved => state.update_edge_drag(event.position, window, cx),
3399 _ => state.end_edge_drag(cx),
3400 });
3401 WindowSelectionState::resolve_content_keys(&state, cx);
3402 });
3403 let drag_state = state.downgrade();
3404 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
3405 if phase.bubble()
3406 && event.pressed_button == Some(MouseButton::Left)
3407 && let Some(state) = drag_state.upgrade()
3408 && state.read(cx).touch.drag.is_some()
3409 {
3410 state.update(cx, |state, cx| {
3411 state.update_edge_drag(event.position, window, cx)
3412 });
3413 WindowSelectionState::resolve_content_keys(&state, cx);
3414 }
3415 });
3416 let drag_state = state.downgrade();
3417 window.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| {
3418 if phase.bubble()
3419 && event.button == MouseButton::Left
3420 && let Some(state) = drag_state.upgrade()
3421 {
3422 state.update(cx, |state, cx| state.end_edge_drag(cx));
3423 }
3424 });
3425
3426 let mouse_move_state = state.downgrade();
3427 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
3428 if phase.bubble()
3429 && let Some(state) = mouse_move_state.upgrade()
3430 && state.read(cx).is_selecting
3432 {
3433 state.update(cx, |state, cx| {
3434 if state.touch.drag.is_some() {
3437 return;
3438 }
3439 state.update_in_window(event.position, window, cx)
3440 });
3441 WindowSelectionState::resolve_content_keys(&state, cx);
3442 }
3443 });
3444
3445 let mouse_up_state = state.downgrade();
3446 window.on_mouse_event(move |_: &MouseUpEvent, phase, _, cx| {
3447 if phase.bubble()
3448 && let Some(state) = mouse_up_state.upgrade()
3449 {
3450 state.update(cx, |state, cx| {
3451 state.mouse_down_prepared = false;
3452 state.end(cx)
3453 });
3454 }
3455 });
3456
3457 let scroll_state = state.downgrade();
3458 window.on_mouse_event(move |event: &ScrollEvent, phase, window, cx| {
3459 let Some(state) = scroll_state.upgrade() else {
3460 return;
3461 };
3462 if phase.capture() {
3466 if state.read(cx).touch.active {
3467 state.update(cx, |state, cx| {
3468 state.edit_menu_on_scroll(event.touch_phase, cx)
3469 });
3470 }
3471 return;
3472 }
3473 if phase.bubble() && state.read(cx).is_selecting {
3475 let position = window.mouse_position();
3476 state.update(cx, |state, cx| {
3477 let position = state
3479 .touch
3480 .drag
3481 .map_or(position, |drag| drag.text_position(position));
3482 state.update_in_window(position, window, cx)
3483 });
3484 WindowSelectionState::resolve_content_keys(&state, cx);
3485 }
3486 });
3487}
3488
3489fn live_text_selection_state(
3490 window: &Window,
3491 cx: &mut App,
3492) -> Option<Entity<WindowSelectionState>> {
3493 WindowSelectionState::existing(window, cx)
3494}
3495
3496pub(crate) fn clear_window_text_selection(window_id: gpui::WindowId, cx: &mut App) {
3497 if !cx.has_global::<SelectionStateRegistry>() {
3498 return;
3499 }
3500 let Some(state) = cx
3501 .global::<SelectionStateRegistry>()
3502 .0
3503 .get(&window_id)
3504 .and_then(WeakEntity::upgrade)
3505 else {
3506 return;
3507 };
3508 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
3509 dispatch_clear_handlers(handlers, cx);
3510}
3511
3512#[cfg(test)]
3513mod tests {
3514 use super::*;
3515 use crate::ElementExt as _;
3516 #[cfg(feature = "gpui-fast")]
3517 use gpui::StatefulInteractiveElement as _;
3518 use gpui::{
3519 Bounds, ContentMask, Context, Hitbox, HitboxBehavior, HitboxId, InteractiveElement as _,
3520 IntoElement, ParentElement as _, Render, SharedString, Styled as _, StyledText,
3521 TestAppContext, TextLayout, Window, div, point, prelude::FluentBuilder as _, px, size,
3522 };
3523 use std::{
3524 cell::{Cell, RefCell},
3525 rc::Rc,
3526 };
3527
3528 struct FakeParticipant {
3529 selection: TextSelectionHandle,
3530 }
3531
3532 struct WindowSelectionView {
3533 selection: TextSelectionHandle,
3534 }
3535
3536 struct SelectionElementOnlyView;
3537 struct ToggleSelectionElementView {
3538 enabled: bool,
3539 selection: TextSelectionHandle,
3540 }
3541
3542 struct DoubleSelectionElementView {
3543 selection: TextSelectionHandle,
3544 }
3545
3546 struct WindowOwnedSelectionView {
3547 selection: TextSelectionHandle,
3548 }
3549
3550 struct FirstFrameScopedSelectionView {
3551 selection: TextSelectionHandle,
3552 }
3553
3554 struct PlainRunLayoutView {
3555 texts: Vec<SharedString>,
3556 layouts: Vec<TextLayout>,
3557 }
3558
3559 impl Render for WindowSelectionView {
3560 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3561 div()
3562 }
3563 }
3564
3565 impl Render for SelectionElementOnlyView {
3566 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3567 div()
3568 .size_full()
3569 .child(TextSelectionLayer)
3570 .child(
3571 div()
3572 .size_full()
3573 .on_mouse_down(MouseButton::Left, |_, _, cx| {
3574 GlobalState::suppress_text_selection(cx);
3575 }),
3576 )
3577 }
3578 }
3579
3580 impl Render for ToggleSelectionElementView {
3581 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3582 let selection = self.selection.clone();
3583 div().when(self.enabled, |this| {
3584 this.child(TextSelectionLayer)
3585 .child(div().size_full().on_prepaint(move |bounds, window, cx| {
3586 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3587 selection.register(
3588 TextSelectionRegistration::new(hitbox, bounds)
3589 .with_text_bounds(vec![bounds]),
3590 window,
3591 cx,
3592 );
3593 }))
3594 })
3595 }
3596 }
3597
3598 impl Render for DoubleSelectionElementView {
3599 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3600 let selection = self.selection.clone();
3601 div()
3602 .size_full()
3603 .child(TextSelectionLayer)
3604 .child(TextSelectionLayer)
3605 .on_prepaint(move |bounds, window, cx| {
3606 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3607 selection.register(
3608 TextSelectionRegistration::new(hitbox, bounds)
3609 .with_text_bounds(vec![bounds]),
3610 window,
3611 cx,
3612 );
3613 })
3614 }
3615 }
3616
3617 impl Render for WindowOwnedSelectionView {
3618 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3619 let selection = self.selection.clone();
3620 div()
3621 .size_full()
3622 .child(TextSelectionLayer)
3623 .child(div().size_full().on_prepaint(move |bounds, window, cx| {
3624 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3625 selection.register(
3626 TextSelectionRegistration::new(hitbox, bounds)
3627 .with_text_bounds(vec![bounds]),
3628 window,
3629 cx,
3630 );
3631 }))
3632 }
3633 }
3634
3635 impl Render for FirstFrameScopedSelectionView {
3636 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3637 let scope = TextSelectionScopeId::from_raw(23);
3638 TextSelection::activate_scope(scope, window, cx);
3639 let selection = self.selection.clone();
3640
3641 div().child(TextSelectionLayer).child(
3642 div()
3643 .size_full()
3644 .on_prepaint(move |bounds, window, cx| {
3645 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
3646 selection.register(
3647 TextSelectionRegistration::new(hitbox, bounds)
3648 .with_text_bounds(vec![bounds]),
3649 window,
3650 cx,
3651 );
3652 })
3653 .text_selection_scope(scope),
3654 )
3655 }
3656 }
3657
3658 impl Render for PlainRunLayoutView {
3659 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3660 self.layouts.clear();
3661 let children = self
3662 .texts
3663 .iter()
3664 .enumerate()
3665 .map(|(index, text)| {
3666 let text = StyledText::new(text.clone());
3667 self.layouts.push(text.layout().clone());
3668 div().absolute().top(px(index as f32 * 40.)).child(text)
3669 })
3670 .collect::<Vec<_>>();
3671 div().size_full().children(children)
3672 }
3673 }
3674
3675 impl FakeParticipant {
3676 fn new(text: &str, cx: &mut gpui::App) -> Self {
3677 let selection = TextSelectionHandle::new(text, cx);
3678 Self { selection }
3679 }
3680
3681 fn register(
3682 &self,
3683 selection_state: &mut WindowSelectionState,
3684 y: f32,
3685 scope: TextSelectionScopeId,
3686 document_order: u64,
3687 cx: &mut gpui::App,
3688 ) {
3689 let bounds = Bounds::new(point(px(0.), px(y)), size(px(100.), px(10.)));
3690 selection_state.register_participant(
3691 self.selection.clone(),
3692 TextSelectionRegistration::new(
3693 Hitbox {
3694 id: HitboxId::placeholder(),
3695 bounds,
3696 content_mask: ContentMask { bounds },
3697 behavior: HitboxBehavior::Normal,
3698 },
3699 bounds,
3700 )
3701 .with_scope(scope)
3702 .with_document_order(document_order)
3703 .with_text_bounds(vec![bounds]),
3704 cx,
3705 );
3706 }
3707 }
3708
3709 fn laid_out_runs(texts: &[&str], cx: &mut TestAppContext) -> Vec<(SharedString, TextLayout)> {
3710 let texts = texts
3711 .iter()
3712 .map(|text| SharedString::from(*text))
3713 .collect::<Vec<_>>();
3714 let view = cx.add_window({
3715 let texts = texts.clone();
3716 move |_, _| PlainRunLayoutView {
3717 texts,
3718 layouts: Vec::new(),
3719 }
3720 });
3721 cx.update_window(*view, |_, window, cx| {
3722 let _ = window.draw(cx);
3723 })
3724 .unwrap();
3725 let layouts = cx.update(|cx| view.read(cx).unwrap().layouts.clone());
3726 texts.into_iter().zip(layouts).collect()
3727 }
3728
3729 fn plain_snapshot(anchor: Point<Pixels>, cursor: Point<Pixels>) -> TextSelectionSnapshot {
3730 TextSelectionSnapshot::new(
3731 TextSelectionEndpoint::new(None, anchor),
3732 TextSelectionEndpoint::new(None, cursor),
3733 )
3734 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
3735 }
3736
3737 #[gpui::test]
3738 fn scope_stack_is_cleaned_after_panicking_subtree(cx: &mut TestAppContext) {
3739 let window_id = {
3740 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3741 window_cx.update(|window, _| window.window_handle().window_id())
3742 };
3743 let scope = TextSelectionScopeId::from_raw(41);
3744
3745 cx.update(|cx| {
3746 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3747 with_text_selection_scope(window_id, scope, cx, |_| panic!("subtree failed"));
3748 }));
3749
3750 assert!(result.is_err());
3751 assert_eq!(current_text_selection_scope(window_id, cx), None);
3752 });
3753 }
3754
3755 #[gpui::test]
3756 fn reentrant_scope_from_one_window_does_not_pollute_another(cx: &mut TestAppContext) {
3757 let first_window_id = {
3758 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3759 window_cx.update(|window, _| window.window_handle().window_id())
3760 };
3761 let second_window_id = {
3762 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3763 window_cx.update(|window, _| window.window_handle().window_id())
3764 };
3765 let scope = TextSelectionScopeId::from_raw(42);
3766
3767 cx.update(|cx| {
3768 with_text_selection_scope(first_window_id, scope, cx, |cx| {
3769 assert_eq!(current_text_selection_scope(second_window_id, cx), None);
3770 assert_eq!(
3771 current_text_selection_scope(first_window_id, cx),
3772 Some(scope)
3773 );
3774 });
3775 });
3776 }
3777
3778 #[gpui::test]
3779 fn selection_callback_can_reenter_its_selection_state(cx: &mut TestAppContext) {
3780 let called = Rc::new(Cell::new(false));
3781 let called_from_callback = called.clone();
3782 let (selection_state, participant) = cx.update(|cx| {
3783 let selection_state = cx.new(|_| WindowSelectionState::default());
3784 let selection_state_for_callback = selection_state.clone();
3785 let participant = FakeParticipant::new("participant", cx);
3786 participant
3787 .selection
3788 .subscribe(
3789 move |event, cx| {
3790 if matches!(event, TextSelectionEvent::SelectionChanged(Some(_))) {
3791 selection_state_for_callback
3792 .update(cx, |_, _| called_from_callback.set(true));
3793 }
3794 },
3795 cx,
3796 )
3797 .detach();
3798 (selection_state, participant)
3799 });
3800 cx.run_until_parked();
3801 cx.update(|cx| {
3802 selection_state.update(cx, |selection_state, cx| {
3803 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx);
3804 selection_state.begin(point(px(1.), px(1.)), false, cx);
3805 selection_state.update(point(px(20.), px(1.)), cx);
3806 });
3807 });
3808 cx.run_until_parked();
3809 assert!(called.get());
3810 }
3811
3812 #[gpui::test]
3813 fn selection_events_preserve_snapshot_then_clear_order(cx: &mut TestAppContext) {
3814 let observed = Rc::new(RefCell::new(Vec::new()));
3815 let observed_for_callback = observed.clone();
3816 let selection = cx.update(|cx| {
3817 let selection = TextSelectionHandle::new("selection", cx);
3818 selection
3819 .subscribe(
3820 move |event, _| {
3821 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
3822 observed_for_callback.borrow_mut().push(snapshot.is_some());
3823 }
3824 },
3825 cx,
3826 )
3827 .detach();
3828 selection
3829 });
3830 cx.run_until_parked();
3831 cx.update(|cx| {
3832 selection.0.update(cx, |state, cx| {
3833 state.set_snapshot(
3834 Some(plain_snapshot(point(px(1.), px(1.)), point(px(8.), px(1.)))),
3835 cx,
3836 );
3837 state.clear_state(cx);
3838 });
3839 });
3840 cx.run_until_parked();
3841 assert_eq!(&*observed.borrow(), &[true, false]);
3842 }
3843
3844 fn text_run(order: u64, text: SharedString, layout: TextLayout) -> TextSelectionRun {
3845 let bounds = layout.bounds();
3846 TextSelectionRun::new(text, layout, bounds).with_document_order(order)
3847 }
3848
3849 #[gpui::test]
3850 fn public_selection_data_uses_builders_and_readers(cx: &mut TestAppContext) {
3851 let bounds = Bounds::new(point(px(1.), px(2.)), size(px(30.), px(10.)));
3852 let hitbox = Hitbox {
3853 id: HitboxId::placeholder(),
3854 bounds,
3855 content_mask: ContentMask { bounds },
3856 behavior: HitboxBehavior::Normal,
3857 };
3858 let scope = TextSelectionScopeId::from_raw(7);
3859 let endpoint = TextSelectionEndpoint::new(None, bounds.origin)
3860 .with_content_key(TextSelectionContentKey::new(11));
3861 let snapshot = TextSelectionSnapshot::new(endpoint, endpoint)
3862 .with_selecting(true)
3863 .with_window_points(Some(TextSelectionWindowPoints {
3864 anchor: bounds.origin,
3865 cursor: bounds.bottom_right(),
3866 }))
3867 .with_coverage(TextSelectionCoverage::Full);
3868 let registration = TextSelectionRegistration::new(hitbox, bounds)
3869 .with_scroll_offset(point(px(3.), px(4.)))
3870 .with_scope(scope)
3871 .with_document_order(9)
3872 .with_text_bounds(vec![bounds]);
3873
3874 assert_eq!(endpoint.entity_id(), None);
3875 assert_eq!(endpoint.content_point(), bounds.origin);
3876 assert_eq!(
3877 endpoint.content_key(),
3878 Some(TextSelectionContentKey::new(11))
3879 );
3880 assert_eq!(snapshot.anchor(), endpoint);
3881 assert_eq!(snapshot.cursor(), endpoint);
3882 assert!(snapshot.is_selecting());
3883 assert_eq!(snapshot.coverage(), TextSelectionCoverage::Full);
3884 assert_eq!(
3885 snapshot.window_points(),
3886 Some(TextSelectionWindowPoints {
3887 anchor: bounds.origin,
3888 cursor: bounds.bottom_right(),
3889 })
3890 );
3891 assert_eq!(registration.bounds(), bounds);
3892 assert_eq!(registration.scroll_offset(), point(px(3.), px(4.)));
3893 assert_eq!(registration.scope(), scope);
3894 assert_eq!(registration.document_order(), 9);
3895 assert_eq!(registration.text_bounds(), &[bounds]);
3896
3897 let (text, layout) = laid_out_runs(&["aé"], cx).pop().unwrap();
3898 let text_run = TextSelectionRun::new(text.clone(), layout.clone(), layout.bounds())
3899 .with_document_order(3);
3900 assert_eq!(text_run.document_order(), 3);
3901 assert_eq!(text_run.text(), &text);
3902 assert_eq!(text_run.layout().len(), layout.len());
3903 assert_eq!(text_run.bounds(), layout.bounds());
3904
3905 let projection = TextSelectionProjection {
3906 ranges: vec![Some(1..3)],
3907 is_active: true,
3908 };
3909 assert_eq!(projection.ranges(), &[Some(1..3)]);
3910 assert!(projection.is_active());
3911 }
3912
3913 #[gpui::test]
3914 fn selection_handle_is_the_public_adapter_seam(cx: &mut TestAppContext) {
3915 let selected = Rc::new(Cell::new(false));
3916 let selected_from_callback = selected.clone();
3917 cx.update(|cx| {
3918 let selection = TextSelectionHandle::new("initial", cx);
3919 let entity_id = selection.entity_id();
3920 selection.set_fallback_copy_text("updated", cx);
3921 selection.set_local_selection(true, cx);
3922 selection
3923 .subscribe(
3924 move |event, _| {
3925 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
3926 selected_from_callback.set(snapshot.is_some());
3927 }
3928 },
3929 cx,
3930 )
3931 .detach();
3932 selection.focus_with(|_, _| {}, cx);
3933 selection.copy_with(|_| "copied".to_string(), cx);
3934 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(3)), cx);
3935
3936 assert_eq!(selection.entity_id(), entity_id);
3937 assert_eq!(selection.snapshot(cx), None);
3938 assert_eq!(
3939 selection.update_runs(&[], cx),
3940 TextSelectionProjection::default()
3941 );
3942 });
3943 assert!(!selected.get());
3944 }
3945
3946 #[gpui::test]
3947 fn selection_handle_can_subscribe_its_window_to_refresh(cx: &mut TestAppContext) {
3948 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3949 selection: TextSelectionHandle::new("refresh", cx),
3950 });
3951 cx.update(|window, cx| {
3952 let selection = TextSelectionHandle::new("refresh", cx);
3953 selection.refresh_window_on_change(window, cx).detach();
3954 });
3955 }
3956
3957 #[gpui::test]
3958 fn plain_projection_preserves_forward_reversed_and_unicode_ranges(cx: &mut TestAppContext) {
3959 let (text, layout) = laid_out_runs(&["aé🙂z"], cx).pop().unwrap();
3960 let run = text_run(0, text, layout.clone());
3961 let start = layout.position_for_index(1).unwrap();
3962 let end = layout.position_for_index(7).unwrap();
3963
3964 let forward = project_ranges(Some(plain_snapshot(start, end)), std::slice::from_ref(&run));
3965 let reversed = project_ranges(Some(plain_snapshot(end, start)), &[run]);
3966
3967 assert_eq!(forward.ranges(), &[Some(1..7)]);
3968 assert_eq!(reversed.ranges(), &[Some(1..7)]);
3969 assert!(forward.is_active());
3970 assert!(reversed.is_active());
3971 }
3972
3973 #[gpui::test]
3974 fn double_click_expands_a_plain_run_to_the_input_word_boundary(cx: &mut TestAppContext) {
3975 let (text, layout) = laid_out_runs(&["one café, three"], cx).pop().unwrap();
3976 let run = text_run(0, text, layout.clone());
3977 let click = layout.position_for_index(6).unwrap();
3978
3979 let (anchor, cursor) =
3980 carets_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
3981 let states = project_ranges(Some(plain_snapshot(anchor.origin, cursor.origin)), &[run]);
3982
3983 assert_eq!(states.ranges(), &[Some(4..9)]);
3984 }
3985
3986 #[gpui::test]
3987 fn multi_click_uses_text_layout_window_coordinates_at_a_nonzero_origin(
3988 cx: &mut TestAppContext,
3989 ) {
3990 let mut runs = laid_out_runs(&["above", "alpha beta"], cx);
3991 let (text, layout) = runs.pop().unwrap();
3992 assert!(layout.bounds().origin.y > px(0.));
3993 let run = text_run(0, text, layout.clone());
3994 let click = layout.position_for_index(7).unwrap();
3995
3996 let (anchor, cursor) =
3997 carets_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
3998 let projection = project_ranges(Some(plain_snapshot(anchor.origin, cursor.origin)), &[run]);
3999
4000 assert_eq!(projection.ranges(), &[Some(6..10)]);
4001 }
4002
4003 #[gpui::test]
4004 fn triple_click_expands_to_the_input_logical_line_not_the_visual_row(cx: &mut TestAppContext) {
4005 let (text, layout) = laid_out_runs(&["second line"], cx).pop().unwrap();
4006 let run = text_run(0, text, layout.clone());
4007 let click = layout.position_for_index(4).unwrap();
4008
4009 let (anchor, cursor) =
4010 carets_for_multi_click(std::slice::from_ref(&run), click, 4).unwrap();
4011 let states = project_ranges(Some(plain_snapshot(anchor.origin, cursor.origin)), &[run]);
4012
4013 assert_eq!(states.ranges(), &[Some(0..11)]);
4014 assert_eq!(line_range_at("first line\nsecond line\nthird", 15), 11..22);
4015 }
4016
4017 #[gpui::test]
4018 fn plain_projection_spans_multiple_runs_and_leaves_empty_gutters_unselected(
4019 cx: &mut TestAppContext,
4020 ) {
4021 let mut runs = laid_out_runs(&["first", "", "second"], cx);
4022 let (first_text, first_layout) = runs.remove(0);
4023 let (gutter_text, gutter_layout) = runs.remove(0);
4024 let (second_text, second_layout) = runs.remove(0);
4025 let start = first_layout.position_for_index(2).unwrap();
4026 let end = second_layout.position_for_index(3).unwrap();
4027 let states = project_ranges(
4028 Some(plain_snapshot(start, end)),
4029 &[
4030 text_run(2, second_text, second_layout),
4031 text_run(1, gutter_text, gutter_layout),
4032 text_run(0, first_text, first_layout),
4033 ],
4034 );
4035
4036 assert_eq!(states.ranges(), &[Some(0..3), None, Some(2..5)]);
4037 assert!(states.is_active());
4038 }
4039
4040 #[gpui::test]
4041 fn plain_projection_caches_multiple_participant_copies_in_document_order(
4042 cx: &mut TestAppContext,
4043 ) {
4044 let mut runs = laid_out_runs(&["one", "two"], cx);
4045 let (first_text, first_layout) = runs.remove(0);
4046 let (second_text, second_layout) = runs.remove(0);
4047 let snapshot = plain_snapshot(
4048 first_layout.position_for_index(1).unwrap(),
4049 second_layout.position_for_index(2).unwrap(),
4050 );
4051 cx.update(|cx| {
4052 let mut selection_state = WindowSelectionState::default();
4053 let first = FakeParticipant::new("", cx);
4054 let second = FakeParticipant::new("", cx);
4055 first.register(
4056 &mut selection_state,
4057 0.,
4058 TextSelectionScopeId::default(),
4059 1,
4060 cx,
4061 );
4062 second.register(
4063 &mut selection_state,
4064 20.,
4065 TextSelectionScopeId::default(),
4066 0,
4067 cx,
4068 );
4069
4070 first
4071 .selection
4072 .0
4073 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
4074 let projection = first
4075 .selection
4076 .update_runs(&[text_run(0, first_text, first_layout)], cx);
4077 assert_eq!(projection.ranges(), &[Some(1..3)]);
4078 assert!(projection.is_active());
4079 second
4080 .selection
4081 .0
4082 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
4083 let projection = second
4084 .selection
4085 .update_runs(&[text_run(0, second_text, second_layout)], cx);
4086 assert_eq!(projection.ranges(), &[Some(0..2)]);
4087 assert!(projection.is_active());
4088
4089 assert_eq!(selection_state.selected_text(cx), "tw\nne");
4090 });
4091 }
4092
4093 #[gpui::test]
4094 fn plain_projection_invalidates_cached_copy_when_the_snapshot_changes(cx: &mut TestAppContext) {
4095 let (text, layout) = laid_out_runs(&["first"], cx).pop().unwrap();
4096 let first_snapshot = plain_snapshot(
4097 layout.position_for_index(1).unwrap(),
4098 layout.position_for_index(3).unwrap(),
4099 );
4100 let changed_snapshot = plain_snapshot(
4101 layout.position_for_index(3).unwrap(),
4102 layout.position_for_index(5).unwrap(),
4103 );
4104 let run = text_run(0, text, layout);
4105 cx.update(|cx| {
4106 let mut selection_state = WindowSelectionState::default();
4107 let participant = FakeParticipant::new("", cx);
4108 participant.register(
4109 &mut selection_state,
4110 0.,
4111 TextSelectionScopeId::default(),
4112 0,
4113 cx,
4114 );
4115 participant.selection.0.update(cx, |state, cx| {
4116 state.set_snapshot(Some(first_snapshot), cx);
4117 state.update_runs(std::slice::from_ref(&run));
4118 });
4119 assert_eq!(selection_state.selected_text(cx), "ir");
4120
4121 participant.selection.0.update(cx, |state, cx| {
4122 state.set_snapshot(Some(changed_snapshot), cx);
4123 });
4124 assert_eq!(selection_state.selected_text(cx), "");
4125
4126 participant.selection.update_runs(&[run], cx);
4127 assert_eq!(selection_state.selected_text(cx), "st");
4128 selection_state.clear(cx);
4129 participant.selection.set_local_selection(true, cx);
4130 assert_eq!(selection_state.selected_text(cx), "");
4131 });
4132 }
4133
4134 #[gpui::test]
4135 fn plain_projection_orders_cached_runs_by_frame_order_not_input_order(cx: &mut TestAppContext) {
4136 let mut runs = laid_out_runs(&["one", "two"], cx);
4137 let (first_text, first_layout) = runs.remove(0);
4138 let (second_text, second_layout) = runs.remove(0);
4139 let snapshot = plain_snapshot(
4140 first_layout.position_for_index(1).unwrap(),
4141 second_layout.position_for_index(2).unwrap(),
4142 );
4143 cx.update(|cx| {
4144 let mut selection_state = WindowSelectionState::default();
4145 let participant = FakeParticipant::new("", cx);
4146 participant.register(
4147 &mut selection_state,
4148 0.,
4149 TextSelectionScopeId::default(),
4150 0,
4151 cx,
4152 );
4153 participant.selection.0.update(cx, |state, cx| {
4154 state.set_snapshot(Some(snapshot), cx);
4155 state.update_runs(&[
4156 text_run(1, first_text, first_layout),
4157 text_run(0, second_text, second_layout),
4158 ]);
4159 });
4160
4161 assert_eq!(selection_state.selected_text(cx), "twne");
4162 });
4163 }
4164
4165 #[gpui::test]
4166 fn plain_projection_safely_rejects_a_text_layout_length_mismatch(cx: &mut TestAppContext) {
4167 let (_, layout) = laid_out_runs(&["short"], cx).pop().unwrap();
4168 let start = layout.position_for_index(0).unwrap();
4169 let end = layout.position_for_index(5).unwrap();
4170 let states = project_ranges(
4171 Some(plain_snapshot(start, end)),
4172 &[text_run(0, SharedString::from("longer"), layout)],
4173 );
4174
4175 assert_eq!(states.ranges(), &[None]);
4176 assert!(states.is_active());
4177 }
4178
4179 #[gpui::test]
4180 fn begin_update_and_end_publish_a_cross_participant_selection(cx: &mut TestAppContext) {
4181 cx.update(|cx| {
4182 let mut selection_state = WindowSelectionState::default();
4183 let first = FakeParticipant::new("first", cx);
4184 let second = FakeParticipant::new("second", cx);
4185 first.register(
4186 &mut selection_state,
4187 0.,
4188 TextSelectionScopeId::default(),
4189 0,
4190 cx,
4191 );
4192 second.register(
4193 &mut selection_state,
4194 20.,
4195 TextSelectionScopeId::default(),
4196 1,
4197 cx,
4198 );
4199
4200 selection_state.begin(point(px(1.), px(1.)), false, cx);
4201 selection_state.update(point(px(1.), px(25.)), cx);
4202 assert!(selection_state.has_selection(cx));
4203 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
4204
4205 selection_state.end(cx);
4206 assert!(!selection_state.is_selecting());
4207 });
4208 }
4209
4210 #[gpui::test]
4211 fn shift_extension_keeps_its_original_anchor_when_reversed(cx: &mut TestAppContext) {
4212 cx.update(|cx| {
4213 let mut selection_state = WindowSelectionState::default();
4214 let participant = FakeParticipant::new("participant", cx);
4215 participant.register(
4216 &mut selection_state,
4217 0.,
4218 TextSelectionScopeId::default(),
4219 0,
4220 cx,
4221 );
4222
4223 selection_state.begin(point(px(2.), px(2.)), false, cx);
4224 selection_state.end(cx);
4225 selection_state.begin(point(px(8.), px(2.)), true, cx);
4226 selection_state.end(cx);
4227 let first_anchor = selection_state.snapshot().unwrap().anchor();
4228
4229 selection_state.begin(point(px(0.), px(2.)), true, cx);
4230 selection_state.end(cx);
4231 let reversed = selection_state.snapshot().unwrap();
4232 assert_eq!(reversed.anchor(), first_anchor);
4233 assert!(reversed.cursor().content_point().x < reversed.anchor().content_point().x);
4234 });
4235 }
4236
4237 #[gpui::test]
4238 fn content_key_resolver_runs_outside_the_window_state_lease(cx: &mut TestAppContext) {
4239 cx.update(|cx| {
4240 let state = cx.new(|_| WindowSelectionState::default());
4241 let participant = FakeParticipant::new("virtual", cx);
4242 let state_for_callback = state.clone();
4243 participant.selection.resolve_content_key_with(
4244 move |_, cx| {
4245 let _ = state_for_callback.read(cx).snapshot();
4246 Some(TextSelectionContentKey::new(7))
4247 },
4248 cx,
4249 );
4250 state.update(cx, |state, cx| {
4251 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
4252 state.begin(point(px(1.), px(1.)), false, cx);
4253 state.update(point(px(8.), px(1.)), cx);
4254 });
4255
4256 WindowSelectionState::resolve_content_keys(&state, cx);
4257
4258 assert_eq!(
4259 state.read(cx).snapshot().unwrap().cursor().content_key(),
4260 Some(TextSelectionContentKey::new(7))
4261 );
4262 });
4263 }
4264
4265 #[gpui::test]
4266 fn active_dnd_does_not_move_a_text_selection_cursor(cx: &mut TestAppContext) {
4267 let window = cx.add_window(|_, cx| WindowSelectionView {
4268 selection: TextSelectionHandle::new("unused", cx),
4269 });
4270 window
4271 .update(cx, |_, window, cx| {
4272 let mut state = WindowSelectionState::default();
4273 let participant = FakeParticipant::new("participant", cx);
4274 participant.register(&mut state, 0., TextSelectionScopeId::default(), 0, cx);
4275 state.begin(point(px(1.), px(1.)), false, cx);
4276 let before = state.cursor.as_ref().unwrap().point;
4277 state.update_in_window_with_active_drag(point(px(80.), px(1.)), true, window, cx);
4278 assert_eq!(state.cursor.as_ref().unwrap().point, before);
4279 })
4280 .unwrap();
4281 }
4282
4283 #[gpui::test]
4284 fn shift_extension_falls_back_when_the_anchor_participant_was_swept(cx: &mut TestAppContext) {
4285 cx.update(|cx| {
4286 let mut selection_state = WindowSelectionState::default();
4287 let first = FakeParticipant::new("first", cx);
4288 let second = FakeParticipant::new("second", cx);
4289 first.register(
4290 &mut selection_state,
4291 0.,
4292 TextSelectionScopeId::default(),
4293 0,
4294 cx,
4295 );
4296 selection_state.begin(point(px(1.), px(1.)), false, cx);
4297 selection_state.update(point(px(8.), px(1.)), cx);
4298 selection_state.end(cx);
4299
4300 selection_state.finish_frame(cx);
4301 selection_state.finish_frame(cx);
4302 second.register(
4303 &mut selection_state,
4304 20.,
4305 TextSelectionScopeId::default(),
4306 1,
4307 cx,
4308 );
4309 selection_state.begin(point(px(1.), px(21.)), true, cx);
4310 selection_state.update(point(px(8.), px(21.)), cx);
4311 selection_state.end(cx);
4312
4313 assert_eq!(selection_state.selected_text(cx), "second");
4314 });
4315 }
4316
4317 #[gpui::test]
4318 fn scope_and_suppression_prevent_unrelated_participants_from_participating(
4319 cx: &mut TestAppContext,
4320 ) {
4321 cx.update(|cx| {
4322 let mut selection_state = WindowSelectionState::default();
4323 let base = FakeParticipant::new("base", cx);
4324 let modal = FakeParticipant::new("modal", cx);
4325 base.register(
4326 &mut selection_state,
4327 0.,
4328 TextSelectionScopeId::default(),
4329 0,
4330 cx,
4331 );
4332 modal.register(&mut selection_state, 20., TextSelectionScopeId(1), 1, cx);
4333
4334 selection_state.set_active_scope(TextSelectionScopeId(1), cx);
4335 selection_state.begin(point(px(1.), px(21.)), false, cx);
4336 selection_state.update(point(px(8.), px(21.)), cx);
4337 selection_state.end(cx);
4338 assert_eq!(selection_state.selected_text(cx), "modal");
4339
4340 selection_state.clear(cx);
4341 GlobalState::init(cx);
4342 GlobalState::suppress_text_selection(cx);
4343 selection_state.begin(point(px(1.), px(21.)), false, cx);
4344 selection_state.update(point(px(8.), px(21.)), cx);
4345 assert!(!selection_state.has_selection(cx));
4346 });
4347 }
4348
4349 #[gpui::test]
4350 fn dead_participants_are_pruned_and_empty_selection_falls_back_safely(cx: &mut TestAppContext) {
4351 let selection_state = cx.update(|cx| {
4352 let selection_state = cx.new(|_| WindowSelectionState::default());
4353 let participant = FakeParticipant::new("gone", cx);
4354 selection_state.update(cx, |selection_state, cx| {
4355 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx)
4356 });
4357 selection_state
4358 });
4359 cx.update(|cx| {
4360 selection_state.update(cx, |selection_state, cx| {
4361 selection_state.begin(point(px(1.), px(1.)), false, cx);
4362 selection_state.update(point(px(8.), px(1.)), cx);
4363 selection_state.end(cx);
4364
4365 assert_eq!(selection_state.selected_text(cx), "");
4366 assert!(!selection_state.has_selection(cx));
4367 });
4368 });
4369 }
4370
4371 #[gpui::test]
4372 fn text_selection_namespace_reports_copies_ends_and_clears_selection(cx: &mut TestAppContext) {
4373 let (view, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
4374 selection: TextSelectionHandle::new("copied", cx),
4375 });
4376 cx.update(|window, cx| {
4377 let selection = view.read(cx).selection.clone();
4378 let selection_state = WindowSelectionState::ensure(window, cx);
4379 selection_state.update(cx, |selection_state, cx| {
4380 FakeParticipant { selection }.register(
4381 selection_state,
4382 0.,
4383 TextSelectionScopeId::default(),
4384 0,
4385 cx,
4386 );
4387 selection_state.begin(point(px(1.), px(1.)), false, cx);
4388 selection_state.update(point(px(8.), px(1.)), cx);
4389 });
4390
4391 assert!(TextSelection::has_selection(window, cx));
4392 assert_eq!(TextSelection::selected_text(window, cx), "copied");
4393 TextSelection::end(window, cx);
4394 assert!(TextSelection::has_selection(window, cx));
4395 TextSelection::clear(window, cx);
4396 assert!(!TextSelection::has_selection(window, cx));
4397 assert_eq!(TextSelection::selected_text(window, cx), "");
4398 });
4399 }
4400
4401 #[gpui::test]
4402 fn two_windows_isolate_selection_copy_clear_and_release_ownership(cx: &mut TestAppContext) {
4403 let first = cx.add_window(|_, cx| WindowOwnedSelectionView {
4404 selection: TextSelectionHandle::new("first", cx),
4405 });
4406 let second = cx.add_window(|_, cx| WindowOwnedSelectionView {
4407 selection: TextSelectionHandle::new("second", cx),
4408 });
4409 let first_selection = cx.update(|cx| first.read(cx).unwrap().selection.clone());
4410 let second_selection = cx.update(|cx| second.read(cx).unwrap().selection.clone());
4411
4412 let first_state = cx
4413 .update_window(*first, |_, window, cx| {
4414 let _ = window.draw(cx);
4415 first_selection.set_local_selection(true, cx);
4416 assert_eq!(TextSelection::selected_text(window, cx), "first");
4417 WindowSelectionState::existing(window, cx)
4418 .unwrap()
4419 .downgrade()
4420 })
4421 .unwrap();
4422 cx.update_window(*second, |_, window, cx| {
4423 let _ = window.draw(cx);
4424 second_selection.set_local_selection(true, cx);
4425 assert_eq!(TextSelection::selected_text(window, cx), "second");
4426 })
4427 .unwrap();
4428
4429 cx.update_window(*first, |_, window, cx| {
4430 TextSelection::clear(window, cx);
4431 assert_eq!(TextSelection::selected_text(window, cx), "");
4432 })
4433 .unwrap();
4434 cx.update_window(*second, |_, window, cx| {
4435 assert_eq!(TextSelection::selected_text(window, cx), "second");
4436 })
4437 .unwrap();
4438
4439 cx.update_window(*first, |_, window, _| window.remove_window())
4440 .unwrap();
4441 cx.run_until_parked();
4442
4443 assert!(first_state.upgrade().is_none());
4444 cx.update_window(*second, |_, window, cx| {
4445 assert_eq!(TextSelection::selected_text(window, cx), "second");
4446 })
4447 .unwrap();
4448 cx.update(|cx| {
4449 assert_eq!(cx.global::<SelectionStateRegistry>().0.len(), 1);
4450 });
4451 }
4452
4453 #[gpui::test]
4454 fn copy_callback_can_reenter_window_and_handle_selection(cx: &mut TestAppContext) {
4455 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
4456 cx.update(|window, cx| {
4457 let _ = window.draw(cx);
4458 let state = WindowSelectionState::existing(window, cx).unwrap();
4459 let selection = TextSelectionHandle::new("fallback", cx);
4460 let state_for_copy = state.clone();
4461 let selection_for_copy = selection.clone();
4462 selection.copy_with(
4463 move |cx: &mut App| {
4464 state_for_copy.update(cx, |state, _| {
4465 assert!(state.snapshot().is_some());
4466 });
4467 assert!(selection_for_copy.snapshot(cx).is_some());
4468 selection_for_copy.set_fallback_copy_text("reentered", cx);
4469 "reentrant copy".to_string()
4470 },
4471 cx,
4472 );
4473 state.update(cx, |state, cx| {
4474 FakeParticipant {
4475 selection: selection.clone(),
4476 }
4477 .register(state, 0., TextSelectionScopeId::default(), 0, cx);
4478 state.begin(point(px(1.), px(1.)), false, cx);
4479 state.update(point(px(8.), px(1.)), cx);
4480 state.end(cx);
4481 });
4482
4483 assert_eq!(TextSelection::selected_text(window, cx), "reentrant copy");
4484 });
4485 }
4486
4487 #[gpui::test]
4488 fn cross_participant_selection_excludes_participants_outside_its_document_interval(
4489 cx: &mut TestAppContext,
4490 ) {
4491 cx.update(|cx| {
4492 let mut selection_state = WindowSelectionState::default();
4493 let first = FakeParticipant::new("first", cx);
4494 let second = FakeParticipant::new("second", cx);
4495 let third = FakeParticipant::new("third", cx);
4496 first.register(
4497 &mut selection_state,
4498 0.,
4499 TextSelectionScopeId::default(),
4500 0,
4501 cx,
4502 );
4503 second.register(
4504 &mut selection_state,
4505 20.,
4506 TextSelectionScopeId::default(),
4507 1,
4508 cx,
4509 );
4510 third.register(
4511 &mut selection_state,
4512 40.,
4513 TextSelectionScopeId::default(),
4514 2,
4515 cx,
4516 );
4517
4518 selection_state.begin(point(px(1.), px(1.)), false, cx);
4519 selection_state.update(point(px(1.), px(25.)), cx);
4520 selection_state.end(cx);
4521
4522 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
4523 assert!(third.selection.snapshot(cx).is_none());
4524 });
4525 }
4526
4527 #[gpui::test]
4528 fn changing_scope_clears_the_previous_scope_selection(cx: &mut TestAppContext) {
4529 cx.update(|cx| {
4530 let mut selection_state = WindowSelectionState::default();
4531 let base = FakeParticipant::new("base", cx);
4532 let modal = FakeParticipant::new("modal", cx);
4533 base.register(
4534 &mut selection_state,
4535 0.,
4536 TextSelectionScopeId::default(),
4537 0,
4538 cx,
4539 );
4540 modal.register(
4541 &mut selection_state,
4542 20.,
4543 TextSelectionScopeId::from_raw(1),
4544 1,
4545 cx,
4546 );
4547
4548 selection_state.begin(point(px(1.), px(1.)), false, cx);
4549 selection_state.update(point(px(8.), px(1.)), cx);
4550 selection_state.end(cx);
4551 selection_state.set_active_scope(TextSelectionScopeId::from_raw(1), cx);
4552
4553 assert!(!selection_state.has_selection(cx));
4554 assert!(base.selection.snapshot(cx).is_none());
4555 });
4556 }
4557
4558 #[gpui::test]
4559 fn blank_only_drag_never_publishes_or_copies_selection(cx: &mut TestAppContext) {
4560 cx.update(|cx| {
4561 let mut selection_state = WindowSelectionState::default();
4562 let participant = FakeParticipant::new("participant", cx);
4563 participant.register(
4564 &mut selection_state,
4565 0.,
4566 TextSelectionScopeId::default(),
4567 0,
4568 cx,
4569 );
4570
4571 selection_state.begin(point(px(200.), px(1.)), false, cx);
4572 selection_state.update(point(px(200.), px(8.)), cx);
4573 selection_state.end(cx);
4574
4575 assert!(!selection_state.has_selection(cx));
4576 assert_eq!(selection_state.selected_text(cx), "");
4577 assert!(participant.selection.snapshot(cx).is_none());
4578 });
4579 }
4580
4581 #[gpui::test]
4582 fn unmounted_selection_does_not_revive_when_the_participant_returns(cx: &mut TestAppContext) {
4583 cx.update(|cx| {
4584 let mut selection_state = WindowSelectionState::default();
4585 let participant = FakeParticipant::new("stale", cx);
4586 participant.register(
4587 &mut selection_state,
4588 0.,
4589 TextSelectionScopeId::default(),
4590 0,
4591 cx,
4592 );
4593 selection_state.begin(point(px(1.), px(1.)), false, cx);
4594 selection_state.update(point(px(8.), px(1.)), cx);
4595 selection_state.end(cx);
4596 assert_eq!(selection_state.selected_text(cx), "stale");
4597
4598 selection_state.finish_frame(cx);
4599 selection_state.finish_frame(cx);
4600 assert_eq!(selection_state.selected_text(cx), "");
4601 assert!(participant.selection.snapshot(cx).is_none());
4602 participant.register(
4603 &mut selection_state,
4604 0.,
4605 TextSelectionScopeId::default(),
4606 0,
4607 cx,
4608 );
4609 assert_eq!(selection_state.selected_text(cx), "");
4610 assert!(participant.selection.snapshot(cx).is_none());
4611
4612 selection_state.begin(point(px(1.), px(1.)), false, cx);
4613 selection_state.update(point(px(8.), px(1.)), cx);
4614 selection_state.end(cx);
4615 assert_eq!(selection_state.selected_text(cx), "stale");
4616 });
4617 }
4618
4619 #[gpui::test]
4620 fn removing_an_unselected_participant_preserves_the_current_selection(cx: &mut TestAppContext) {
4621 cx.update(|cx| {
4622 let mut selection_state = WindowSelectionState::default();
4623 let selected = FakeParticipant::new("selected", cx);
4624 let unrelated = FakeParticipant::new("unrelated", cx);
4625 let scope = TextSelectionScopeId::default();
4626 selected.register(&mut selection_state, 0., scope, 0, cx);
4627 unrelated.register(&mut selection_state, 20., scope, 1, cx);
4628 selection_state.begin(point(px(1.), px(1.)), false, cx);
4629 selection_state.update(point(px(8.), px(1.)), cx);
4630 selection_state.end(cx);
4631 assert_eq!(selection_state.selected_text(cx), "selected");
4632
4633 selection_state.finish_frame(cx);
4634 selected.register(&mut selection_state, 0., scope, 0, cx);
4635 selection_state.finish_frame(cx);
4636 assert_eq!(selection_state.selected_text(cx), "selected");
4637 unrelated.register(&mut selection_state, 20., scope, 1, cx);
4638 assert_eq!(selection_state.selected_text(cx), "selected");
4639 });
4640 }
4641
4642 #[gpui::test]
4643 fn removing_either_range_endpoint_discards_the_shared_selection(cx: &mut TestAppContext) {
4644 for remove_anchor in [true, false] {
4645 cx.update(|cx| {
4646 let mut selection_state = WindowSelectionState::default();
4647 let anchor = FakeParticipant::new("anchor", cx);
4648 let cursor = FakeParticipant::new("cursor", cx);
4649 let scope = TextSelectionScopeId::default();
4650 anchor.register(&mut selection_state, 0., scope, 0, cx);
4651 cursor.register(&mut selection_state, 20., scope, 1, cx);
4652 selection_state.begin(point(px(1.), px(1.)), false, cx);
4653 selection_state.update(point(px(8.), px(21.)), cx);
4654 selection_state.end(cx);
4655 assert_eq!(selection_state.selected_text(cx), "anchor\ncursor");
4656 selection_state.finish_frame(cx);
4657
4658 if remove_anchor {
4659 cursor.register(&mut selection_state, 20., scope, 1, cx);
4660 } else {
4661 anchor.register(&mut selection_state, 0., scope, 0, cx);
4662 }
4663 selection_state.finish_frame(cx);
4664 assert_eq!(selection_state.selected_text(cx), "");
4665 anchor.register(&mut selection_state, 0., scope, 0, cx);
4666 cursor.register(&mut selection_state, 20., scope, 1, cx);
4667 assert_eq!(selection_state.selected_text(cx), "");
4668 });
4669 }
4670 }
4671
4672 #[gpui::test]
4673 fn clear_or_unmount_stops_auto_scroll_before_discarding_the_anchor(cx: &mut TestAppContext) {
4674 for unmount in [false, true] {
4675 let commands = Rc::new(RefCell::new(Vec::new()));
4676 let observed = commands.clone();
4677 let (mut selection_state, participant) = cx.update(|cx| {
4678 let selection_state = WindowSelectionState::default();
4679 let participant = FakeParticipant::new("scroll", cx);
4680 participant
4681 .selection
4682 .subscribe(
4683 move |event, _| {
4684 if let TextSelectionEvent::AutoScroll(delta) = event {
4685 observed.borrow_mut().push(*delta);
4686 }
4687 },
4688 cx,
4689 )
4690 .detach();
4691 (selection_state, participant)
4692 });
4693 cx.run_until_parked();
4694 cx.update(|cx| {
4695 participant.register(
4696 &mut selection_state,
4697 0.,
4698 TextSelectionScopeId::default(),
4699 0,
4700 cx,
4701 );
4702
4703 selection_state.begin(point(px(1.), px(1.)), false, cx);
4704 selection_state.update(point(px(1.), px(25.)), cx);
4705 if unmount {
4706 selection_state.finish_frame(cx);
4707 selection_state.finish_frame(cx);
4708 } else {
4709 selection_state.clear(cx);
4710 }
4711 });
4712 cx.run_until_parked();
4713 assert!(commands.borrow().iter().any(Option::is_some));
4714 assert_eq!(commands.borrow().last(), Some(&None));
4715 }
4716 }
4717
4718 #[gpui::test]
4719 fn drag_auto_scroll_stops_when_the_content_mask_collapses(cx: &mut TestAppContext) {
4720 let window = cx.add_window(|_, cx| WindowSelectionView {
4721 selection: TextSelectionHandle::new("unused", cx),
4722 });
4723 window
4724 .update(cx, |_, window, cx| {
4725 let state = cx.new(|_| WindowSelectionState::default());
4726 let participant = FakeParticipant::new("participant", cx);
4727 state.update(cx, |state, cx| {
4728 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
4729 state.begin(point(px(1.), px(1.)), false, cx);
4730 });
4731 let collapsed = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(0.)));
4734 state.update(cx, |state, cx| {
4735 state.register_participant(
4736 participant.selection.clone(),
4737 TextSelectionRegistration::new(
4738 Hitbox {
4739 id: HitboxId::placeholder(),
4740 bounds: collapsed,
4741 content_mask: ContentMask { bounds: collapsed },
4742 behavior: HitboxBehavior::Normal,
4743 },
4744 collapsed,
4745 )
4746 .with_text_bounds(vec![collapsed]),
4747 cx,
4748 );
4749 state.update_in_window(point(px(1.), px(50.)), window, cx);
4750 assert!(!state.auto_scroll.is_active());
4751 assert!(state.auto_scroll.last_drag_position.is_none());
4752 });
4753 })
4754 .unwrap();
4755 }
4756
4757 #[gpui::test]
4758 fn pointer_moves_after_a_click_do_not_auto_scroll(cx: &mut TestAppContext) {
4759 let window = cx.add_window(|_, cx| WindowSelectionView {
4760 selection: TextSelectionHandle::new("unused", cx),
4761 });
4762 window
4763 .update(cx, |_, window, cx| {
4764 let state = cx.new(|_| WindowSelectionState::default());
4765 let participant = FakeParticipant::new("participant", cx);
4766 state.update(cx, |state, cx| {
4767 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
4768 state.begin(point(px(1.), px(1.)), false, cx);
4770 state.end(cx);
4771 assert!(state.anchor.is_some());
4772
4773 state.update_in_window(point(px(1.), px(50.)), window, cx);
4774 assert!(!state.auto_scroll.is_active());
4775 });
4776 })
4777 .unwrap();
4778 }
4779
4780 #[gpui::test]
4781 fn proxy_endpoints_break_equal_position_ties_by_document_order(cx: &mut TestAppContext) {
4782 cx.update(|cx| {
4783 let mut selection_state = WindowSelectionState::default();
4784 let later = FakeParticipant::new("later", cx);
4785 let earlier = FakeParticipant::new("earlier", cx);
4786 later.register(
4787 &mut selection_state,
4788 0.,
4789 TextSelectionScopeId::default(),
4790 2,
4791 cx,
4792 );
4793 earlier.register(
4794 &mut selection_state,
4795 0.,
4796 TextSelectionScopeId::default(),
4797 1,
4798 cx,
4799 );
4800
4801 selection_state.begin(point(px(1.), px(1.)), false, cx);
4802 selection_state.update(point(px(200.), px(25.)), cx);
4803 let endpoint = selection_state.snapshot().unwrap().cursor();
4804
4805 assert_eq!(endpoint.entity_id(), Some(earlier.selection.entity_id()));
4806 });
4807 }
4808
4809 #[gpui::test]
4810 fn equal_area_hovered_participants_break_ties_by_document_order(cx: &mut TestAppContext) {
4811 cx.update(|cx| {
4812 for _ in 0..64 {
4813 let mut selection_state = WindowSelectionState::default();
4814 let later = FakeParticipant::new("later", cx);
4815 let earliest = FakeParticipant::new("earliest", cx);
4816 let middle = FakeParticipant::new("middle", cx);
4817 later.register(
4818 &mut selection_state,
4819 0.,
4820 TextSelectionScopeId::default(),
4821 30,
4822 cx,
4823 );
4824 earliest.register(
4825 &mut selection_state,
4826 0.,
4827 TextSelectionScopeId::default(),
4828 10,
4829 cx,
4830 );
4831 middle.register(
4832 &mut selection_state,
4833 0.,
4834 TextSelectionScopeId::default(),
4835 20,
4836 cx,
4837 );
4838
4839 selection_state.begin(point(px(1.), px(1.)), false, cx);
4840 selection_state.update(point(px(8.), px(1.)), cx);
4841
4842 assert_eq!(
4843 selection_state.snapshot().unwrap().anchor().entity_id(),
4844 Some(earliest.selection.entity_id())
4845 );
4846 }
4847 });
4848 }
4849
4850 #[gpui::test]
4851 fn text_selection_namespace_is_a_safe_no_op_until_the_element_is_rendered(
4852 cx: &mut TestAppContext,
4853 ) {
4854 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
4855 selection: TextSelectionHandle::new("not enabled", cx),
4856 });
4857 cx.update(|window, cx| {
4858 assert!(!TextSelection::has_selection(window, cx));
4859 assert_eq!(TextSelection::selected_text(window, cx), "");
4860 TextSelection::clear(window, cx);
4861 TextSelection::end(window, cx);
4862 assert!(!TextSelection::has_selection(window, cx));
4863 });
4864 }
4865
4866 #[gpui::test]
4867 fn unit_selection_element_supports_scope_and_registration_on_the_first_frame(
4868 cx: &mut TestAppContext,
4869 ) {
4870 let (view, cx) = cx.add_window_view(|_, cx| FirstFrameScopedSelectionView {
4871 selection: TextSelectionHandle::new("first frame", cx),
4872 });
4873 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
4874
4875 cx.update(|window, cx| {
4876 let _ = window.draw(cx);
4877 let state = WindowSelectionState::existing(window, cx).unwrap();
4878 assert_eq!(
4879 state.read(cx).active_scope,
4880 TextSelectionScopeId::from_raw(23)
4881 );
4882 assert!(
4883 state
4884 .read(cx)
4885 .participants
4886 .borrow()
4887 .contains_key(&selection.entity_id())
4888 );
4889 });
4890 }
4891
4892 #[gpui::test]
4893 fn lazy_registration_does_not_enable_queries_without_the_element(cx: &mut TestAppContext) {
4894 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
4895 selection: TextSelectionHandle::new("registered", cx),
4896 });
4897 cx.update(|window, cx| {
4898 let selection = TextSelectionHandle::new("registered", cx);
4899 selection.set_local_selection(true, cx);
4900 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(20.)));
4901 let hitbox = Hitbox {
4902 id: HitboxId::placeholder(),
4903 bounds,
4904 content_mask: ContentMask { bounds },
4905 behavior: HitboxBehavior::Normal,
4906 };
4907 selection.register(
4908 TextSelectionRegistration::new(hitbox, bounds).with_text_bounds(vec![bounds]),
4909 window,
4910 cx,
4911 );
4912 assert_eq!(TextSelection::selected_text(window, cx), "");
4913 assert!(!TextSelection::has_selection(window, cx));
4914 TextSelection::clear(window, cx);
4915 assert_eq!(TextSelection::selected_text(window, cx), "");
4916 });
4917 }
4918
4919 struct RetainedSelectionText {
4920 text: Entity<crate::text::TextViewState>,
4921 renders: Rc<Cell<usize>>,
4922 scrollable: bool,
4923 }
4924
4925 impl RetainedSelectionText {
4926 fn new(text: &str, cx: &mut App) -> Entity<Self> {
4927 let text = cx.new(|cx| crate::text::TextViewState::markdown(text, cx));
4928 cx.new(|_| Self {
4929 text,
4930 renders: Rc::new(Cell::new(0)),
4931 scrollable: false,
4932 })
4933 }
4934 }
4935
4936 impl Render for RetainedSelectionText {
4937 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4938 self.renders.set(self.renders.get() + 1);
4939 div()
4940 .w(px(240.))
4941 .h(px(32.))
4942 .child(crate::text::TextView::new(&self.text).scrollable(self.scrollable))
4943 }
4944 }
4945
4946 struct RetainedSelectionRoot {
4947 texts: Vec<Entity<RetainedSelectionText>>,
4948 }
4949
4950 impl Render for RetainedSelectionRoot {
4951 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4952 div()
4953 .size_full()
4954 .flex()
4955 .flex_col()
4956 .child(TextSelectionLayer)
4957 .children(self.texts.iter().cloned())
4958 }
4959 }
4960
4961 #[cfg(feature = "gpui-fast")]
4962 struct RetainedScrollingSelectionRoot {
4963 text: Entity<RetainedSelectionText>,
4964 unrelated: Entity<RetainedSelectionText>,
4965 scroll: gpui::ScrollHandle,
4966 }
4967
4968 #[cfg(feature = "gpui-fast")]
4969 impl Render for RetainedScrollingSelectionRoot {
4970 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4971 div()
4972 .size_full()
4973 .flex()
4974 .flex_col()
4975 .child(TextSelectionLayer)
4976 .child(
4977 div()
4978 .id("scrolling-texts")
4979 .w(px(240.))
4980 .h(px(64.))
4981 .flex_none()
4982 .overflow_y_scroll()
4983 .track_scroll(&self.scroll)
4984 .child(div().h(px(352.)).child(self.text.clone())),
4985 )
4986 .child(self.unrelated.clone())
4987 }
4988 }
4989
4990 fn draw_selection_frame(cx: &mut gpui::VisualTestContext) {
4991 cx.update(|window, cx| window.draw(cx).clear(cx));
4992 cx.run_until_parked();
4993 }
4994
4995 fn select_retained_texts(
4998 last: usize,
4999 release: bool,
5000 cx: &mut gpui::VisualTestContext,
5001 ) -> String {
5002 cx.update(|window, cx| {
5003 let state = WindowSelectionState::existing(window, cx).unwrap();
5004 let mut bounds = state
5005 .read(cx)
5006 .participants
5007 .borrow()
5008 .values()
5009 .map(|participant| participant.registration.text_bounds[0])
5010 .collect::<Vec<_>>();
5011 bounds.sort_by(|a, b| a.top().partial_cmp(&b.top()).unwrap());
5012 let start = point(bounds[0].left() + px(0.1), bounds[0].center().y);
5013 let end = point(bounds[last].right() - px(0.1), bounds[last].center().y);
5014 window.dispatch_event(
5015 gpui::PlatformInput::MouseDown(MouseDownEvent {
5016 position: start,
5017 button: MouseButton::Left,
5018 click_count: 1,
5019 ..Default::default()
5020 }),
5021 cx,
5022 );
5023 window.dispatch_event(
5024 gpui::PlatformInput::MouseMove(MouseMoveEvent {
5025 position: end,
5026 pressed_button: Some(MouseButton::Left),
5027 ..Default::default()
5028 }),
5029 cx,
5030 );
5031 if release {
5032 window.dispatch_event(
5033 gpui::PlatformInput::MouseUp(MouseUpEvent {
5034 position: end,
5035 button: MouseButton::Left,
5036 modifiers: gpui::Modifiers::default(),
5037 click_count: 1,
5038 }),
5039 cx,
5040 );
5041 }
5042 assert!(TextSelection::has_selection(window, cx));
5043 TextSelection::selected_text(window, cx)
5044 })
5045 }
5046
5047 #[cfg(feature = "gpui-fast")]
5050 #[gpui::test]
5051 fn active_selection_keeps_unrelated_text_views_retained(cx: &mut TestAppContext) {
5052 cx.update(crate::init);
5053 let (root, cx) = cx.add_window_view(|_, cx| RetainedSelectionRoot {
5054 texts: ["alpha", "bravo", "charlie"]
5055 .into_iter()
5056 .map(|text| RetainedSelectionText::new(text, cx))
5057 .collect(),
5058 });
5059 draw_selection_frame(cx);
5060 draw_selection_frame(cx);
5061 assert_eq!(select_retained_texts(0, false, cx).trim(), "alpha");
5062 draw_selection_frame(cx);
5063 draw_selection_frame(cx);
5064 let texts = root.read_with(cx, |root, _| root.texts.clone());
5065 let unrelated = texts[1].read_with(cx, |text, _| text.renders.clone());
5066 let before = unrelated.get();
5067 let first_line = cx.update(|window, cx| {
5068 let state = WindowSelectionState::existing(window, cx).unwrap();
5069 state
5070 .read(cx)
5071 .participants
5072 .borrow()
5073 .values()
5074 .map(|participant| participant.registration.text_bounds[0])
5075 .min_by(|a, b| a.top().partial_cmp(&b.top()).unwrap())
5076 .unwrap()
5077 });
5078 for fraction in [0.2, 0.4, 0.6, 0.8] {
5079 cx.update(|window, cx| {
5080 window.dispatch_event(
5081 gpui::PlatformInput::MouseMove(MouseMoveEvent {
5082 position: point(
5083 first_line.left() + first_line.size.width * fraction,
5084 first_line.center().y,
5085 ),
5086 pressed_button: Some(MouseButton::Left),
5087 ..Default::default()
5088 }),
5089 cx,
5090 );
5091 });
5092 draw_selection_frame(cx);
5093 }
5094 assert_ne!(
5095 cx.update(|window, cx| TextSelection::selected_text(window, cx))
5096 .trim(),
5097 "alpha"
5098 );
5099 assert_eq!(
5100 unrelated.get(),
5101 before,
5102 "active registration redrew an unrelated view"
5103 );
5104 }
5105
5106 #[gpui::test]
5107 fn active_selection_frame_publication_visits_participants_linearly(cx: &mut TestAppContext) {
5108 for count in [4, 16] {
5109 cx.update(crate::init);
5110 let (_, cx) = cx.add_window_view(|_, cx| RetainedSelectionRoot {
5111 texts: (0..count)
5112 .map(|_| RetainedSelectionText::new("text", cx))
5113 .collect(),
5114 });
5115 draw_selection_frame(cx);
5116 draw_selection_frame(cx);
5117 assert_eq!(select_retained_texts(0, false, cx).trim(), "text");
5118 draw_selection_frame(cx);
5119 draw_selection_frame(cx);
5120 cx.update(|window, cx| {
5121 let state = WindowSelectionState::existing(window, cx).unwrap();
5122 state.read(cx).snapshot_visits.set(0);
5123 window.refresh();
5124 window.draw(cx).clear(cx);
5125 });
5126 cx.run_until_parked();
5127 cx.update(|window, cx| {
5128 let state = WindowSelectionState::existing(window, cx).unwrap();
5129 let visits = state.read(cx).snapshot_visits.get();
5130 assert!(
5131 visits >= count,
5132 "the frame must actually visit the participants"
5133 );
5134 assert!(
5135 visits <= 3 * count,
5136 "{count} participants required {visits} snapshot visits"
5137 );
5138 });
5139 }
5140 }
5141
5142 #[cfg(feature = "gpui-fast")]
5143 #[gpui::test]
5144 fn scrolling_selected_text_does_not_rebuild_unrelated_document_order(cx: &mut TestAppContext) {
5145 cx.update(crate::init);
5146 let (root, cx) = cx.add_window_view(|_, cx| {
5147 let scrolling = RetainedSelectionText::new("alpha\n\nbravo\n\ncharlie\n\ndelta", cx);
5148 scrolling.update(cx, |text, _| text.scrollable = true);
5149 RetainedSelectionRoot {
5150 texts: vec![scrolling, RetainedSelectionText::new("unrelated", cx)],
5151 }
5152 });
5153 draw_selection_frame(cx);
5154 draw_selection_frame(cx);
5155 assert_eq!(select_retained_texts(0, false, cx).trim(), "alpha");
5156 draw_selection_frame(cx);
5157 draw_selection_frame(cx);
5158 let unrelated = root.read_with(cx, |root, cx| root.texts[1].read(cx).renders.clone());
5159 let before = unrelated.get();
5160 let previous_offset = cx.update(|window, cx| {
5161 let state = WindowSelectionState::existing(window, cx).unwrap();
5162 let registration = state
5163 .read(cx)
5164 .participants
5165 .borrow()
5166 .values()
5167 .find(|participant| participant.registration.self_scroll)
5168 .unwrap()
5169 .registration
5170 .clone();
5171 let offset = registration.scroll_offset;
5172 window.dispatch_event(
5173 gpui::PlatformInput::Scroll(ScrollEvent {
5174 position: registration.bounds.center(),
5175 delta: ScrollDelta::Pixels(point(px(0.), px(-20.))),
5176 touch_phase: TouchPhase::Moved,
5177 ..Default::default()
5178 }),
5179 cx,
5180 );
5181 offset
5182 });
5183 for _ in 0..3 {
5184 draw_selection_frame(cx);
5185 }
5186 cx.update(|window, cx| {
5187 let state = WindowSelectionState::existing(window, cx).unwrap();
5188 let participants = state.read(cx).participants.borrow();
5189 let scrolling = participants
5190 .values()
5191 .find(|participant| participant.registration.self_scroll)
5192 .unwrap();
5193 assert_ne!(
5194 scrolling.registration.scroll_offset, previous_offset,
5195 "the content must actually scroll"
5196 );
5197 assert!(!state.read(cx).refresh_held_cursor.get());
5198 });
5199 assert_eq!(
5200 unrelated.get(),
5201 before,
5202 "a content scroll must not request a full order rebuild"
5203 );
5204 }
5205
5206 #[cfg(feature = "gpui-fast")]
5207 #[gpui::test]
5208 fn ancestor_scrolling_keeps_unrelated_text_views_retained(cx: &mut TestAppContext) {
5209 for selected in [false, true] {
5210 cx.update(crate::init);
5211 let (root, cx) = cx.add_window_view(|_, cx| RetainedScrollingSelectionRoot {
5212 text: RetainedSelectionText::new("alpha", cx),
5213 unrelated: RetainedSelectionText::new("unrelated", cx),
5214 scroll: gpui::ScrollHandle::new(),
5215 });
5216 draw_selection_frame(cx);
5217 draw_selection_frame(cx);
5218 if selected {
5219 assert_eq!(select_retained_texts(0, true, cx).trim(), "alpha");
5220 draw_selection_frame(cx);
5221 draw_selection_frame(cx);
5222 }
5223 let (scroll, renders, text) = root.read_with(cx, |root, cx| {
5224 (
5225 root.scroll.clone(),
5226 root.unrelated.read(cx).renders.clone(),
5227 root.text.read(cx).text.clone(),
5228 )
5229 });
5230 let before = renders.get();
5231 let bounds_before = text.read_with(cx, |text, _| text.bounds());
5232 for _ in 0..4 {
5233 let previous_offset = scroll.offset();
5234 cx.update(|window, cx| {
5235 window.dispatch_event(
5236 gpui::PlatformInput::Scroll(ScrollEvent {
5237 position: point(px(20.), px(20.)),
5238 delta: ScrollDelta::Pixels(point(px(0.), px(-4.))),
5239 touch_phase: TouchPhase::Moved,
5240 ..Default::default()
5241 }),
5242 cx,
5243 );
5244 });
5245 for _ in 0..3 {
5246 draw_selection_frame(cx);
5247 }
5248 assert_ne!(scroll.offset(), previous_offset, "the ancestor must scroll");
5249 assert_eq!(
5250 renders.get(),
5251 before,
5252 "ancestor scrolling rebuilt an unrelated text view (selected: {selected})"
5253 );
5254 let copied = cx.update(|window, cx| TextSelection::selected_text(window, cx));
5255 assert_eq!(copied.trim(), if selected { "alpha" } else { "" });
5256 }
5257 assert_ne!(text.read_with(cx, |text, _| text.bounds()), bounds_before);
5258 }
5259 }
5260
5261 #[gpui::test]
5262 fn retained_text_order_survives_partial_frames_insertion_and_reordering(
5263 cx: &mut TestAppContext,
5264 ) {
5265 cx.update(crate::init);
5266 let (root, cx) = cx.add_window_view(|_, cx| RetainedSelectionRoot {
5267 texts: ["alpha", "bravo", "charlie"]
5268 .into_iter()
5269 .map(|text| RetainedSelectionText::new(text, cx))
5270 .collect(),
5271 });
5272 draw_selection_frame(cx);
5273 draw_selection_frame(cx);
5274 let words = |text: String| {
5275 text.split_whitespace()
5276 .map(str::to_owned)
5277 .collect::<Vec<_>>()
5278 };
5279 assert_eq!(
5280 words(select_retained_texts(2, true, cx)),
5281 ["alpha", "bravo", "charlie"]
5282 );
5283 let texts = root.read_with(cx, |root, _| root.texts.clone());
5284 for _ in 0..3 {
5285 texts[1].update(cx, |_, cx| cx.notify());
5286 draw_selection_frame(cx);
5287 assert_eq!(
5288 words(cx.update(|window, cx| TextSelection::selected_text(window, cx))),
5289 ["alpha", "bravo", "charlie"]
5290 );
5291 }
5292 root.update(cx, |root, cx| {
5293 root.texts
5294 .insert(1, RetainedSelectionText::new("delta", cx));
5295 cx.notify();
5296 });
5297 for _ in 0..3 {
5298 draw_selection_frame(cx);
5299 }
5300 assert_eq!(
5301 words(cx.update(|window, cx| TextSelection::selected_text(window, cx))),
5302 ["alpha", "delta", "bravo", "charlie"]
5303 );
5304 root.update(cx, |root, cx| {
5305 root.texts.swap(1, 2);
5306 cx.notify();
5307 });
5308 for _ in 0..3 {
5309 draw_selection_frame(cx);
5310 }
5311 assert_eq!(
5312 words(cx.update(|window, cx| TextSelection::selected_text(window, cx))),
5313 ["alpha", "bravo", "delta", "charlie"]
5314 );
5315 #[cfg(feature = "gpui-fast")]
5316 {
5317 let renders = texts[0].read_with(cx, |text, _| text.renders.clone());
5318 let before = renders.get();
5319 for _ in 0..3 {
5320 draw_selection_frame(cx);
5321 }
5322 assert_eq!(renders.get(), before, "order recovery must settle");
5323 }
5324 root.update(cx, |root, cx| {
5327 root.texts.pop();
5328 cx.notify();
5329 });
5330 draw_selection_frame(cx);
5331 draw_selection_frame(cx);
5332 assert!(
5333 cx.update(|window, cx| TextSelection::selected_text(window, cx))
5334 .is_empty()
5335 );
5336 root.update(cx, |root, cx| {
5337 root.texts.push(texts[2].clone());
5338 cx.notify();
5339 });
5340 draw_selection_frame(cx);
5341 assert!(
5342 cx.update(|window, cx| TextSelection::selected_text(window, cx))
5343 .is_empty()
5344 );
5345 }
5346
5347 #[cfg(feature = "gpui-fast")]
5350 struct SelectionStateReader {
5351 renders: Rc<Cell<usize>>,
5352 }
5353
5354 #[cfg(feature = "gpui-fast")]
5355 impl Render for SelectionStateReader {
5356 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5357 self.renders.set(self.renders.get() + 1);
5358 if let Some(state) = WindowSelectionState::existing(window, cx) {
5359 let _ = state.read(cx).participants.borrow().len();
5360 }
5361 div().size_full()
5362 }
5363 }
5364
5365 #[cfg(feature = "gpui-fast")]
5368 struct ReaderBesideSelectionView {
5369 text: gpui::Entity<ToggleSelectionElementView>,
5370 reader: gpui::Entity<SelectionStateReader>,
5371 }
5372
5373 #[cfg(feature = "gpui-fast")]
5374 impl Render for ReaderBesideSelectionView {
5375 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5376 div()
5377 .size_full()
5378 .child(self.reader.clone())
5379 .child(self.text.clone())
5380 }
5381 }
5382
5383 #[cfg(feature = "gpui-fast")]
5387 #[gpui::test]
5388 fn redrawing_selectable_text_leaves_its_readers_retained(cx: &mut TestAppContext) {
5389 let renders = Rc::new(Cell::new(0));
5390 let (view, cx) = cx.add_window_view(|_, cx| ReaderBesideSelectionView {
5391 text: cx.new(|cx| ToggleSelectionElementView {
5392 enabled: true,
5393 selection: TextSelectionHandle::new("text", cx),
5394 }),
5395 reader: cx.new(|_| SelectionStateReader {
5396 renders: renders.clone(),
5397 }),
5398 });
5399 let text = cx.update(|_, cx| view.read(cx).text.clone());
5400 let redraw = |cx: &mut gpui::VisualTestContext| {
5401 text.update(cx, |_, cx| cx.notify());
5402 cx.update(|window, cx| {
5403 let _ = window.draw(cx);
5404 window.simulate_next_frame(cx);
5405 });
5406 cx.run_until_parked();
5407 };
5408 redraw(cx);
5409 redraw(cx);
5410 let before = renders.get();
5411 for _ in 0..4 {
5412 redraw(cx);
5413 }
5414 assert_eq!(renders.get(), before, "the reader rendered again");
5415 }
5416
5417 #[gpui::test]
5418 fn retained_selection_state_releases_and_does_not_resurrect_selection(cx: &mut TestAppContext) {
5419 let (view, cx) = cx.add_window_view(|_, cx| ToggleSelectionElementView {
5420 enabled: true,
5421 selection: TextSelectionHandle::new("local", cx),
5422 });
5423 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
5424 cx.update(|window, cx| {
5425 let _ = window.draw(cx);
5426 selection.set_local_selection(true, cx);
5427 assert!(TextSelection::has_selection(window, cx));
5428
5429 window.simulate_next_frame(cx);
5430 assert!(TextSelection::has_selection(window, cx));
5431 let _ = window.draw(cx);
5432 assert!(TextSelection::has_selection(window, cx));
5433 });
5434 view.update(cx, |view, cx| {
5435 view.enabled = false;
5436 cx.notify();
5437 });
5438 cx.update(|window, cx| {
5439 let _ = window.draw(cx);
5440 });
5441 cx.update(|window, cx| {
5442 window.simulate_next_frame(cx);
5443 });
5444 cx.update(|window, cx| {
5445 window.simulate_next_frame(cx);
5446 });
5447 cx.run_until_parked();
5448 cx.update(|window, cx| {
5449 assert!(!TextSelection::has_selection(window, cx));
5450 assert_eq!(TextSelection::selected_text(window, cx), "");
5451 assert!(!selection.has_local_selection(cx));
5452 });
5453
5454 view.update(cx, |view, cx| {
5455 view.enabled = true;
5456 cx.notify();
5457 });
5458 cx.update(|window, cx| {
5459 let _ = window.draw(cx);
5460 assert!(!TextSelection::has_selection(window, cx));
5461 assert_eq!(TextSelection::selected_text(window, cx), "");
5462 });
5463 }
5464
5465 #[gpui::test]
5466 fn mounted_selection_element_does_not_keep_an_idle_frame_queue_alive(cx: &mut TestAppContext) {
5467 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
5468 cx.update(|window, cx| {
5469 let _ = window.draw(cx);
5470 assert_eq!(window.simulate_next_frame(cx), 0);
5471 assert_eq!(window.simulate_next_frame(cx), 0);
5472 assert!(live_text_selection_state(window, cx).is_some());
5473 });
5474 }
5475
5476 #[gpui::test]
5477 fn selection_element_initializes_suppression_and_respects_bubble_suppression(
5478 cx: &mut TestAppContext,
5479 ) {
5480 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
5481 cx.update(|window, cx| {
5482 let _ = window.draw(cx);
5483 });
5484 cx.simulate_mouse_down(
5485 point(px(1.), px(1.)),
5486 MouseButton::Left,
5487 gpui::Modifiers::default(),
5488 );
5489 cx.simulate_mouse_up(
5490 point(px(1.), px(1.)),
5491 MouseButton::Left,
5492 gpui::Modifiers::default(),
5493 );
5494 cx.update(|window, cx| {
5495 assert!(GlobalState::is_text_selection_suppressed(cx));
5496 assert!(!TextSelection::has_selection(window, cx));
5497 });
5498 }
5499
5500 #[gpui::test]
5501 fn frame_sweep_keeps_a_participant_registered_before_the_selection_element_paints(
5502 cx: &mut TestAppContext,
5503 ) {
5504 cx.update(|cx| {
5505 let mut selection_state = WindowSelectionState::default();
5506 let participant = FakeParticipant::new("painted first", cx);
5507 participant.register(
5508 &mut selection_state,
5509 0.,
5510 TextSelectionScopeId::default(),
5511 0,
5512 cx,
5513 );
5514 selection_state.begin(point(px(1.), px(1.)), false, cx);
5515 selection_state.update(point(px(8.), px(1.)), cx);
5516 selection_state.end(cx);
5517
5518 selection_state.finish_frame(cx);
5519
5520 assert_eq!(selection_state.selected_text(cx), "painted first");
5521 assert!(participant.selection.snapshot(cx).is_some());
5522 });
5523 }
5524
5525 #[gpui::test]
5526 fn two_selection_elements_schedule_only_one_post_frame_sweep(cx: &mut TestAppContext) {
5527 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
5528 selection: TextSelectionHandle::new("once", cx),
5529 });
5530 cx.update(|window, cx| {
5531 let selection_state = WindowSelectionState::ensure(window, cx);
5532 let selection = view.read(cx).selection.clone();
5533 selection_state.update(cx, |selection_state, cx| {
5534 FakeParticipant { selection }.register(
5535 selection_state,
5536 0.,
5537 TextSelectionScopeId::default(),
5538 0,
5539 cx,
5540 );
5541 selection_state.begin(point(px(1.), px(1.)), false, cx);
5542 selection_state.update(point(px(8.), px(1.)), cx);
5543 selection_state.end(cx);
5544 });
5545
5546 let _ = window.draw(cx);
5547 window.simulate_next_frame(cx);
5548
5549 let items = selection_state.read(cx).copy_items(cx);
5550 assert_eq!(resolve_copy_items(items, cx), "once");
5551 });
5552 }
5553
5554 #[gpui::test]
5555 fn duplicate_selection_elements_gate_real_pointer_gestures_and_reentrant_clear(
5556 cx: &mut TestAppContext,
5557 ) {
5558 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
5559 selection: TextSelectionHandle::new("once", cx),
5560 });
5561 let clear_count = Rc::new(Cell::new(0));
5562 cx.update(|window, cx| {
5563 let state = WindowSelectionState::ensure(window, cx);
5564 let state_for_clear = state.clone();
5565 let count = clear_count.clone();
5566 let selection = view.read(cx).selection.clone();
5567 selection
5568 .subscribe(
5569 move |event, cx| {
5570 if matches!(event, TextSelectionEvent::Cleared) {
5571 count.set(count.get() + 1);
5572 let _ = state_for_clear.read(cx).snapshot();
5573 }
5574 },
5575 cx,
5576 )
5577 .detach();
5578 let _ = window.draw(cx);
5579 });
5580
5581 cx.simulate_mouse_down(
5582 point(px(10.), px(10.)),
5583 MouseButton::Left,
5584 gpui::Modifiers::default(),
5585 );
5586 cx.simulate_mouse_up(
5587 point(px(10.), px(10.)),
5588 MouseButton::Left,
5589 gpui::Modifiers::default(),
5590 );
5591 cx.simulate_mouse_down(
5592 point(px(70.), px(10.)),
5593 MouseButton::Left,
5594 gpui::Modifiers {
5595 shift: true,
5596 ..Default::default()
5597 },
5598 );
5599 cx.simulate_mouse_up(
5600 point(px(70.), px(10.)),
5601 MouseButton::Left,
5602 gpui::Modifiers::default(),
5603 );
5604 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
5605
5606 cx.simulate_mouse_down(
5607 point(px(15.), px(10.)),
5608 MouseButton::Left,
5609 gpui::Modifiers::default(),
5610 );
5611 cx.simulate_mouse_move(
5612 point(px(85.), px(10.)),
5613 Some(MouseButton::Left),
5614 gpui::Modifiers::default(),
5615 );
5616 cx.simulate_mouse_up(
5617 point(px(85.), px(10.)),
5618 MouseButton::Left,
5619 gpui::Modifiers::default(),
5620 );
5621 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
5622 assert_eq!(clear_count.get(), 3);
5623 }
5624
5625 #[gpui::test]
5626 fn selection_layer_handles_real_double_and_triple_click_events(cx: &mut TestAppContext) {
5627 let (text, layout) = laid_out_runs(&["alpha beta"], cx).pop().unwrap();
5628 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
5629 selection: TextSelectionHandle::new("", cx),
5630 });
5631 cx.update(|window, cx| {
5632 let _ = window.draw(cx);
5633 let selection = view.read(cx).selection.clone();
5634 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(17)), cx);
5635 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
5636 });
5637
5638 let position = layout.position_for_index(7).unwrap();
5639 cx.simulate_event(MouseDownEvent {
5640 position,
5641 modifiers: gpui::Modifiers::default(),
5642 button: MouseButton::Left,
5643 click_count: 2,
5644 first_mouse: false,
5645 });
5646 cx.simulate_event(MouseUpEvent {
5647 position,
5648 modifiers: gpui::Modifiers::default(),
5649 button: MouseButton::Left,
5650 click_count: 2,
5651 });
5652 cx.update(|window, cx| {
5653 let selection = view.read(cx).selection.clone();
5654 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
5655 assert_eq!(TextSelection::selected_text(window, cx), "beta");
5656 let snapshot = selection.snapshot(cx).unwrap();
5657 assert_eq!(
5658 snapshot.anchor().content_key(),
5659 Some(TextSelectionContentKey::new(17))
5660 );
5661 assert_eq!(
5662 snapshot.cursor().content_key(),
5663 Some(TextSelectionContentKey::new(17))
5664 );
5665 });
5666
5667 cx.simulate_event(MouseDownEvent {
5668 position,
5669 modifiers: gpui::Modifiers::default(),
5670 button: MouseButton::Left,
5671 click_count: 3,
5672 first_mouse: false,
5673 });
5674 cx.simulate_event(MouseUpEvent {
5675 position,
5676 modifiers: gpui::Modifiers::default(),
5677 button: MouseButton::Left,
5678 click_count: 3,
5679 });
5680 cx.update(|window, cx| {
5681 let selection = view.read(cx).selection.clone();
5682 selection.update_runs(&[text_run(0, text, layout)], cx);
5683 assert_eq!(TextSelection::selected_text(window, cx), "alpha beta");
5684 });
5685 }
5686
5687 struct SelectableTextsView {
5689 texts: Vec<&'static str>,
5690 }
5691
5692 impl Render for SelectableTextsView {
5693 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5694 div()
5695 .size_full()
5696 .child(TextSelectionLayer)
5697 .child(
5698 div()
5699 .w(px(400.))
5700 .children(self.texts.iter().enumerate().map(|(ix, text)| {
5701 crate::SelectableText::new(("text", ix), *text)
5702 .document_order(ix as u64)
5703 })),
5704 )
5705 }
5706 }
5707
5708 fn painted_position(text: &str, index: usize, window: &Window, cx: &App) -> Point<Pixels> {
5710 let state = WindowSelectionState::existing(window, cx).unwrap();
5711 let participants = state.read(cx).participants.borrow();
5712 participants
5713 .values()
5714 .filter_map(|registration| registration.participant.upgrade())
5715 .find_map(|participant| {
5716 let runs = participant.read(cx).runs.borrow();
5717 let run = runs.iter().find(|run| run.text.as_ref() == text)?;
5718 let position = run.layout.position_for_index(index)?;
5719 Some(point(
5720 position.x + px(1.),
5721 position.y + run.layout.line_height() / 2.,
5722 ))
5723 })
5724 .unwrap()
5725 }
5726
5727 fn press_at(
5730 text: &str,
5731 index: usize,
5732 click_count: usize,
5733 shift: bool,
5734 cx: &mut gpui::VisualTestContext,
5735 ) -> Point<Pixels> {
5736 let position = cx.update(|window, cx| painted_position(text, index, window, cx));
5737 cx.simulate_event(MouseDownEvent {
5738 position,
5739 modifiers: gpui::Modifiers {
5740 shift,
5741 ..Default::default()
5742 },
5743 button: MouseButton::Left,
5744 click_count,
5745 first_mouse: false,
5746 });
5747 position
5748 }
5749
5750 fn drag_to(text: &str, index: usize, cx: &mut gpui::VisualTestContext) -> String {
5753 let position = cx.update(|window, cx| painted_position(text, index, window, cx));
5754 cx.simulate_event(MouseMoveEvent {
5755 position,
5756 pressed_button: Some(MouseButton::Left),
5757 modifiers: gpui::Modifiers::default(),
5758 });
5759 painted_selection(cx)
5760 }
5761
5762 fn release(click_count: usize, cx: &mut gpui::VisualTestContext) {
5763 let position = cx.update(|window, _| window.mouse_position());
5764 cx.simulate_event(MouseUpEvent {
5765 position,
5766 modifiers: gpui::Modifiers::default(),
5767 button: MouseButton::Left,
5768 click_count,
5769 });
5770 }
5771
5772 fn painted_selection(cx: &mut gpui::VisualTestContext) -> String {
5773 cx.update(|window, cx| {
5774 let _ = window.draw(cx);
5775 TextSelection::selected_text(window, cx)
5776 })
5777 }
5778
5779 #[gpui::test]
5780 fn double_click_drag_extends_the_selection_by_words(cx: &mut TestAppContext) {
5781 let (_, cx) = cx.add_window_view(|_, _| SelectableTextsView {
5782 texts: vec!["one two three", "four five six"],
5783 });
5784 painted_selection(cx);
5785
5786 press_at("one two three", 5, 2, false, cx);
5787 assert_eq!(painted_selection(cx), "two");
5788 assert_eq!(drag_to("one two three", 9, cx), "two three");
5789 assert_eq!(drag_to("one two three", 1, cx), "one two");
5791 assert_eq!(drag_to("four five six", 6, cx), "two three\nfour five");
5792 release(2, cx);
5793 assert_eq!(painted_selection(cx), "two three\nfour five");
5794 }
5795
5796 #[gpui::test]
5797 fn triple_click_drag_extends_the_selection_by_lines(cx: &mut TestAppContext) {
5798 let text = "first line\nsecond line\nthird line";
5799 let (_, cx) = cx.add_window_view(|_, _| SelectableTextsView { texts: vec![text] });
5800 painted_selection(cx);
5801
5802 press_at(text, 13, 3, false, cx);
5803 assert_eq!(painted_selection(cx), "second line");
5804 assert_eq!(drag_to(text, 25, cx), "second line\nthird line");
5805 assert_eq!(drag_to(text, 2, cx), "first line\nsecond line");
5806 release(3, cx);
5807 assert_eq!(painted_selection(cx), "first line\nsecond line");
5808 }
5809
5810 #[gpui::test]
5811 fn shift_click_after_a_double_click_extends_by_words(cx: &mut TestAppContext) {
5812 let text = "one two three";
5813 let (_, cx) = cx.add_window_view(|_, _| SelectableTextsView { texts: vec![text] });
5814 painted_selection(cx);
5815
5816 press_at(text, 5, 2, false, cx);
5817 release(2, cx);
5818 press_at(text, 9, 1, true, cx);
5819 release(1, cx);
5820 assert_eq!(painted_selection(cx), "two three");
5821 press_at(text, 1, 1, true, cx);
5822 release(1, cx);
5823 assert_eq!(painted_selection(cx), "one two");
5824
5825 press_at(text, 5, 1, false, cx);
5827 assert_eq!(drag_to(text, 9, cx), "wo t");
5828 release(1, cx);
5829 }
5830}