Skip to main content

gpui_base/
text_selection.rs

1use std::{
2    collections::HashMap,
3    ops::Range,
4    rc::Rc,
5    sync::atomic::{AtomicU64, Ordering},
6};
7
8use gpui::{
9    App, AppContext as _, Bounds, Context, Element, ElementId, Entity, EntityId, EventEmitter,
10    Global, GlobalElementId, Half, Hitbox, InputEvent as _, InspectorElementId, IntoElement,
11    LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
12    ScrollDelta, ScrollWheelEvent, SharedString, Style, Subscription, TextLayout, WeakEntity,
13    Window, point, px,
14};
15
16use crate::text_boundary::{line_range_at, word_range_at};
17use crate::{AutoScroll, GlobalState};
18
19/// An opaque selection layer identifier.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub struct TextSelectionScopeId(u64);
22
23impl TextSelectionScopeId {
24    /// Allocates a process-unique scope identifier.
25    ///
26    /// Keep the returned identifier for the semantic lifetime of the scope;
27    /// do not allocate a new identifier on every frame.
28    pub fn new() -> Self {
29        static NEXT_SCOPE_ID: AtomicU64 = AtomicU64::new(1);
30        let value = NEXT_SCOPE_ID
31            .try_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
32                value.checked_add(1)
33            })
34            .expect("text selection scope identifiers exhausted");
35        Self(value)
36    }
37
38    #[cfg(test)]
39    const fn from_raw(value: u64) -> Self {
40        Self(value)
41    }
42}
43
44/// Stable participant-defined identity for virtualized participant content.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct TextSelectionContentKey(u64);
47
48impl TextSelectionContentKey {
49    /// Creates a key from a participant-defined stable content identity.
50    pub const fn new(value: u64) -> Self {
51        Self(value)
52    }
53
54    /// Returns the participant-defined value.
55    pub const fn value(self) -> u64 {
56        self.0
57    }
58}
59
60/// A selection endpoint anchored to a participant's content coordinates.
61#[derive(Clone, Copy, Debug, PartialEq)]
62pub struct TextSelectionEndpoint {
63    entity_id: Option<EntityId>,
64    point: Point<Pixels>,
65    content_key: Option<TextSelectionContentKey>,
66}
67
68impl TextSelectionEndpoint {
69    /// Creates an endpoint at a participant-relative content point.
70    pub(crate) const fn new(entity_id: Option<EntityId>, point: Point<Pixels>) -> Self {
71        Self {
72            entity_id,
73            point,
74            content_key: None,
75        }
76    }
77
78    /// Sets participant-defined endpoint metadata.
79    pub(crate) const fn with_content_key(mut self, content_key: TextSelectionContentKey) -> Self {
80        self.content_key = Some(content_key);
81        self
82    }
83
84    /// Returns the participant which owns this endpoint, when it hit one.
85    pub const fn entity_id(&self) -> Option<EntityId> {
86        self.entity_id
87    }
88
89    /// Returns the participant-relative content point.
90    pub const fn content_point(&self) -> Point<Pixels> {
91        self.point
92    }
93
94    /// Returns participant-defined endpoint metadata captured when it hit a participant.
95    pub const fn content_key(&self) -> Option<TextSelectionContentKey> {
96        self.content_key
97    }
98}
99
100/// Window-coordinate anchor and cursor points for painting a selection.
101#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct TextSelectionWindowPoints {
103    anchor: Point<Pixels>,
104    cursor: Point<Pixels>,
105}
106
107impl TextSelectionWindowPoints {
108    /// Returns the stable anchor in window coordinates.
109    pub const fn anchor(&self) -> Point<Pixels> {
110        self.anchor
111    }
112
113    /// Returns the moving cursor in window coordinates.
114    pub const fn cursor(&self) -> Point<Pixels> {
115        self.cursor
116    }
117}
118
119/// Participant-relative selection endpoints with an optional rendering projection.
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct TextSelectionSnapshot {
122    anchor: TextSelectionEndpoint,
123    cursor: TextSelectionEndpoint,
124    is_selecting: bool,
125    window_points: Option<TextSelectionWindowPoints>,
126    coverage: TextSelectionCoverage,
127}
128
129/// How much of one participant participates in a window selection.
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub enum TextSelectionCoverage {
132    /// Only the interval between this participant's two endpoints is selected.
133    #[default]
134    Bounded,
135    /// The participant is selected from its beginning through its endpoint.
136    FromStart,
137    /// The participant is selected from its endpoint through its end.
138    ToEnd,
139    /// The entire participant lies between endpoints in other participants.
140    Full,
141}
142
143impl TextSelectionSnapshot {
144    /// Creates a snapshot from stable participant-relative endpoints.
145    pub(crate) const fn new(anchor: TextSelectionEndpoint, cursor: TextSelectionEndpoint) -> Self {
146        Self {
147            anchor,
148            cursor,
149            is_selecting: false,
150            window_points: None,
151            coverage: TextSelectionCoverage::Bounded,
152        }
153    }
154
155    /// Sets whether the pointer gesture is still active.
156    pub(crate) const fn with_selecting(mut self, is_selecting: bool) -> Self {
157        self.is_selecting = is_selecting;
158        self
159    }
160
161    /// Sets the current window-coordinate rendering projection.
162    pub(crate) const fn with_window_points(
163        mut self,
164        window_points: Option<TextSelectionWindowPoints>,
165    ) -> Self {
166        self.window_points = window_points;
167        self
168    }
169
170    /// Sets the portion of the receiving participant covered by this selection.
171    #[cfg(test)]
172    pub(crate) const fn with_coverage(mut self, coverage: TextSelectionCoverage) -> Self {
173        self.coverage = coverage;
174        self
175    }
176
177    /// Returns the stable anchor endpoint.
178    pub const fn anchor(&self) -> TextSelectionEndpoint {
179        self.anchor
180    }
181
182    /// Returns the moving cursor endpoint.
183    pub const fn cursor(&self) -> TextSelectionEndpoint {
184        self.cursor
185    }
186
187    /// Returns whether the pointer gesture is still active.
188    pub const fn is_selecting(&self) -> bool {
189        self.is_selecting
190    }
191
192    /// Returns the window-coordinate endpoints for participants that need them.
193    pub const fn window_points(&self) -> Option<TextSelectionWindowPoints> {
194        self.window_points
195    }
196
197    /// Returns the portion of the receiving participant covered by this selection.
198    pub const fn coverage(&self) -> TextSelectionCoverage {
199        self.coverage
200    }
201}
202
203/// Per-frame geometry reported by a [`TextSelectionHandle`] participant.
204pub struct TextSelectionRegistration {
205    hitbox: Hitbox,
206    bounds: Bounds<Pixels>,
207    scroll_offset: Point<Pixels>,
208    scope: TextSelectionScopeId,
209    document_order: u64,
210    text_bounds: Vec<Bounds<Pixels>>,
211    self_scroll: bool,
212}
213
214impl TextSelectionRegistration {
215    /// Creates a registration with default scope, order, and scroll offset.
216    pub fn new(hitbox: Hitbox, bounds: Bounds<Pixels>) -> Self {
217        Self {
218            hitbox,
219            bounds,
220            scroll_offset: Point::default(),
221            scope: TextSelectionScopeId::default(),
222            document_order: 0,
223            text_bounds: Vec::new(),
224            self_scroll: false,
225        }
226    }
227
228    /// Marks a participant that scrolls its own content in response to
229    /// [`TextSelectionEvent::AutoScroll`]. Drag auto-scroll then drives it
230    /// directly, measured against its own bounds, instead of synthesizing a
231    /// wheel event for the nearest scrollable ancestor.
232    pub(crate) fn with_self_scroll(mut self, self_scroll: bool) -> Self {
233        self.self_scroll = self_scroll;
234        self
235    }
236
237    /// Sets the participant's content scroll offset.
238    pub fn with_scroll_offset(mut self, scroll_offset: Point<Pixels>) -> Self {
239        self.scroll_offset = scroll_offset;
240        self
241    }
242
243    /// Sets the opaque selection scope.
244    pub fn with_scope(mut self, scope: TextSelectionScopeId) -> Self {
245        self.scope = scope;
246        self
247    }
248
249    /// Sets the stable logical document order.
250    pub fn with_document_order(mut self, document_order: u64) -> Self {
251        self.document_order = document_order;
252        self
253    }
254
255    /// Sets the glyph-bearing bounds used to reject blank-only gestures.
256    pub fn with_text_bounds(mut self, text_bounds: Vec<Bounds<Pixels>>) -> Self {
257        self.text_bounds = text_bounds;
258        self
259    }
260
261    /// Returns the participant hitbox.
262    pub fn hitbox(&self) -> &Hitbox {
263        &self.hitbox
264    }
265
266    /// Returns the participant's window-coordinate bounds.
267    pub const fn bounds(&self) -> Bounds<Pixels> {
268        self.bounds
269    }
270
271    /// Returns the participant's content scroll offset.
272    pub const fn scroll_offset(&self) -> Point<Pixels> {
273        self.scroll_offset
274    }
275
276    /// Returns the opaque selection scope.
277    pub const fn scope(&self) -> TextSelectionScopeId {
278        self.scope
279    }
280
281    /// Returns the stable logical document order.
282    pub const fn document_order(&self) -> u64 {
283        self.document_order
284    }
285
286    /// Returns the glyph-bearing bounds used to reject blank-only gestures.
287    pub fn text_bounds(&self) -> &[Bounds<Pixels>] {
288        &self.text_bounds
289    }
290}
291
292/// Laid-out text reported by a plain selection participant during paint.
293#[derive(Clone)]
294pub struct TextSelectionRun {
295    /// Logical order within the containing participant.
296    document_order: u64,
297    /// The exact text used to produce `layout`.
298    text: SharedString,
299    /// Laid-out glyph geometry in window coordinates.
300    layout: TextLayout,
301    /// The run's window-coordinate paint bounds.
302    bounds: Bounds<Pixels>,
303}
304
305impl TextSelectionRun {
306    /// Creates a laid-out text run.
307    pub fn new(text: impl Into<SharedString>, layout: TextLayout, bounds: Bounds<Pixels>) -> Self {
308        Self {
309            document_order: 0,
310            text: text.into(),
311            layout,
312            bounds,
313        }
314    }
315
316    /// Sets the run's logical order within the participant.
317    pub const fn with_document_order(mut self, document_order: u64) -> Self {
318        self.document_order = document_order;
319        self
320    }
321
322    /// Returns the run's logical order within its participant.
323    pub const fn document_order(&self) -> u64 {
324        self.document_order
325    }
326
327    /// Returns the exact text used to produce the layout.
328    pub fn text(&self) -> &SharedString {
329        &self.text
330    }
331
332    /// Returns the laid-out glyph geometry.
333    pub fn layout(&self) -> &TextLayout {
334        &self.layout
335    }
336
337    /// Returns the run's window-coordinate paint bounds.
338    pub const fn bounds(&self) -> Bounds<Pixels> {
339        self.bounds
340    }
341}
342
343/// Selection projected onto a participant's laid-out text runs.
344#[derive(Clone, Debug, Default, PartialEq, Eq)]
345pub struct TextSelectionProjection {
346    /// Selected UTF-8 byte ranges paired with the input runs.
347    ranges: Vec<Option<Range<usize>>>,
348    /// Whether the participant participates in the current selection.
349    is_active: bool,
350}
351
352impl TextSelectionProjection {
353    /// Returns selected UTF-8 byte ranges paired with the input runs.
354    pub fn ranges(&self) -> &[Option<Range<usize>>] {
355        &self.ranges
356    }
357
358    /// Returns whether the participant participates in the selection.
359    pub const fn is_active(&self) -> bool {
360        self.is_active
361    }
362}
363
364/// Projects a participant selection snapshot onto laid-out plain-text runs.
365///
366/// The returned states retain the input order so callers can pair every state
367/// with its run. The ranges are always character boundaries; `order` is used
368/// only when a participant caches selected text for copying.
369fn project_ranges(
370    snapshot: Option<TextSelectionSnapshot>,
371    runs: &[TextSelectionRun],
372) -> TextSelectionProjection {
373    let Some(snapshot) = snapshot else {
374        return TextSelectionProjection {
375            ranges: vec![None; runs.len()],
376            is_active: false,
377        };
378    };
379    let Some(window_points) = snapshot.window_points() else {
380        return TextSelectionProjection {
381            ranges: vec![None; runs.len()],
382            is_active: true,
383        };
384    };
385
386    TextSelectionProjection {
387        ranges: runs
388            .iter()
389            .map(|run| selection_range_for_run(run, window_points.anchor, window_points.cursor))
390            .collect(),
391        is_active: true,
392    }
393}
394
395fn selection_range_for_run(
396    run: &TextSelectionRun,
397    selection_start: Point<Pixels>,
398    selection_end: Point<Pixels>,
399) -> Option<Range<usize>> {
400    if run.text.len() != run.layout.len() {
401        return None;
402    }
403
404    let line_height = run.layout.line_height();
405    let mut range = None;
406    for (offset, character) in run.text.char_indices() {
407        let next_offset = offset + character.len_utf8();
408        let Some(position) = run.layout.position_for_index(offset) else {
409            continue;
410        };
411
412        let char_width = run
413            .layout
414            .position_for_index(next_offset)
415            .filter(|next| next.y == position.y)
416            .map_or_else(|| line_height.half(), |next| next.x - position.x);
417
418        if point_in_selection_band(
419            position,
420            char_width,
421            selection_start,
422            selection_end,
423            line_height,
424        ) {
425            range.get_or_insert(offset..offset).end = next_offset;
426        }
427    }
428    range
429}
430
431fn points_for_multi_click(
432    runs: &[TextSelectionRun],
433    position: Point<Pixels>,
434    click_count: usize,
435) -> Option<(Point<Pixels>, Point<Pixels>)> {
436    let run = runs.iter().find(|run| run.bounds.contains(&position))?;
437    if run.text.len() != run.layout.len() {
438        return None;
439    }
440    let offset = run.layout.index_for_position(position).ok()?;
441    let range = match click_count {
442        2 => word_range_at(&run.text, offset)?,
443        3.. => line_range_at(&run.text, offset),
444        _ => return None,
445    };
446    if range.is_empty() {
447        return None;
448    }
449    Some((
450        run.layout.position_for_index(range.start)?,
451        run.layout.position_for_index(range.end)?,
452    ))
453}
454
455fn point_in_selection_band(
456    position: Point<Pixels>,
457    char_width: Pixels,
458    selection_start: Point<Pixels>,
459    selection_end: Point<Pixels>,
460    line_height: Pixels,
461) -> bool {
462    let point_in_line =
463        |point: Point<Pixels>| point.y >= position.y && point.y < position.y + line_height;
464    let top = selection_start.y.min(selection_end.y);
465    let bottom = selection_start.y.max(selection_end.y);
466    let x = position.x + char_width.half();
467
468    if position.y + line_height <= top || position.y > bottom {
469        return false;
470    }
471
472    if point_in_line(selection_start) && point_in_line(selection_end) {
473        let left = selection_start.x.min(selection_end.x);
474        let right = selection_start.x.max(selection_end.x);
475        return x >= left && x <= right;
476    }
477
478    let (top_point, bottom_point) = if selection_start.y < selection_end.y {
479        (selection_start, selection_end)
480    } else {
481        (selection_end, selection_start)
482    };
483    if point_in_line(top_point) {
484        x >= top_point.x
485    } else if point_in_line(bottom_point) {
486        x <= bottom_point.x
487    } else {
488        true
489    }
490}
491
492type FocusCallback = Rc<dyn Fn(&mut Window, &mut App)>;
493type ClearHandler = Rc<dyn Fn(&mut App)>;
494type CopyCallback = Rc<dyn Fn(&mut App) -> String>;
495type ContentKeyResolver = Rc<dyn Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey>>;
496
497/// Notifications emitted by a text-selection participant.
498#[derive(Clone, Copy, Debug, PartialEq)]
499pub enum TextSelectionEvent {
500    /// The participant's window-selection projection changed.
501    SelectionChanged(Option<TextSelectionSnapshot>),
502    /// The active drag requests vertical auto-scroll, or `None` to stop.
503    AutoScroll(Option<Pixels>),
504    /// Window selection cleared the participant's participant-local state.
505    Cleared,
506}
507
508struct CopyItem {
509    document_order: u64,
510    callback: Option<CopyCallback>,
511    fallback: String,
512}
513
514fn resolve_copy_items(mut items: Vec<CopyItem>, cx: &mut App) -> String {
515    items.sort_by_key(|item| item.document_order);
516    items
517        .into_iter()
518        .map(|item| {
519            item.callback
520                .map(|callback| callback(cx))
521                .unwrap_or(item.fallback)
522        })
523        .filter(|text| !text.trim().is_empty())
524        .collect::<Vec<_>>()
525        .join("\n")
526}
527
528fn dispatch_clear_handlers(handlers: Vec<ClearHandler>, cx: &mut App) {
529    for handler in handlers {
530        handler(cx);
531    }
532}
533
534struct SelectableTextState {
535    fallback_copy_text: String,
536    projected_copy_text: Option<String>,
537    runs: Vec<TextSelectionRun>,
538    local_selection: bool,
539    snapshot: Option<TextSelectionSnapshot>,
540    on_focus: Option<FocusCallback>,
541    clear: Option<ClearHandler>,
542    copy: Option<CopyCallback>,
543    content_key_resolver: Option<ContentKeyResolver>,
544}
545
546impl EventEmitter<TextSelectionEvent> for SelectableTextState {}
547
548impl SelectableTextState {
549    fn new(fallback_copy_text: impl Into<String>) -> Self {
550        Self {
551            fallback_copy_text: fallback_copy_text.into(),
552            projected_copy_text: None,
553            runs: Vec::new(),
554            local_selection: false,
555            snapshot: None,
556            on_focus: None,
557            clear: None,
558            copy: None,
559            content_key_resolver: None,
560        }
561    }
562
563    /// The current geometry selection snapshot for this participant.
564    fn snapshot(&self) -> Option<TextSelectionSnapshot> {
565        self.snapshot
566    }
567
568    /// Sets the text copied by this participant when it participates in selection.
569    fn set_fallback_copy_text(&mut self, text: impl Into<String>) {
570        self.fallback_copy_text = text.into();
571        self.projected_copy_text = None;
572    }
573
574    /// Marks participant-local selection (for example select-all) as active.
575    fn set_local_selection(&mut self, active: bool) {
576        self.local_selection = active;
577    }
578
579    /// Projects this participant's current snapshot onto plain-text runs and caches
580    /// their selected substrings for the window selection query.
581    ///
582    /// Call this once per painted run. A snapshot change or
583    /// Clearing window selection invalidates the cache immediately, so copy
584    /// never returns text from a previous projection while waiting to repaint.
585    fn update_runs(&mut self, runs: &[TextSelectionRun]) -> TextSelectionProjection {
586        self.runs = runs.to_vec();
587        let states = project_ranges(self.snapshot, runs);
588        let mut selected_runs = runs
589            .iter()
590            .zip(states.ranges())
591            .enumerate()
592            .filter_map(|(index, (run, state))| {
593                state.as_ref().map(|range| {
594                    debug_assert!(run.text.is_char_boundary(range.start));
595                    debug_assert!(run.text.is_char_boundary(range.end));
596                    (
597                        run.document_order,
598                        index,
599                        run.text[range.clone()].to_string(),
600                    )
601                })
602            })
603            .collect::<Vec<_>>();
604        selected_runs.sort_by_key(|(order, index, _)| (*order, *index));
605        self.projected_copy_text =
606            Some(selected_runs.into_iter().map(|(_, _, text)| text).collect());
607        states
608    }
609
610    /// Installs the callback which focuses the participant when a drag begins in it.
611    fn set_focus_handler(&mut self, callback: impl Fn(&mut Window, &mut App) + 'static) {
612        self.on_focus = Some(Rc::new(callback));
613    }
614
615    fn clear_with(&mut self, callback: impl Fn(&mut App) + 'static) {
616        self.clear = Some(Rc::new(callback));
617    }
618
619    /// Installs a participant-specific copy projection.
620    fn copy_with(&mut self, callback: impl Fn(&mut App) -> String + 'static) {
621        self.copy = Some(Rc::new(callback));
622    }
623
624    /// Installs a participant-specific lookup for stable virtualized content keys.
625    fn resolve_content_key_with(
626        &mut self,
627        callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
628    ) {
629        self.content_key_resolver = Some(Rc::new(callback));
630    }
631
632    fn set_snapshot(&mut self, snapshot: Option<TextSelectionSnapshot>, cx: &mut Context<Self>) {
633        if self.snapshot == snapshot {
634            return;
635        }
636        self.snapshot = snapshot;
637        self.projected_copy_text = None;
638        cx.emit(TextSelectionEvent::SelectionChanged(snapshot));
639    }
640
641    fn clear_state(&mut self, cx: &mut Context<Self>) -> Option<ClearHandler> {
642        self.snapshot = None;
643        self.projected_copy_text = None;
644        self.local_selection = false;
645        cx.emit(TextSelectionEvent::Cleared);
646        cx.emit(TextSelectionEvent::SelectionChanged(None));
647        self.clear.clone()
648    }
649
650    fn set_auto_scroll(&self, delta: Option<Pixels>, cx: &mut Context<Self>) {
651        cx.emit(TextSelectionEvent::AutoScroll(delta));
652    }
653
654    fn focus(&self, window: &mut Window, cx: &mut App) {
655        if let Some(callback) = self.on_focus.clone() {
656            window.defer(cx, move |window, cx| callback(window, cx));
657        }
658    }
659
660    fn copy_item(&self, document_order: u64) -> Option<CopyItem> {
661        (self.snapshot.is_some() || self.local_selection).then(|| CopyItem {
662            document_order,
663            callback: self.copy.clone(),
664            fallback: self
665                .projected_copy_text
666                .clone()
667                .unwrap_or_else(|| self.fallback_copy_text.clone()),
668        })
669    }
670}
671
672/// A stable, participant-neutral handle for text that participates in window selection.
673#[derive(Clone)]
674pub struct TextSelectionHandle(Entity<SelectableTextState>);
675
676impl TextSelectionHandle {
677    /// Creates a selection participant handle with fallback text for copying.
678    pub fn new(fallback_copy_text: impl Into<String>, cx: &mut App) -> Self {
679        Self(cx.new(|_| SelectableTextState::new(fallback_copy_text)))
680    }
681
682    /// Returns this participant's stable identity.
683    pub fn entity_id(&self) -> EntityId {
684        self.0.entity_id()
685    }
686
687    /// Returns the current geometry selection snapshot for this participant.
688    pub fn snapshot(&self, cx: &App) -> Option<TextSelectionSnapshot> {
689        self.0.read(cx).snapshot()
690    }
691
692    /// Sets the fallback text copied while this participant participates.
693    pub fn set_fallback_copy_text(&self, text: impl Into<String>, cx: &mut App) {
694        self.0
695            .update(cx, |state, _| state.set_fallback_copy_text(text));
696    }
697
698    /// Marks participant-local selection, such as select-all, as active.
699    pub fn set_local_selection(&self, active: bool, cx: &mut App) {
700        self.0
701            .update(cx, |state, _| state.set_local_selection(active));
702    }
703
704    /// Returns whether participant-local selection is active.
705    pub fn has_local_selection(&self, cx: &App) -> bool {
706        self.0.read(cx).local_selection
707    }
708
709    /// Registers this participant and its geometry for the current frame.
710    pub fn register(
711        &self,
712        mut registration: TextSelectionRegistration,
713        window: &mut Window,
714        cx: &mut App,
715    ) {
716        if let Some(scope) = current_text_selection_scope(window.window_handle().window_id(), cx) {
717            registration.scope = scope;
718        }
719        let Some(state) = WindowSelectionState::existing(window, cx) else {
720            return;
721        };
722        state.update(cx, |state, cx| {
723            state.register_participant(self.clone(), registration, cx)
724        });
725    }
726
727    /// Projects the current snapshot onto plain-text runs and caches their copy text.
728    pub fn update_runs(&self, runs: &[TextSelectionRun], cx: &mut App) -> TextSelectionProjection {
729        self.0.update(cx, |state, _| state.update_runs(runs))
730    }
731
732    /// Subscribes to participant selection notifications.
733    pub fn subscribe(
734        &self,
735        mut callback: impl FnMut(&TextSelectionEvent, &mut App) + 'static,
736        cx: &mut App,
737    ) -> Subscription {
738        cx.subscribe(&self.0, move |_, event, cx| callback(event, cx))
739    }
740
741    /// Subscribes `window` to refresh whenever this participant's selection changes.
742    #[must_use = "retain the subscription or explicitly detach it"]
743    pub fn refresh_window_on_change(&self, window: &Window, cx: &mut App) -> Subscription {
744        let window = window.window_handle();
745        self.subscribe(
746            move |event, cx| {
747                if matches!(event, TextSelectionEvent::SelectionChanged(_)) {
748                    _ = window.update(cx, |_, window, _| window.refresh());
749                }
750            },
751            cx,
752        )
753    }
754
755    /// Sets the callback which focuses the participant when a drag begins in it.
756    pub fn focus_with(&self, callback: impl Fn(&mut Window, &mut App) + 'static, cx: &mut App) {
757        self.0
758            .update(cx, |state, _| state.set_focus_handler(callback));
759    }
760
761    /// Sets the synchronous participant cleanup command used by window clear.
762    pub fn clear_with(&self, callback: impl Fn(&mut App) + 'static, cx: &mut App) {
763        self.0.update(cx, |state, _| state.clear_with(callback));
764    }
765
766    /// Sets a participant-specific copy projection.
767    pub fn copy_with(&self, callback: impl Fn(&mut App) -> String + 'static, cx: &mut App) {
768        self.0.update(cx, |state, _| state.copy_with(callback));
769    }
770
771    /// Sets a participant-specific lookup for stable virtualized content keys.
772    pub fn resolve_content_key_with(
773        &self,
774        callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
775        cx: &mut App,
776    ) {
777        self.0
778            .update(cx, |state, _| state.resolve_content_key_with(callback));
779    }
780
781    fn downgrade(&self) -> WeakEntity<SelectableTextState> {
782        self.0.downgrade()
783    }
784}
785
786#[derive(Clone)]
787struct ParticipantRegistration {
788    participant: WeakEntity<SelectableTextState>,
789    registration: Rc<TextSelectionRegistration>,
790    generation: u64,
791}
792
793#[derive(Clone)]
794struct SelectionEndpoint {
795    participant: Option<WeakEntity<SelectableTextState>>,
796    point: Point<Pixels>,
797    inside: bool,
798    inside_text: bool,
799    content_key: Option<TextSelectionContentKey>,
800    content_key_resolver: Option<(ContentKeyResolver, Point<Pixels>)>,
801}
802
803impl SelectionEndpoint {
804    fn snapshot(&self) -> TextSelectionEndpoint {
805        let snapshot = TextSelectionEndpoint::new(self.entity_id(), self.point);
806        if let Some(content_key) = self.content_key {
807            snapshot.with_content_key(content_key)
808        } else {
809            snapshot
810        }
811    }
812
813    fn resolve(
814        &self,
815        participants: &HashMap<EntityId, ParticipantRegistration>,
816    ) -> Option<Point<Pixels>> {
817        let participant = self.participant.as_ref()?;
818        let registration = participants.get(&participant.entity_id())?;
819        participant.upgrade()?;
820        Some(
821            self.point
822                + registration.registration.scroll_offset
823                + registration.registration.bounds.origin,
824        )
825    }
826
827    fn entity_id(&self) -> Option<EntityId> {
828        self.participant
829            .as_ref()
830            .map(|participant| participant.entity_id())
831    }
832}
833
834/// Window-local generic text-selection state.
835#[derive(Default)]
836struct WindowSelectionState {
837    participants: HashMap<EntityId, ParticipantRegistration>,
838    active_scope: TextSelectionScopeId,
839    anchor: Option<SelectionEndpoint>,
840    cursor: Option<SelectionEndpoint>,
841    pending_extension_anchor: Option<SelectionEndpoint>,
842    is_selecting: bool,
843    did_hit_text: bool,
844    frame_generation: u64,
845    finish_frame_scheduled: bool,
846    refresh_held_cursor: bool,
847    mouse_down_prepared: bool,
848    auto_scroll: AutoScroll,
849}
850
851impl WindowSelectionState {
852    fn resolve_content_keys(state: &Entity<Self>, cx: &mut App) {
853        let pending = state.update(cx, |state, _| {
854            [
855                state
856                    .anchor
857                    .as_ref()
858                    .and_then(|endpoint| endpoint.content_key_resolver.clone()),
859                state
860                    .cursor
861                    .as_ref()
862                    .and_then(|endpoint| endpoint.content_key_resolver.clone()),
863            ]
864        });
865        let resolved =
866            pending.map(|pending| pending.and_then(|(callback, point)| callback(point, cx)));
867        state.update(cx, |state, cx| {
868            if let (Some(endpoint), Some(key)) = (state.anchor.as_mut(), resolved[0]) {
869                endpoint.content_key = Some(key);
870                endpoint.content_key_resolver = None;
871            }
872            if let (Some(endpoint), Some(key)) = (state.cursor.as_mut(), resolved[1]) {
873                endpoint.content_key = Some(key);
874                endpoint.content_key_resolver = None;
875            }
876            state.publish_snapshots(cx);
877        });
878    }
879    fn acquire(window_id: gpui::WindowId, cx: &mut App) -> Entity<Self> {
880        if !cx.has_global::<SelectionStateRegistry>() {
881            cx.set_global(SelectionStateRegistry::default());
882        }
883        if let Some(state) = cx
884            .global::<SelectionStateRegistry>()
885            .0
886            .get(&window_id)
887            .and_then(WeakEntity::upgrade)
888        {
889            return state;
890        }
891
892        let active_scope = if cx.has_global::<PendingTextSelectionScopes>() {
893            cx.global_mut::<PendingTextSelectionScopes>()
894                .0
895                .remove(&window_id)
896                .unwrap_or_default()
897        } else {
898            TextSelectionScopeId::default()
899        };
900
901        let state = cx.new(move |cx| {
902            let entity_id = cx.entity_id();
903            cx.on_release(move |state: &mut WindowSelectionState, cx| {
904                let handlers = state.clear_state(cx);
905                if cx.has_global::<SelectionStateRegistry>() {
906                    let registry = &mut cx.global_mut::<SelectionStateRegistry>().0;
907                    if registry
908                        .get(&window_id)
909                        .is_some_and(|state| state.entity_id() == entity_id)
910                    {
911                        registry.remove(&window_id);
912                    }
913                }
914                if !handlers.is_empty() {
915                    cx.defer(move |cx| dispatch_clear_handlers(handlers, cx));
916                }
917            })
918            .detach();
919            Self {
920                active_scope,
921                ..Self::default()
922            }
923        });
924        cx.global_mut::<SelectionStateRegistry>()
925            .0
926            .insert(window_id, state.downgrade());
927        state
928    }
929
930    #[cfg(test)]
931    fn ensure(window: &Window, cx: &mut App) -> Entity<Self> {
932        Self::acquire(window.window_handle().window_id(), cx)
933    }
934
935    fn existing(window: &Window, cx: &App) -> Option<Entity<Self>> {
936        if !cx.has_global::<SelectionStateRegistry>() {
937            return None;
938        }
939        cx.global::<SelectionStateRegistry>()
940            .0
941            .get(&window.window_handle().window_id())
942            .and_then(WeakEntity::upgrade)
943    }
944
945    /// Updates the active scope. Participants from other scopes cannot participate.
946    #[cfg(test)]
947    fn set_active_scope(&mut self, scope: TextSelectionScopeId, cx: &mut App) {
948        let handlers = self.set_active_scope_state(scope, cx);
949        dispatch_clear_handlers(handlers, cx);
950    }
951
952    fn set_active_scope_state(
953        &mut self,
954        scope: TextSelectionScopeId,
955        cx: &mut App,
956    ) -> Vec<ClearHandler> {
957        if self.active_scope == scope {
958            return Vec::new();
959        }
960        let handlers = self.clear_state(cx);
961        self.active_scope = scope;
962        self.publish_snapshots(cx);
963        handlers
964    }
965
966    /// Sweeps participants after a rendered frame has completed.
967    ///
968    /// Registrations are stamped with the current generation while any sibling
969    /// is painting. Sweeping only after paint makes registration independent of
970    /// whether a participant or the lifecycle element paints first.
971    pub fn finish_frame(&mut self, cx: &mut App) -> Vec<ClearHandler> {
972        self.finish_frame_scheduled = false;
973        let stale = self
974            .participants
975            .iter()
976            .filter_map(|(id, registration)| {
977                (registration.generation != self.frame_generation)
978                    .then(|| (*id, registration.participant.clone()))
979            })
980            .collect::<Vec<_>>();
981        let mut handlers = Vec::new();
982        for (id, participant) in stale {
983            self.participants.remove(&id);
984            if let Some(participant) = participant.upgrade() {
985                if let Some(handler) = participant.update(cx, |state, cx| state.clear_state(cx)) {
986                    handlers.push(handler);
987                }
988            }
989        }
990        self.publish_snapshots(cx);
991        self.frame_generation = self.frame_generation.wrapping_add(1);
992        handlers
993    }
994
995    fn schedule_finish_frame(&mut self) -> bool {
996        if self.finish_frame_scheduled {
997            return false;
998        }
999        self.finish_frame_scheduled = true;
1000        true
1001    }
1002
1003    /// Registers this frame's geometry for a participant.
1004    pub fn register_participant(
1005        &mut self,
1006        selection: TextSelectionHandle,
1007        registration: TextSelectionRegistration,
1008        cx: &mut App,
1009    ) {
1010        self.prune_dead_participants();
1011        if self.is_selecting
1012            && registration.self_scroll
1013            && self.anchor.as_ref().and_then(SelectionEndpoint::entity_id)
1014                == Some(selection.entity_id())
1015            && self
1016                .participants
1017                .get(&selection.entity_id())
1018                .is_some_and(|previous| {
1019                    previous.registration.scroll_offset != registration.scroll_offset
1020                        || previous.registration.bounds != registration.bounds
1021                })
1022        {
1023            self.refresh_held_cursor = true;
1024        }
1025        self.participants.insert(
1026            selection.entity_id(),
1027            ParticipantRegistration {
1028                participant: selection.downgrade(),
1029                registration: Rc::new(registration),
1030                generation: self.frame_generation,
1031            },
1032        );
1033        self.publish_snapshots(cx);
1034    }
1035
1036    /// Starts a selection gesture using bounds hit testing (useful to adapters/tests).
1037    #[cfg(test)]
1038    fn begin(&mut self, position: Point<Pixels>, extend: bool, cx: &mut App) {
1039        self.begin_impl(position, extend, false, None, cx);
1040    }
1041
1042    /// Updates the current gesture using bounds hit testing.
1043    #[cfg(test)]
1044    fn update(&mut self, position: Point<Pixels>, cx: &mut App) {
1045        self.update_impl(position, None, cx);
1046    }
1047
1048    /// Ends the current gesture and keeps its selection visible.
1049    pub fn end(&mut self, cx: &mut App) {
1050        self.pending_extension_anchor = None;
1051        if !self.is_selecting {
1052            return;
1053        }
1054        self.is_selecting = false;
1055        if !self.did_hit_text {
1056            self.anchor = None;
1057            self.cursor = None;
1058        }
1059        self.stop_anchor_auto_scroll(cx);
1060        self.publish_snapshots(cx);
1061    }
1062
1063    /// Clears both window selection and every participant's local selection.
1064    pub fn clear(&mut self, cx: &mut App) {
1065        let handlers = self.clear_state(cx);
1066        dispatch_clear_handlers(handlers, cx);
1067    }
1068
1069    fn clear_state(&mut self, cx: &mut App) -> Vec<ClearHandler> {
1070        self.stop_anchor_auto_scroll(cx);
1071        self.anchor = None;
1072        self.cursor = None;
1073        self.pending_extension_anchor = None;
1074        self.is_selecting = false;
1075        self.did_hit_text = false;
1076        self.prune_dead_participants();
1077        self.participants
1078            .values()
1079            .filter_map(|registration| registration.participant.upgrade())
1080            .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1081            .collect()
1082    }
1083
1084    fn copy_items(&self, cx: &App) -> Vec<CopyItem> {
1085        self.participants
1086            .values()
1087            .filter_map(|registration| {
1088                let participant = registration.participant.upgrade()?;
1089                participant
1090                    .read(cx)
1091                    .copy_item(registration.registration.document_order)
1092            })
1093            .collect()
1094    }
1095
1096    #[cfg(test)]
1097    fn selected_text(&self, cx: &mut App) -> String {
1098        resolve_copy_items(self.copy_items(cx), cx)
1099    }
1100
1101    /// Returns whether a drag or a participant-local selection is active.
1102    pub fn has_selection(&self, cx: &App) -> bool {
1103        self.snapshot().is_some()
1104            || self.participants.values().any(|registration| {
1105                registration
1106                    .participant
1107                    .upgrade()
1108                    .is_some_and(|participant| participant.read(cx).local_selection)
1109            })
1110    }
1111
1112    /// Returns the current resolved selection endpoints.
1113    pub fn snapshot(&self) -> Option<TextSelectionSnapshot> {
1114        if !self.did_hit_text {
1115            return None;
1116        }
1117        let anchor_endpoint = self.anchor.as_ref()?;
1118        let cursor_endpoint = self.cursor.as_ref()?;
1119        let anchor = anchor_endpoint.resolve(&self.participants)?;
1120        let cursor = cursor_endpoint.resolve(&self.participants)?;
1121        (anchor != cursor).then(|| {
1122            TextSelectionSnapshot::new(anchor_endpoint.snapshot(), cursor_endpoint.snapshot())
1123                .with_selecting(self.is_selecting)
1124                .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
1125        })
1126    }
1127
1128    /// Returns whether a drag is currently in progress.
1129    #[cfg(test)]
1130    fn is_selecting(&self) -> bool {
1131        self.is_selecting
1132    }
1133
1134    fn prepare_for_mouse_down(&mut self, extend: bool, cx: &mut App) -> Vec<ClearHandler> {
1135        let pending_extension_anchor = extend.then(|| self.anchor.clone()).flatten();
1136        self.stop_anchor_auto_scroll(cx);
1137        self.anchor = None;
1138        self.cursor = None;
1139        self.pending_extension_anchor = None;
1140        self.is_selecting = false;
1141        self.did_hit_text = false;
1142        self.prune_dead_participants();
1143        let handlers = self
1144            .participants
1145            .values()
1146            .filter_map(|registration| registration.participant.upgrade())
1147            .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1148            .collect();
1149        self.pending_extension_anchor = pending_extension_anchor;
1150        handlers
1151    }
1152
1153    fn begin_in_window(
1154        &mut self,
1155        position: Point<Pixels>,
1156        extend: bool,
1157        window: &mut Window,
1158        cx: &mut App,
1159    ) {
1160        self.begin_impl(position, extend, true, Some(window), cx);
1161    }
1162
1163    fn update_in_window(
1164        &mut self,
1165        position: Point<Pixels>,
1166        window: &Window,
1167        cx: &mut Context<Self>,
1168    ) {
1169        if !cx.has_active_drag() {
1170            self.update_impl(position, Some(window), cx);
1171            self.update_auto_scroll(position, window, cx);
1172        }
1173    }
1174
1175    fn select_at(
1176        &mut self,
1177        position: Point<Pixels>,
1178        click_count: usize,
1179        window: &mut Window,
1180        cx: &mut App,
1181    ) {
1182        GlobalState::init(cx);
1183        if GlobalState::is_text_selection_suppressed(cx) {
1184            return;
1185        }
1186        let hit = self.endpoint(position, Some(window), cx);
1187        if !hit.inside_text {
1188            return;
1189        }
1190        let Some(participant) = hit
1191            .participant
1192            .and_then(|participant| participant.upgrade())
1193        else {
1194            return;
1195        };
1196        let points = points_for_multi_click(&participant.read(cx).runs, position, click_count);
1197        let Some((anchor, cursor)) = points else {
1198            return;
1199        };
1200        let Some(registration) = self.participants.get(&participant.entity_id()) else {
1201            return;
1202        };
1203        let content_key_resolver = participant.read(cx).content_key_resolver.clone();
1204        let to_endpoint = |point: Point<Pixels>| {
1205            let content_point = point
1206                - registration.registration.bounds.origin
1207                - registration.registration.scroll_offset;
1208            SelectionEndpoint {
1209                participant: Some(participant.downgrade()),
1210                point: content_point,
1211                inside: true,
1212                inside_text: true,
1213                content_key: None,
1214                content_key_resolver: content_key_resolver
1215                    .clone()
1216                    .map(|resolver| (resolver, content_point)),
1217            }
1218        };
1219        self.anchor = Some(to_endpoint(anchor));
1220        self.cursor = Some(to_endpoint(cursor));
1221        self.did_hit_text = true;
1222        self.is_selecting = false;
1223        participant.update(cx, |state, cx| state.focus(window, cx));
1224        self.publish_snapshots(cx);
1225    }
1226
1227    #[cfg(test)]
1228    fn update_in_window_with_active_drag(
1229        &mut self,
1230        position: Point<Pixels>,
1231        active_drag: bool,
1232        window: &Window,
1233        cx: &mut App,
1234    ) {
1235        if !active_drag {
1236            self.update_impl(position, Some(window), cx);
1237        }
1238    }
1239
1240    fn begin_impl(
1241        &mut self,
1242        position: Point<Pixels>,
1243        extend: bool,
1244        already_prepared: bool,
1245        window: Option<&mut Window>,
1246        cx: &mut App,
1247    ) {
1248        GlobalState::init(cx);
1249        if GlobalState::is_text_selection_suppressed(cx) {
1250            self.pending_extension_anchor = None;
1251            return;
1252        }
1253        let previous_anchor = extend
1254            .then(|| {
1255                self.pending_extension_anchor
1256                    .take()
1257                    .or_else(|| self.anchor.clone())
1258            })
1259            .flatten()
1260            .filter(|anchor| anchor.resolve(&self.participants).is_some());
1261        if !extend && !already_prepared {
1262            self.clear(cx);
1263        }
1264        let endpoint = self.endpoint(position, window.as_deref(), cx);
1265        let focus_participant = endpoint
1266            .inside
1267            .then(|| endpoint.participant.clone())
1268            .flatten();
1269        let anchor = previous_anchor.unwrap_or_else(|| endpoint.clone());
1270        self.anchor = Some(anchor.clone());
1271        self.cursor = Some(endpoint.clone());
1272        self.did_hit_text = anchor.inside_text || endpoint.inside_text;
1273        self.is_selecting = true;
1274        if let Some(participant) = focus_participant.and_then(|participant| participant.upgrade()) {
1275            if let Some(window) = window {
1276                participant.update(cx, |state, cx| state.focus(window, cx));
1277            }
1278        }
1279        self.publish_snapshots(cx);
1280    }
1281
1282    fn update_impl(&mut self, position: Point<Pixels>, window: Option<&Window>, cx: &mut App) {
1283        if !self.is_selecting {
1284            return;
1285        }
1286        let endpoint = self.endpoint(position, window, cx);
1287        self.did_hit_text |= endpoint.inside_text;
1288        self.cursor = Some(endpoint);
1289        if window.is_none() {
1290            self.update_participant_auto_scroll(position, cx);
1291        }
1292        self.publish_snapshots(cx);
1293    }
1294
1295    fn endpoint(
1296        &mut self,
1297        position: Point<Pixels>,
1298        window: Option<&Window>,
1299        cx: &App,
1300    ) -> SelectionEndpoint {
1301        self.prune_dead_participants();
1302        let mut hit: Option<(
1303            WeakEntity<SelectableTextState>,
1304            Rc<TextSelectionRegistration>,
1305            f32,
1306        )> = None;
1307        let mut predecessor: Option<(
1308            WeakEntity<SelectableTextState>,
1309            Rc<TextSelectionRegistration>,
1310        )> = None;
1311        let mut first: Option<(
1312            WeakEntity<SelectableTextState>,
1313            Rc<TextSelectionRegistration>,
1314        )> = None;
1315
1316        for registration in self.participants.values() {
1317            if registration.registration.scope != self.active_scope
1318                || registration.participant.upgrade().is_none()
1319            {
1320                continue;
1321            }
1322            let participant_geometry = &registration.registration;
1323            let hovered = window.map_or_else(
1324                || participant_geometry.bounds.contains(&position),
1325                |window| participant_geometry.hitbox.is_hovered(window),
1326            );
1327            if hovered {
1328                let area = f32::from(participant_geometry.bounds.size.width)
1329                    * f32::from(participant_geometry.bounds.size.height);
1330                if hit.as_ref().is_none_or(|(_, best, best_area)| {
1331                    area < *best_area
1332                        || (area == *best_area
1333                            && participant_geometry.document_order < best.document_order)
1334                }) {
1335                    hit = Some((
1336                        registration.participant.clone(),
1337                        participant_geometry.clone(),
1338                        area,
1339                    ));
1340                }
1341            }
1342            if participant_geometry.bounds.top() <= position.y
1343                && predecessor.as_ref().is_none_or(|(_, best)| {
1344                    participant_geometry.bounds.top() > best.bounds.top()
1345                        || (participant_geometry.bounds.top() == best.bounds.top()
1346                            && participant_geometry.document_order < best.document_order)
1347                })
1348            {
1349                predecessor = Some((
1350                    registration.participant.clone(),
1351                    participant_geometry.clone(),
1352                ));
1353            }
1354            if first.as_ref().is_none_or(|(_, best)| {
1355                participant_geometry.bounds.top() < best.bounds.top()
1356                    || (participant_geometry.bounds.top() == best.bounds.top()
1357                        && participant_geometry.document_order < best.document_order)
1358            }) {
1359                first = Some((
1360                    registration.participant.clone(),
1361                    participant_geometry.clone(),
1362                ));
1363            }
1364        }
1365
1366        let selection = hit
1367            .map(|(participant, registration, _)| (participant, registration, true))
1368            .or_else(|| {
1369                predecessor
1370                    .or(first)
1371                    .map(|(participant, registration)| (participant, registration, false))
1372            });
1373        match selection {
1374            Some((participant, registration, inside)) => {
1375                let point = position - registration.bounds.origin - registration.scroll_offset;
1376                let content_key_resolver = participant.upgrade().and_then(|participant| {
1377                    participant
1378                        .read(cx)
1379                        .content_key_resolver
1380                        .clone()
1381                        .map(|callback| (callback, point))
1382                });
1383                SelectionEndpoint {
1384                    point,
1385                    participant: Some(participant),
1386                    inside,
1387                    inside_text: inside
1388                        && registration
1389                            .text_bounds
1390                            .iter()
1391                            .any(|bounds| bounds.contains(&position)),
1392                    content_key: None,
1393                    content_key_resolver,
1394                }
1395            }
1396            None => SelectionEndpoint {
1397                participant: None,
1398                point: position,
1399                inside: false,
1400                inside_text: false,
1401                content_key: None,
1402                content_key_resolver: None,
1403            },
1404        }
1405    }
1406
1407    fn publish_snapshots(&mut self, cx: &mut App) {
1408        self.prune_dead_participants();
1409        let snapshot = self.snapshot();
1410        let single_participant = self.single_participant();
1411        for (id, registration) in &self.participants {
1412            let Some(participant) = registration.participant.upgrade() else {
1413                continue;
1414            };
1415            let participant_snapshot = (registration.registration.scope == self.active_scope
1416                && self.participates(*id, registration)
1417                && single_participant.is_none_or(|single| single == *id))
1418            .then_some(snapshot)
1419            .flatten()
1420            .map(|mut snapshot| {
1421                snapshot.coverage = self.coverage_for(*id);
1422                snapshot
1423            });
1424            participant.update(cx, |state, cx| state.set_snapshot(participant_snapshot, cx));
1425        }
1426    }
1427
1428    fn coverage_for(&self, id: EntityId) -> TextSelectionCoverage {
1429        let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1430            return TextSelectionCoverage::Bounded;
1431        };
1432        let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1433            return TextSelectionCoverage::Bounded;
1434        };
1435        if anchor == cursor {
1436            return TextSelectionCoverage::Bounded;
1437        }
1438        let anchor_order = self.participants[&anchor].registration.document_order;
1439        let cursor_order = self.participants[&cursor].registration.document_order;
1440        if id != anchor && id != cursor {
1441            TextSelectionCoverage::Full
1442        } else if (id == anchor) == (anchor_order < cursor_order) {
1443            TextSelectionCoverage::ToEnd
1444        } else {
1445            TextSelectionCoverage::FromStart
1446        }
1447    }
1448
1449    fn single_participant(&self) -> Option<EntityId> {
1450        let anchor = self.anchor.as_ref()?.entity_id()?;
1451        let cursor = self.cursor.as_ref()?.entity_id()?;
1452        (anchor == cursor).then_some(anchor)
1453    }
1454
1455    fn participates(&self, id: EntityId, registration: &ParticipantRegistration) -> bool {
1456        let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1457            return false;
1458        };
1459        let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1460            return false;
1461        };
1462        let Some(anchor_registration) = self.participants.get(&anchor) else {
1463            return false;
1464        };
1465        let Some(cursor_registration) = self.participants.get(&cursor) else {
1466            return false;
1467        };
1468        let start = anchor_registration
1469            .registration
1470            .document_order
1471            .min(cursor_registration.registration.document_order);
1472        let end = anchor_registration
1473            .registration
1474            .document_order
1475            .max(cursor_registration.registration.document_order);
1476        (start..=end).contains(&registration.registration.document_order)
1477            || id == anchor
1478            || id == cursor
1479    }
1480
1481    fn update_auto_scroll(
1482        &mut self,
1483        position: Point<Pixels>,
1484        window: &Window,
1485        cx: &mut Context<Self>,
1486    ) {
1487        // A finished gesture keeps its anchor for shift-click extension; only
1488        // a live drag may scroll.
1489        if !self.is_selecting {
1490            return;
1491        }
1492        let Some((_, registration)) = self.anchor_registration() else {
1493            return;
1494        };
1495        // Exactly one writer per drag: a participant that scrolls its own
1496        // content is notified directly; anything else gets a synthetic wheel.
1497        if registration.self_scroll {
1498            self.auto_scroll.stop();
1499            self.update_participant_auto_scroll(position, cx);
1500            return;
1501        }
1502        // The content mask is the nearest clipping viewport established by a
1503        // scrollable ancestor. It remains stable as the participant itself
1504        // moves, so selection keeps scrolling the same related region even
1505        // after the anchor text has moved out of view.
1506        let visible_bounds = registration.hitbox.content_mask.bounds;
1507        // Keeps the synthesized wheel event hit-testing inside the mask.
1508        const HIT_TEST_INSET: Pixels = px(1.);
1509        // A collapsed mask leaves an empty clamp range below — stop.
1510        if visible_bounds.size.width < HIT_TEST_INSET * 2.
1511            || visible_bounds.size.height < HIT_TEST_INSET * 2.
1512        {
1513            self.stop_anchor_auto_scroll(cx);
1514            return;
1515        }
1516        let delta = AutoScroll::compute_delta(position.y, visible_bounds);
1517        let event_position = point(
1518            position.x.clamp(
1519                visible_bounds.left() + HIT_TEST_INSET,
1520                visible_bounds.right() - HIT_TEST_INSET,
1521            ),
1522            position.y.clamp(
1523                visible_bounds.top() + HIT_TEST_INSET,
1524                visible_bounds.bottom() - HIT_TEST_INSET,
1525            ),
1526        );
1527        self.auto_scroll.last_drag_position = Some(event_position);
1528        let window = window.window_handle();
1529        self.auto_scroll.set(delta, cx, move |delta, state, cx| {
1530            let Some(position) = state.auto_scroll.last_drag_position else {
1531                return;
1532            };
1533            let window = window;
1534            cx.defer(move |cx| {
1535                _ = window.update(cx, |_, window, cx| {
1536                    window.dispatch_event(
1537                        ScrollWheelEvent {
1538                            position,
1539                            delta: ScrollDelta::Pixels(point(px(0.), -delta)),
1540                            ..Default::default()
1541                        }
1542                        .to_platform_input(),
1543                        cx,
1544                    );
1545                });
1546            });
1547        });
1548    }
1549
1550    /// Drives the anchor participant's own scrolling, measured against the
1551    /// visible portion of the participant's element bounds.
1552    fn update_participant_auto_scroll(&self, position: Point<Pixels>, cx: &mut App) {
1553        let Some((participant, registration)) = self.anchor_registration() else {
1554            return;
1555        };
1556        let visible_bounds = registration
1557            .bounds
1558            .intersect(&registration.hitbox.content_mask.bounds);
1559        let delta = if visible_bounds.size.width > px(0.) && visible_bounds.size.height > px(0.) {
1560            AutoScroll::compute_delta(position.y, visible_bounds)
1561        } else {
1562            None
1563        };
1564        participant.update(cx, |state, cx| state.set_auto_scroll(delta, cx));
1565    }
1566
1567    fn stop_anchor_auto_scroll(&mut self, cx: &mut App) {
1568        self.auto_scroll.stop();
1569        let Some(participant) = self.anchor_participant() else {
1570            return;
1571        };
1572        participant.update(cx, |state, cx| state.set_auto_scroll(None, cx));
1573    }
1574
1575    /// The live participant owning the anchor of the current gesture.
1576    fn anchor_participant(&self) -> Option<Entity<SelectableTextState>> {
1577        self.anchor
1578            .as_ref()
1579            .filter(|anchor| anchor.inside)?
1580            .participant
1581            .as_ref()?
1582            .upgrade()
1583    }
1584
1585    /// The anchor participant together with its current frame registration.
1586    fn anchor_registration(
1587        &self,
1588    ) -> Option<(Entity<SelectableTextState>, Rc<TextSelectionRegistration>)> {
1589        let participant = self.anchor_participant()?;
1590        let registration = self.participants.get(&participant.entity_id())?;
1591        Some((participant, registration.registration.clone()))
1592    }
1593
1594    fn prune_dead_participants(&mut self) {
1595        self.participants
1596            .retain(|_, registration| registration.participant.upgrade().is_some());
1597    }
1598}
1599
1600#[derive(Default)]
1601/// Non-owning window locator; retained [`TextSelection`] element state owns
1602/// each live selection entity.
1603struct SelectionStateRegistry(HashMap<gpui::WindowId, WeakEntity<WindowSelectionState>>);
1604
1605impl Global for SelectionStateRegistry {}
1606
1607#[derive(Default)]
1608struct PendingTextSelectionScopes(HashMap<gpui::WindowId, TextSelectionScopeId>);
1609
1610impl Global for PendingTextSelectionScopes {}
1611
1612#[derive(Default)]
1613struct TextSelectionScopeStacks(HashMap<gpui::WindowId, Vec<TextSelectionScopeId>>);
1614
1615impl Global for TextSelectionScopeStacks {}
1616
1617fn push_text_selection_scope(window_id: gpui::WindowId, scope: TextSelectionScopeId, cx: &mut App) {
1618    if !cx.has_global::<TextSelectionScopeStacks>() {
1619        cx.set_global(TextSelectionScopeStacks::default());
1620    }
1621    cx.global_mut::<TextSelectionScopeStacks>()
1622        .0
1623        .entry(window_id)
1624        .or_default()
1625        .push(scope);
1626}
1627
1628fn pop_text_selection_scope(window_id: gpui::WindowId, cx: &mut App) {
1629    let stacks = &mut cx.global_mut::<TextSelectionScopeStacks>().0;
1630    let remove_stack = stacks.get_mut(&window_id).is_some_and(|stack| {
1631        stack.pop();
1632        stack.is_empty()
1633    });
1634    if remove_stack {
1635        stacks.remove(&window_id);
1636    }
1637}
1638
1639fn current_text_selection_scope(
1640    window_id: gpui::WindowId,
1641    cx: &App,
1642) -> Option<TextSelectionScopeId> {
1643    cx.has_global::<TextSelectionScopeStacks>()
1644        .then(|| {
1645            cx.global::<TextSelectionScopeStacks>()
1646                .0
1647                .get(&window_id)
1648                .and_then(|stack| stack.last().copied())
1649        })
1650        .flatten()
1651}
1652
1653fn with_text_selection_scope<T>(
1654    window_id: gpui::WindowId,
1655    scope: TextSelectionScopeId,
1656    cx: &mut App,
1657    callback: impl FnOnce(&mut App) -> T,
1658) -> T {
1659    push_text_selection_scope(window_id, scope, cx);
1660    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(cx)));
1661    pop_text_selection_scope(window_id, cx);
1662    match result {
1663        Ok(result) => result,
1664        Err(payload) => std::panic::resume_unwind(payload),
1665    }
1666}
1667
1668/// Window-level operations for text selection.
1669pub struct TextSelection;
1670
1671impl TextSelection {
1672    /// Returns the currently selected text in logical document order.
1673    pub fn selected_text(window: &mut Window, cx: &mut App) -> String {
1674        let Some(state) = live_text_selection_state(window, cx) else {
1675            return String::new();
1676        };
1677        let items = state.read(cx).copy_items(cx);
1678        resolve_copy_items(items, cx)
1679    }
1680
1681    /// Returns whether the window has a geometry selection or any participant
1682    /// has an active participant-local selection such as select-all.
1683    pub fn has_selection(window: &mut Window, cx: &mut App) -> bool {
1684        live_text_selection_state(window, cx).is_some_and(|state| state.read(cx).has_selection(cx))
1685    }
1686
1687    /// Clears window selection and every participant's local selection.
1688    pub fn clear(window: &mut Window, cx: &mut App) {
1689        if let Some(state) = live_text_selection_state(window, cx) {
1690            let handlers = state.update(cx, |state, cx| state.clear_state(cx));
1691            dispatch_clear_handlers(handlers, cx);
1692        }
1693    }
1694
1695    /// Clears selection for a known window identifier.
1696    ///
1697    /// Prefer [`Self::clear`] when a window reference is available. This
1698    /// narrow entry point supports hosts retiring deprecated window wrappers.
1699    pub fn clear_for_window(window_id: gpui::WindowId, cx: &mut App) {
1700        clear_window_text_selection(window_id, cx);
1701    }
1702
1703    /// Ends the current drag while leaving its selection visible.
1704    pub fn end(window: &mut Window, cx: &mut App) {
1705        if let Some(state) = live_text_selection_state(window, cx) {
1706            state.update(cx, |state, cx| state.end(cx));
1707        }
1708    }
1709
1710    /// Activates the opaque selection scope for this window.
1711    pub fn activate_scope(scope: TextSelectionScopeId, window: &mut Window, cx: &mut App) {
1712        let Some(state) = WindowSelectionState::existing(window, cx) else {
1713            if !cx.has_global::<PendingTextSelectionScopes>() {
1714                cx.set_global(PendingTextSelectionScopes::default());
1715            }
1716            cx.global_mut::<PendingTextSelectionScopes>()
1717                .0
1718                .insert(window.window_handle().window_id(), scope);
1719            return;
1720        };
1721        let handlers = state.update(cx, |state, cx| state.set_active_scope_state(scope, cx));
1722        dispatch_clear_handlers(handlers, cx);
1723    }
1724}
1725
1726/// A zero-sized root layer which enables text selection for a window.
1727///
1728/// Mount one as the root's first child. Its stable `"window-text-selection"`
1729/// element identity retains the window-local selection entity across frames.
1730pub struct TextSelectionLayer;
1731
1732pub(crate) fn text_selection_scope(
1733    scope: TextSelectionScopeId,
1734    element: impl IntoElement,
1735) -> impl IntoElement {
1736    TextSelectionScopeMarker {
1737        scope,
1738        element: element.into_element(),
1739    }
1740}
1741
1742struct TextSelectionScopeMarker<E> {
1743    scope: TextSelectionScopeId,
1744    element: E,
1745}
1746
1747impl<E: Element> IntoElement for TextSelectionScopeMarker<E> {
1748    type Element = Self;
1749
1750    fn into_element(self) -> Self::Element {
1751        self
1752    }
1753}
1754
1755impl<E: Element> Element for TextSelectionScopeMarker<E> {
1756    type RequestLayoutState = E::RequestLayoutState;
1757    type PrepaintState = E::PrepaintState;
1758
1759    fn id(&self) -> Option<ElementId> {
1760        self.element.id()
1761    }
1762
1763    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
1764        self.element.source_location()
1765    }
1766
1767    fn request_layout(
1768        &mut self,
1769        id: Option<&GlobalElementId>,
1770        inspector_id: Option<&InspectorElementId>,
1771        window: &mut Window,
1772        cx: &mut App,
1773    ) -> (LayoutId, Self::RequestLayoutState) {
1774        let window_id = window.window_handle().window_id();
1775        with_text_selection_scope(window_id, self.scope, cx, |cx| {
1776            self.element.request_layout(id, inspector_id, window, cx)
1777        })
1778    }
1779
1780    fn prepaint(
1781        &mut self,
1782        id: Option<&GlobalElementId>,
1783        inspector_id: Option<&InspectorElementId>,
1784        bounds: Bounds<Pixels>,
1785        request_layout: &mut Self::RequestLayoutState,
1786        window: &mut Window,
1787        cx: &mut App,
1788    ) -> Self::PrepaintState {
1789        let window_id = window.window_handle().window_id();
1790        with_text_selection_scope(window_id, self.scope, cx, |cx| {
1791            self.element
1792                .prepaint(id, inspector_id, bounds, request_layout, window, cx)
1793        })
1794    }
1795
1796    fn paint(
1797        &mut self,
1798        id: Option<&GlobalElementId>,
1799        inspector_id: Option<&InspectorElementId>,
1800        bounds: Bounds<Pixels>,
1801        request_layout: &mut Self::RequestLayoutState,
1802        prepaint: &mut Self::PrepaintState,
1803        window: &mut Window,
1804        cx: &mut App,
1805    ) {
1806        let window_id = window.window_handle().window_id();
1807        with_text_selection_scope(window_id, self.scope, cx, |cx| {
1808            self.element.paint(
1809                id,
1810                inspector_id,
1811                bounds,
1812                request_layout,
1813                prepaint,
1814                window,
1815                cx,
1816            );
1817        });
1818    }
1819}
1820
1821#[doc(hidden)]
1822pub struct TextSelectionLayerPrepaintState(Entity<WindowSelectionState>);
1823
1824impl IntoElement for TextSelectionLayer {
1825    type Element = Self;
1826
1827    fn into_element(self) -> Self::Element {
1828        self
1829    }
1830}
1831
1832impl Element for TextSelectionLayer {
1833    type RequestLayoutState = ();
1834    type PrepaintState = TextSelectionLayerPrepaintState;
1835
1836    fn id(&self) -> Option<ElementId> {
1837        Some("window-text-selection".into())
1838    }
1839
1840    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
1841        None
1842    }
1843
1844    fn request_layout(
1845        &mut self,
1846        _: Option<&GlobalElementId>,
1847        _: Option<&InspectorElementId>,
1848        window: &mut Window,
1849        cx: &mut App,
1850    ) -> (LayoutId, Self::RequestLayoutState) {
1851        (window.request_layout(Style::default(), [], cx), ())
1852    }
1853
1854    fn prepaint(
1855        &mut self,
1856        global_id: Option<&GlobalElementId>,
1857        _: Option<&InspectorElementId>,
1858        _: Bounds<Pixels>,
1859        _: &mut Self::RequestLayoutState,
1860        window: &mut Window,
1861        cx: &mut App,
1862    ) -> Self::PrepaintState {
1863        // Automatic participant order is paint order within this frame. Keep
1864        // this lifecycle in base so base-only applications do not need a
1865        // separate root component to reset it. Otherwise, registering the
1866        // first of two selected TextViews temporarily reverses their order
1867        // against the previous frame and alternates coverage forever.
1868        GlobalState::init(cx);
1869        GlobalState::global_mut(cx).begin_selection_frame();
1870        TextSelectionLayerPrepaintState(retain_text_selection_state(global_id, window, cx))
1871    }
1872
1873    fn paint(
1874        &mut self,
1875        _: Option<&GlobalElementId>,
1876        _: Option<&InspectorElementId>,
1877        _: Bounds<Pixels>,
1878        _: &mut Self::RequestLayoutState,
1879        state: &mut Self::PrepaintState,
1880        window: &mut Window,
1881        cx: &mut App,
1882    ) {
1883        paint_text_selection(&state.0, window, cx);
1884    }
1885}
1886
1887fn retain_text_selection_state(
1888    global_id: Option<&GlobalElementId>,
1889    window: &mut Window,
1890    cx: &mut App,
1891) -> Entity<WindowSelectionState> {
1892    let window_id = window.window_handle().window_id();
1893    let state = window.with_element_state::<Entity<WindowSelectionState>, _>(
1894        global_id.expect("TextSelection has a stable element id"),
1895        |retained, _| {
1896            let state = retained.unwrap_or_else(|| WindowSelectionState::acquire(window_id, cx));
1897            (state.clone(), state)
1898        },
1899    );
1900    if !cx.has_global::<SelectionStateRegistry>() {
1901        cx.set_global(SelectionStateRegistry::default());
1902    }
1903    cx.global_mut::<SelectionStateRegistry>()
1904        .0
1905        .insert(window_id, state.downgrade());
1906    state
1907}
1908
1909fn paint_text_selection(state: &Entity<WindowSelectionState>, window: &mut Window, cx: &mut App) {
1910    if state.update(cx, |state, _| state.schedule_finish_frame()) {
1911        let state = state.downgrade();
1912        window.defer(cx, move |window, cx| {
1913            let Some(state) = state.upgrade() else {
1914                return;
1915            };
1916            let handlers = state.update(cx, |state, cx| state.finish_frame(cx));
1917            dispatch_clear_handlers(handlers, cx);
1918            // Direct participant scrolling produces no wheel event. Refresh
1919            // the held cursor after paint registers the new scroll geometry.
1920            let refresh_cursor = state.update(cx, |state, cx| {
1921                if std::mem::take(&mut state.refresh_held_cursor) && state.is_selecting {
1922                    state.update_in_window(window.mouse_position(), window, cx);
1923                    true
1924                } else {
1925                    false
1926                }
1927            });
1928            if refresh_cursor {
1929                WindowSelectionState::resolve_content_keys(&state, cx);
1930            }
1931        });
1932    }
1933
1934    let mouse_down_state = state.downgrade();
1935    window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
1936        if event.button != MouseButton::Left {
1937            return;
1938        }
1939        let Some(state) = mouse_down_state.upgrade() else {
1940            return;
1941        };
1942        if phase.capture() {
1943            GlobalState::init(cx);
1944            GlobalState::reset_text_selection_suppression(cx);
1945            let handlers = state.update(cx, |state, cx| {
1946                if state.mouse_down_prepared {
1947                    return Vec::new();
1948                }
1949                state.mouse_down_prepared = true;
1950                state.prepare_for_mouse_down(event.click_count == 1 && event.modifiers.shift, cx)
1951            });
1952            dispatch_clear_handlers(handlers, cx);
1953        } else if event.click_count == 1 {
1954            if GlobalState::is_text_selection_suppressed(cx) {
1955                state.update(cx, |state, _| state.pending_extension_anchor = None);
1956                return;
1957            }
1958            state.update(cx, |state, cx| {
1959                if !state.is_selecting {
1960                    state.begin_in_window(event.position, event.modifiers.shift, window, cx)
1961                }
1962            });
1963            WindowSelectionState::resolve_content_keys(&state, cx);
1964        } else if event.click_count >= 2 {
1965            if GlobalState::is_text_selection_suppressed(cx) {
1966                return;
1967            }
1968            state.update(cx, |state, cx| {
1969                state.select_at(event.position, event.click_count, window, cx)
1970            });
1971            WindowSelectionState::resolve_content_keys(&state, cx);
1972        }
1973    });
1974
1975    let mouse_move_state = state.downgrade();
1976    window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
1977        if phase.bubble()
1978            && let Some(state) = mouse_move_state.upgrade()
1979        {
1980            state.update(cx, |state, cx| {
1981                state.update_in_window(event.position, window, cx)
1982            });
1983            WindowSelectionState::resolve_content_keys(&state, cx);
1984        }
1985    });
1986
1987    let mouse_up_state = state.downgrade();
1988    window.on_mouse_event(move |_: &MouseUpEvent, phase, _, cx| {
1989        if phase.bubble()
1990            && let Some(state) = mouse_up_state.upgrade()
1991        {
1992            state.update(cx, |state, cx| {
1993                state.mouse_down_prepared = false;
1994                state.end(cx)
1995            });
1996        }
1997    });
1998
1999    let scroll_state = state.downgrade();
2000    window.on_mouse_event(move |_: &ScrollWheelEvent, phase, window, cx| {
2001        if phase.bubble()
2002            && let Some(state) = scroll_state.upgrade()
2003        {
2004            let position = window.mouse_position();
2005            state.update(cx, |state, cx| state.update_in_window(position, window, cx));
2006            WindowSelectionState::resolve_content_keys(&state, cx);
2007        }
2008    });
2009}
2010
2011fn live_text_selection_state(
2012    window: &Window,
2013    cx: &mut App,
2014) -> Option<Entity<WindowSelectionState>> {
2015    WindowSelectionState::existing(window, cx)
2016}
2017
2018pub(crate) fn clear_window_text_selection(window_id: gpui::WindowId, cx: &mut App) {
2019    if !cx.has_global::<SelectionStateRegistry>() {
2020        return;
2021    }
2022    let Some(state) = cx
2023        .global::<SelectionStateRegistry>()
2024        .0
2025        .get(&window_id)
2026        .and_then(WeakEntity::upgrade)
2027    else {
2028        return;
2029    };
2030    let handlers = state.update(cx, |state, cx| state.clear_state(cx));
2031    dispatch_clear_handlers(handlers, cx);
2032}
2033
2034#[cfg(test)]
2035mod tests {
2036    use super::*;
2037    use crate::ElementExt as _;
2038    use gpui::{
2039        Bounds, ContentMask, Context, Hitbox, HitboxBehavior, HitboxId, InteractiveElement as _,
2040        IntoElement, ParentElement as _, Render, SharedString, Styled as _, StyledText,
2041        TestAppContext, TextLayout, Window, div, point, prelude::FluentBuilder as _, px, size,
2042    };
2043    use std::{
2044        cell::{Cell, RefCell},
2045        rc::Rc,
2046    };
2047
2048    struct FakeParticipant {
2049        selection: TextSelectionHandle,
2050    }
2051
2052    struct WindowSelectionView {
2053        selection: TextSelectionHandle,
2054    }
2055
2056    struct SelectionElementOnlyView;
2057    struct ToggleSelectionElementView {
2058        enabled: bool,
2059        selection: TextSelectionHandle,
2060    }
2061
2062    struct DoubleSelectionElementView {
2063        selection: TextSelectionHandle,
2064    }
2065
2066    struct WindowOwnedSelectionView {
2067        selection: TextSelectionHandle,
2068    }
2069
2070    struct FirstFrameScopedSelectionView {
2071        selection: TextSelectionHandle,
2072    }
2073
2074    struct PlainRunLayoutView {
2075        texts: Vec<SharedString>,
2076        layouts: Vec<TextLayout>,
2077    }
2078
2079    impl Render for WindowSelectionView {
2080        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2081            div()
2082        }
2083    }
2084
2085    impl Render for SelectionElementOnlyView {
2086        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2087            div()
2088                .size_full()
2089                .child(TextSelectionLayer)
2090                .child(
2091                    div()
2092                        .size_full()
2093                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
2094                            GlobalState::suppress_text_selection(cx);
2095                        }),
2096                )
2097        }
2098    }
2099
2100    impl Render for ToggleSelectionElementView {
2101        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2102            let selection = self.selection.clone();
2103            div().when(self.enabled, |this| {
2104                this.child(TextSelectionLayer)
2105                    .child(div().size_full().on_prepaint(move |bounds, window, cx| {
2106                        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2107                        selection.register(
2108                            TextSelectionRegistration::new(hitbox, bounds)
2109                                .with_text_bounds(vec![bounds]),
2110                            window,
2111                            cx,
2112                        );
2113                    }))
2114            })
2115        }
2116    }
2117
2118    impl Render for DoubleSelectionElementView {
2119        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2120            let selection = self.selection.clone();
2121            div()
2122                .size_full()
2123                .child(TextSelectionLayer)
2124                .child(TextSelectionLayer)
2125                .on_prepaint(move |bounds, window, cx| {
2126                    let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2127                    selection.register(
2128                        TextSelectionRegistration::new(hitbox, bounds)
2129                            .with_text_bounds(vec![bounds]),
2130                        window,
2131                        cx,
2132                    );
2133                })
2134        }
2135    }
2136
2137    impl Render for WindowOwnedSelectionView {
2138        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2139            let selection = self.selection.clone();
2140            div()
2141                .size_full()
2142                .child(TextSelectionLayer)
2143                .child(div().size_full().on_prepaint(move |bounds, window, cx| {
2144                    let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2145                    selection.register(
2146                        TextSelectionRegistration::new(hitbox, bounds)
2147                            .with_text_bounds(vec![bounds]),
2148                        window,
2149                        cx,
2150                    );
2151                }))
2152        }
2153    }
2154
2155    impl Render for FirstFrameScopedSelectionView {
2156        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2157            let scope = TextSelectionScopeId::from_raw(23);
2158            TextSelection::activate_scope(scope, window, cx);
2159            let selection = self.selection.clone();
2160
2161            div().child(TextSelectionLayer).child(
2162                div()
2163                    .size_full()
2164                    .on_prepaint(move |bounds, window, cx| {
2165                        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2166                        selection.register(
2167                            TextSelectionRegistration::new(hitbox, bounds)
2168                                .with_text_bounds(vec![bounds]),
2169                            window,
2170                            cx,
2171                        );
2172                    })
2173                    .text_selection_scope(scope),
2174            )
2175        }
2176    }
2177
2178    impl Render for PlainRunLayoutView {
2179        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2180            self.layouts.clear();
2181            let children = self
2182                .texts
2183                .iter()
2184                .enumerate()
2185                .map(|(index, text)| {
2186                    let text = StyledText::new(text.clone());
2187                    self.layouts.push(text.layout().clone());
2188                    div().absolute().top(px(index as f32 * 40.)).child(text)
2189                })
2190                .collect::<Vec<_>>();
2191            div().size_full().children(children)
2192        }
2193    }
2194
2195    impl FakeParticipant {
2196        fn new(text: &str, cx: &mut gpui::App) -> Self {
2197            let selection = TextSelectionHandle::new(text, cx);
2198            Self { selection }
2199        }
2200
2201        fn register(
2202            &self,
2203            selection_state: &mut WindowSelectionState,
2204            y: f32,
2205            scope: TextSelectionScopeId,
2206            document_order: u64,
2207            cx: &mut gpui::App,
2208        ) {
2209            let bounds = Bounds::new(point(px(0.), px(y)), size(px(100.), px(10.)));
2210            selection_state.register_participant(
2211                self.selection.clone(),
2212                TextSelectionRegistration::new(
2213                    Hitbox {
2214                        id: HitboxId::placeholder(),
2215                        bounds,
2216                        content_mask: ContentMask { bounds },
2217                        behavior: HitboxBehavior::Normal,
2218                    },
2219                    bounds,
2220                )
2221                .with_scope(scope)
2222                .with_document_order(document_order)
2223                .with_text_bounds(vec![bounds]),
2224                cx,
2225            );
2226        }
2227    }
2228
2229    fn laid_out_runs(texts: &[&str], cx: &mut TestAppContext) -> Vec<(SharedString, TextLayout)> {
2230        let texts = texts
2231            .iter()
2232            .map(|text| SharedString::from(*text))
2233            .collect::<Vec<_>>();
2234        let view = cx.add_window({
2235            let texts = texts.clone();
2236            move |_, _| PlainRunLayoutView {
2237                texts,
2238                layouts: Vec::new(),
2239            }
2240        });
2241        cx.update_window(*view, |_, window, cx| {
2242            let _ = window.draw(cx);
2243        })
2244        .unwrap();
2245        let layouts = cx.update(|cx| view.read(cx).unwrap().layouts.clone());
2246        texts.into_iter().zip(layouts).collect()
2247    }
2248
2249    fn plain_snapshot(anchor: Point<Pixels>, cursor: Point<Pixels>) -> TextSelectionSnapshot {
2250        TextSelectionSnapshot::new(
2251            TextSelectionEndpoint::new(None, anchor),
2252            TextSelectionEndpoint::new(None, cursor),
2253        )
2254        .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
2255    }
2256
2257    #[gpui::test]
2258    fn scope_stack_is_cleaned_after_panicking_subtree(cx: &mut TestAppContext) {
2259        let window_id = {
2260            let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2261            window_cx.update(|window, _| window.window_handle().window_id())
2262        };
2263        let scope = TextSelectionScopeId::from_raw(41);
2264
2265        cx.update(|cx| {
2266            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2267                with_text_selection_scope(window_id, scope, cx, |_| panic!("subtree failed"));
2268            }));
2269
2270            assert!(result.is_err());
2271            assert_eq!(current_text_selection_scope(window_id, cx), None);
2272        });
2273    }
2274
2275    #[gpui::test]
2276    fn reentrant_scope_from_one_window_does_not_pollute_another(cx: &mut TestAppContext) {
2277        let first_window_id = {
2278            let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2279            window_cx.update(|window, _| window.window_handle().window_id())
2280        };
2281        let second_window_id = {
2282            let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2283            window_cx.update(|window, _| window.window_handle().window_id())
2284        };
2285        let scope = TextSelectionScopeId::from_raw(42);
2286
2287        cx.update(|cx| {
2288            with_text_selection_scope(first_window_id, scope, cx, |cx| {
2289                assert_eq!(current_text_selection_scope(second_window_id, cx), None);
2290                assert_eq!(
2291                    current_text_selection_scope(first_window_id, cx),
2292                    Some(scope)
2293                );
2294            });
2295        });
2296    }
2297
2298    #[gpui::test]
2299    fn selection_callback_can_reenter_its_selection_state(cx: &mut TestAppContext) {
2300        let called = Rc::new(Cell::new(false));
2301        let called_from_callback = called.clone();
2302        let (selection_state, participant) = cx.update(|cx| {
2303            let selection_state = cx.new(|_| WindowSelectionState::default());
2304            let selection_state_for_callback = selection_state.clone();
2305            let participant = FakeParticipant::new("participant", cx);
2306            participant
2307                .selection
2308                .subscribe(
2309                    move |event, cx| {
2310                        if matches!(event, TextSelectionEvent::SelectionChanged(Some(_))) {
2311                            selection_state_for_callback
2312                                .update(cx, |_, _| called_from_callback.set(true));
2313                        }
2314                    },
2315                    cx,
2316                )
2317                .detach();
2318            (selection_state, participant)
2319        });
2320        cx.run_until_parked();
2321        cx.update(|cx| {
2322            selection_state.update(cx, |selection_state, cx| {
2323                participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx);
2324                selection_state.begin(point(px(1.), px(1.)), false, cx);
2325                selection_state.update(point(px(20.), px(1.)), cx);
2326            });
2327        });
2328        cx.run_until_parked();
2329        assert!(called.get());
2330    }
2331
2332    #[gpui::test]
2333    fn selection_events_preserve_snapshot_then_clear_order(cx: &mut TestAppContext) {
2334        let observed = Rc::new(RefCell::new(Vec::new()));
2335        let observed_for_callback = observed.clone();
2336        let selection = cx.update(|cx| {
2337            let selection = TextSelectionHandle::new("selection", cx);
2338            selection
2339                .subscribe(
2340                    move |event, _| {
2341                        if let TextSelectionEvent::SelectionChanged(snapshot) = event {
2342                            observed_for_callback.borrow_mut().push(snapshot.is_some());
2343                        }
2344                    },
2345                    cx,
2346                )
2347                .detach();
2348            selection
2349        });
2350        cx.run_until_parked();
2351        cx.update(|cx| {
2352            selection.0.update(cx, |state, cx| {
2353                state.set_snapshot(
2354                    Some(plain_snapshot(point(px(1.), px(1.)), point(px(8.), px(1.)))),
2355                    cx,
2356                );
2357                state.clear_state(cx);
2358            });
2359        });
2360        cx.run_until_parked();
2361        assert_eq!(&*observed.borrow(), &[true, false]);
2362    }
2363
2364    fn text_run(order: u64, text: SharedString, layout: TextLayout) -> TextSelectionRun {
2365        let bounds = layout.bounds();
2366        TextSelectionRun::new(text, layout, bounds).with_document_order(order)
2367    }
2368
2369    #[gpui::test]
2370    fn public_selection_data_uses_builders_and_readers(cx: &mut TestAppContext) {
2371        let bounds = Bounds::new(point(px(1.), px(2.)), size(px(30.), px(10.)));
2372        let hitbox = Hitbox {
2373            id: HitboxId::placeholder(),
2374            bounds,
2375            content_mask: ContentMask { bounds },
2376            behavior: HitboxBehavior::Normal,
2377        };
2378        let scope = TextSelectionScopeId::from_raw(7);
2379        let endpoint = TextSelectionEndpoint::new(None, bounds.origin)
2380            .with_content_key(TextSelectionContentKey::new(11));
2381        let snapshot = TextSelectionSnapshot::new(endpoint, endpoint)
2382            .with_selecting(true)
2383            .with_window_points(Some(TextSelectionWindowPoints {
2384                anchor: bounds.origin,
2385                cursor: bounds.bottom_right(),
2386            }))
2387            .with_coverage(TextSelectionCoverage::Full);
2388        let registration = TextSelectionRegistration::new(hitbox, bounds)
2389            .with_scroll_offset(point(px(3.), px(4.)))
2390            .with_scope(scope)
2391            .with_document_order(9)
2392            .with_text_bounds(vec![bounds]);
2393
2394        assert_eq!(endpoint.entity_id(), None);
2395        assert_eq!(endpoint.content_point(), bounds.origin);
2396        assert_eq!(
2397            endpoint.content_key(),
2398            Some(TextSelectionContentKey::new(11))
2399        );
2400        assert_eq!(snapshot.anchor(), endpoint);
2401        assert_eq!(snapshot.cursor(), endpoint);
2402        assert!(snapshot.is_selecting());
2403        assert_eq!(snapshot.coverage(), TextSelectionCoverage::Full);
2404        assert_eq!(
2405            snapshot.window_points(),
2406            Some(TextSelectionWindowPoints {
2407                anchor: bounds.origin,
2408                cursor: bounds.bottom_right(),
2409            })
2410        );
2411        assert_eq!(registration.bounds(), bounds);
2412        assert_eq!(registration.scroll_offset(), point(px(3.), px(4.)));
2413        assert_eq!(registration.scope(), scope);
2414        assert_eq!(registration.document_order(), 9);
2415        assert_eq!(registration.text_bounds(), &[bounds]);
2416
2417        let (text, layout) = laid_out_runs(&["aé"], cx).pop().unwrap();
2418        let text_run = TextSelectionRun::new(text.clone(), layout.clone(), layout.bounds())
2419            .with_document_order(3);
2420        assert_eq!(text_run.document_order(), 3);
2421        assert_eq!(text_run.text(), &text);
2422        assert_eq!(text_run.layout().len(), layout.len());
2423        assert_eq!(text_run.bounds(), layout.bounds());
2424
2425        let projection = TextSelectionProjection {
2426            ranges: vec![Some(1..3)],
2427            is_active: true,
2428        };
2429        assert_eq!(projection.ranges(), &[Some(1..3)]);
2430        assert!(projection.is_active());
2431    }
2432
2433    #[gpui::test]
2434    fn selection_handle_is_the_public_adapter_seam(cx: &mut TestAppContext) {
2435        let selected = Rc::new(Cell::new(false));
2436        let selected_from_callback = selected.clone();
2437        cx.update(|cx| {
2438            let selection = TextSelectionHandle::new("initial", cx);
2439            let entity_id = selection.entity_id();
2440            selection.set_fallback_copy_text("updated", cx);
2441            selection.set_local_selection(true, cx);
2442            selection
2443                .subscribe(
2444                    move |event, _| {
2445                        if let TextSelectionEvent::SelectionChanged(snapshot) = event {
2446                            selected_from_callback.set(snapshot.is_some());
2447                        }
2448                    },
2449                    cx,
2450                )
2451                .detach();
2452            selection.focus_with(|_, _| {}, cx);
2453            selection.copy_with(|_| "copied".to_string(), cx);
2454            selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(3)), cx);
2455
2456            assert_eq!(selection.entity_id(), entity_id);
2457            assert_eq!(selection.snapshot(cx), None);
2458            assert_eq!(
2459                selection.update_runs(&[], cx),
2460                TextSelectionProjection::default()
2461            );
2462        });
2463        assert!(!selected.get());
2464    }
2465
2466    #[gpui::test]
2467    fn selection_handle_can_subscribe_its_window_to_refresh(cx: &mut TestAppContext) {
2468        let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
2469            selection: TextSelectionHandle::new("refresh", cx),
2470        });
2471        cx.update(|window, cx| {
2472            let selection = TextSelectionHandle::new("refresh", cx);
2473            selection.refresh_window_on_change(window, cx).detach();
2474        });
2475    }
2476
2477    #[gpui::test]
2478    fn plain_projection_preserves_forward_reversed_and_unicode_ranges(cx: &mut TestAppContext) {
2479        let (text, layout) = laid_out_runs(&["aé🙂z"], cx).pop().unwrap();
2480        let run = text_run(0, text, layout.clone());
2481        let start = layout.position_for_index(1).unwrap();
2482        let end = layout.position_for_index(7).unwrap();
2483
2484        let forward = project_ranges(Some(plain_snapshot(start, end)), std::slice::from_ref(&run));
2485        let reversed = project_ranges(Some(plain_snapshot(end, start)), &[run]);
2486
2487        assert_eq!(forward.ranges(), &[Some(1..7)]);
2488        assert_eq!(reversed.ranges(), &[Some(1..7)]);
2489        assert!(forward.is_active());
2490        assert!(reversed.is_active());
2491    }
2492
2493    #[gpui::test]
2494    fn double_click_expands_a_plain_run_to_the_input_word_boundary(cx: &mut TestAppContext) {
2495        let (text, layout) = laid_out_runs(&["one café, three"], cx).pop().unwrap();
2496        let run = text_run(0, text, layout.clone());
2497        let click = layout.position_for_index(6).unwrap();
2498
2499        let (anchor, cursor) =
2500            points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
2501        let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
2502
2503        assert_eq!(states.ranges(), &[Some(4..9)]);
2504    }
2505
2506    #[gpui::test]
2507    fn multi_click_uses_text_layout_window_coordinates_at_a_nonzero_origin(
2508        cx: &mut TestAppContext,
2509    ) {
2510        let mut runs = laid_out_runs(&["above", "alpha beta"], cx);
2511        let (text, layout) = runs.pop().unwrap();
2512        assert!(layout.bounds().origin.y > px(0.));
2513        let run = text_run(0, text, layout.clone());
2514        let click = layout.position_for_index(7).unwrap();
2515
2516        let (anchor, cursor) =
2517            points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
2518        let projection = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
2519
2520        assert_eq!(projection.ranges(), &[Some(6..10)]);
2521    }
2522
2523    #[gpui::test]
2524    fn triple_click_expands_to_the_input_logical_line_not_the_visual_row(cx: &mut TestAppContext) {
2525        let (text, layout) = laid_out_runs(&["second line"], cx).pop().unwrap();
2526        let run = text_run(0, text, layout.clone());
2527        let click = layout.position_for_index(4).unwrap();
2528
2529        let (anchor, cursor) =
2530            points_for_multi_click(std::slice::from_ref(&run), click, 4).unwrap();
2531        let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
2532
2533        assert_eq!(states.ranges(), &[Some(0..11)]);
2534        assert_eq!(line_range_at("first line\nsecond line\nthird", 15), 11..22);
2535    }
2536
2537    #[gpui::test]
2538    fn plain_projection_spans_multiple_runs_and_leaves_empty_gutters_unselected(
2539        cx: &mut TestAppContext,
2540    ) {
2541        let mut runs = laid_out_runs(&["first", "", "second"], cx);
2542        let (first_text, first_layout) = runs.remove(0);
2543        let (gutter_text, gutter_layout) = runs.remove(0);
2544        let (second_text, second_layout) = runs.remove(0);
2545        let start = first_layout.position_for_index(2).unwrap();
2546        let end = second_layout.position_for_index(3).unwrap();
2547        let states = project_ranges(
2548            Some(plain_snapshot(start, end)),
2549            &[
2550                text_run(2, second_text, second_layout),
2551                text_run(1, gutter_text, gutter_layout),
2552                text_run(0, first_text, first_layout),
2553            ],
2554        );
2555
2556        assert_eq!(states.ranges(), &[Some(0..3), None, Some(2..5)]);
2557        assert!(states.is_active());
2558    }
2559
2560    #[gpui::test]
2561    fn plain_projection_caches_multiple_participant_copies_in_document_order(
2562        cx: &mut TestAppContext,
2563    ) {
2564        let mut runs = laid_out_runs(&["one", "two"], cx);
2565        let (first_text, first_layout) = runs.remove(0);
2566        let (second_text, second_layout) = runs.remove(0);
2567        let snapshot = plain_snapshot(
2568            first_layout.position_for_index(1).unwrap(),
2569            second_layout.position_for_index(2).unwrap(),
2570        );
2571        cx.update(|cx| {
2572            let mut selection_state = WindowSelectionState::default();
2573            let first = FakeParticipant::new("", cx);
2574            let second = FakeParticipant::new("", cx);
2575            first.register(
2576                &mut selection_state,
2577                0.,
2578                TextSelectionScopeId::default(),
2579                1,
2580                cx,
2581            );
2582            second.register(
2583                &mut selection_state,
2584                20.,
2585                TextSelectionScopeId::default(),
2586                0,
2587                cx,
2588            );
2589
2590            first
2591                .selection
2592                .0
2593                .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
2594            let projection = first
2595                .selection
2596                .update_runs(&[text_run(0, first_text, first_layout)], cx);
2597            assert_eq!(projection.ranges(), &[Some(1..3)]);
2598            assert!(projection.is_active());
2599            second
2600                .selection
2601                .0
2602                .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
2603            let projection = second
2604                .selection
2605                .update_runs(&[text_run(0, second_text, second_layout)], cx);
2606            assert_eq!(projection.ranges(), &[Some(0..2)]);
2607            assert!(projection.is_active());
2608
2609            assert_eq!(selection_state.selected_text(cx), "tw\nne");
2610        });
2611    }
2612
2613    #[gpui::test]
2614    fn plain_projection_invalidates_cached_copy_when_the_snapshot_changes(cx: &mut TestAppContext) {
2615        let (text, layout) = laid_out_runs(&["first"], cx).pop().unwrap();
2616        let first_snapshot = plain_snapshot(
2617            layout.position_for_index(1).unwrap(),
2618            layout.position_for_index(3).unwrap(),
2619        );
2620        let changed_snapshot = plain_snapshot(
2621            layout.position_for_index(3).unwrap(),
2622            layout.position_for_index(5).unwrap(),
2623        );
2624        let run = text_run(0, text, layout);
2625        cx.update(|cx| {
2626            let mut selection_state = WindowSelectionState::default();
2627            let participant = FakeParticipant::new("", cx);
2628            participant.register(
2629                &mut selection_state,
2630                0.,
2631                TextSelectionScopeId::default(),
2632                0,
2633                cx,
2634            );
2635            participant.selection.0.update(cx, |state, cx| {
2636                state.set_snapshot(Some(first_snapshot), cx);
2637                state.update_runs(std::slice::from_ref(&run));
2638            });
2639            assert_eq!(selection_state.selected_text(cx), "ir");
2640
2641            participant.selection.0.update(cx, |state, cx| {
2642                state.set_snapshot(Some(changed_snapshot), cx);
2643            });
2644            assert_eq!(selection_state.selected_text(cx), "");
2645
2646            participant.selection.update_runs(&[run], cx);
2647            assert_eq!(selection_state.selected_text(cx), "st");
2648            selection_state.clear(cx);
2649            participant.selection.set_local_selection(true, cx);
2650            assert_eq!(selection_state.selected_text(cx), "");
2651        });
2652    }
2653
2654    #[gpui::test]
2655    fn plain_projection_orders_cached_runs_by_frame_order_not_input_order(cx: &mut TestAppContext) {
2656        let mut runs = laid_out_runs(&["one", "two"], cx);
2657        let (first_text, first_layout) = runs.remove(0);
2658        let (second_text, second_layout) = runs.remove(0);
2659        let snapshot = plain_snapshot(
2660            first_layout.position_for_index(1).unwrap(),
2661            second_layout.position_for_index(2).unwrap(),
2662        );
2663        cx.update(|cx| {
2664            let mut selection_state = WindowSelectionState::default();
2665            let participant = FakeParticipant::new("", cx);
2666            participant.register(
2667                &mut selection_state,
2668                0.,
2669                TextSelectionScopeId::default(),
2670                0,
2671                cx,
2672            );
2673            participant.selection.0.update(cx, |state, cx| {
2674                state.set_snapshot(Some(snapshot), cx);
2675                state.update_runs(&[
2676                    text_run(1, first_text, first_layout),
2677                    text_run(0, second_text, second_layout),
2678                ]);
2679            });
2680
2681            assert_eq!(selection_state.selected_text(cx), "twne");
2682        });
2683    }
2684
2685    #[gpui::test]
2686    fn plain_projection_safely_rejects_a_text_layout_length_mismatch(cx: &mut TestAppContext) {
2687        let (_, layout) = laid_out_runs(&["short"], cx).pop().unwrap();
2688        let start = layout.position_for_index(0).unwrap();
2689        let end = layout.position_for_index(5).unwrap();
2690        let states = project_ranges(
2691            Some(plain_snapshot(start, end)),
2692            &[text_run(0, SharedString::from("longer"), layout)],
2693        );
2694
2695        assert_eq!(states.ranges(), &[None]);
2696        assert!(states.is_active());
2697    }
2698
2699    #[gpui::test]
2700    fn begin_update_and_end_publish_a_cross_participant_selection(cx: &mut TestAppContext) {
2701        cx.update(|cx| {
2702            let mut selection_state = WindowSelectionState::default();
2703            let first = FakeParticipant::new("first", cx);
2704            let second = FakeParticipant::new("second", cx);
2705            first.register(
2706                &mut selection_state,
2707                0.,
2708                TextSelectionScopeId::default(),
2709                0,
2710                cx,
2711            );
2712            second.register(
2713                &mut selection_state,
2714                20.,
2715                TextSelectionScopeId::default(),
2716                1,
2717                cx,
2718            );
2719
2720            selection_state.begin(point(px(1.), px(1.)), false, cx);
2721            selection_state.update(point(px(1.), px(25.)), cx);
2722            assert!(selection_state.has_selection(cx));
2723            assert_eq!(selection_state.selected_text(cx), "first\nsecond");
2724
2725            selection_state.end(cx);
2726            assert!(!selection_state.is_selecting());
2727        });
2728    }
2729
2730    #[gpui::test]
2731    fn shift_extension_keeps_its_original_anchor_when_reversed(cx: &mut TestAppContext) {
2732        cx.update(|cx| {
2733            let mut selection_state = WindowSelectionState::default();
2734            let participant = FakeParticipant::new("participant", cx);
2735            participant.register(
2736                &mut selection_state,
2737                0.,
2738                TextSelectionScopeId::default(),
2739                0,
2740                cx,
2741            );
2742
2743            selection_state.begin(point(px(2.), px(2.)), false, cx);
2744            selection_state.end(cx);
2745            selection_state.begin(point(px(8.), px(2.)), true, cx);
2746            selection_state.end(cx);
2747            let first_anchor = selection_state.snapshot().unwrap().anchor();
2748
2749            selection_state.begin(point(px(0.), px(2.)), true, cx);
2750            selection_state.end(cx);
2751            let reversed = selection_state.snapshot().unwrap();
2752            assert_eq!(reversed.anchor(), first_anchor);
2753            assert!(reversed.cursor().content_point().x < reversed.anchor().content_point().x);
2754        });
2755    }
2756
2757    #[gpui::test]
2758    fn content_key_resolver_runs_outside_the_window_state_lease(cx: &mut TestAppContext) {
2759        cx.update(|cx| {
2760            let state = cx.new(|_| WindowSelectionState::default());
2761            let participant = FakeParticipant::new("virtual", cx);
2762            let state_for_callback = state.clone();
2763            participant.selection.resolve_content_key_with(
2764                move |_, cx| {
2765                    let _ = state_for_callback.read(cx).snapshot();
2766                    Some(TextSelectionContentKey::new(7))
2767                },
2768                cx,
2769            );
2770            state.update(cx, |state, cx| {
2771                participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
2772                state.begin(point(px(1.), px(1.)), false, cx);
2773                state.update(point(px(8.), px(1.)), cx);
2774            });
2775
2776            WindowSelectionState::resolve_content_keys(&state, cx);
2777
2778            assert_eq!(
2779                state.read(cx).snapshot().unwrap().cursor().content_key(),
2780                Some(TextSelectionContentKey::new(7))
2781            );
2782        });
2783    }
2784
2785    #[gpui::test]
2786    fn active_dnd_does_not_move_a_text_selection_cursor(cx: &mut TestAppContext) {
2787        let window = cx.add_window(|_, cx| WindowSelectionView {
2788            selection: TextSelectionHandle::new("unused", cx),
2789        });
2790        window
2791            .update(cx, |_, window, cx| {
2792                let mut state = WindowSelectionState::default();
2793                let participant = FakeParticipant::new("participant", cx);
2794                participant.register(&mut state, 0., TextSelectionScopeId::default(), 0, cx);
2795                state.begin(point(px(1.), px(1.)), false, cx);
2796                let before = state.cursor.as_ref().unwrap().point;
2797                state.update_in_window_with_active_drag(point(px(80.), px(1.)), true, window, cx);
2798                assert_eq!(state.cursor.as_ref().unwrap().point, before);
2799            })
2800            .unwrap();
2801    }
2802
2803    #[gpui::test]
2804    fn shift_extension_falls_back_when_the_anchor_participant_was_swept(cx: &mut TestAppContext) {
2805        cx.update(|cx| {
2806            let mut selection_state = WindowSelectionState::default();
2807            let first = FakeParticipant::new("first", cx);
2808            let second = FakeParticipant::new("second", cx);
2809            first.register(
2810                &mut selection_state,
2811                0.,
2812                TextSelectionScopeId::default(),
2813                0,
2814                cx,
2815            );
2816            selection_state.begin(point(px(1.), px(1.)), false, cx);
2817            selection_state.update(point(px(8.), px(1.)), cx);
2818            selection_state.end(cx);
2819
2820            selection_state.finish_frame(cx);
2821            selection_state.finish_frame(cx);
2822            second.register(
2823                &mut selection_state,
2824                20.,
2825                TextSelectionScopeId::default(),
2826                1,
2827                cx,
2828            );
2829            selection_state.begin(point(px(1.), px(21.)), true, cx);
2830            selection_state.update(point(px(8.), px(21.)), cx);
2831            selection_state.end(cx);
2832
2833            assert_eq!(selection_state.selected_text(cx), "second");
2834        });
2835    }
2836
2837    #[gpui::test]
2838    fn scope_and_suppression_prevent_unrelated_participants_from_participating(
2839        cx: &mut TestAppContext,
2840    ) {
2841        cx.update(|cx| {
2842            let mut selection_state = WindowSelectionState::default();
2843            let base = FakeParticipant::new("base", cx);
2844            let modal = FakeParticipant::new("modal", cx);
2845            base.register(
2846                &mut selection_state,
2847                0.,
2848                TextSelectionScopeId::default(),
2849                0,
2850                cx,
2851            );
2852            modal.register(&mut selection_state, 20., TextSelectionScopeId(1), 1, cx);
2853
2854            selection_state.set_active_scope(TextSelectionScopeId(1), cx);
2855            selection_state.begin(point(px(1.), px(21.)), false, cx);
2856            selection_state.update(point(px(8.), px(21.)), cx);
2857            selection_state.end(cx);
2858            assert_eq!(selection_state.selected_text(cx), "modal");
2859
2860            selection_state.clear(cx);
2861            GlobalState::init(cx);
2862            GlobalState::suppress_text_selection(cx);
2863            selection_state.begin(point(px(1.), px(21.)), false, cx);
2864            selection_state.update(point(px(8.), px(21.)), cx);
2865            assert!(!selection_state.has_selection(cx));
2866        });
2867    }
2868
2869    #[gpui::test]
2870    fn dead_participants_are_pruned_and_empty_selection_falls_back_safely(cx: &mut TestAppContext) {
2871        let selection_state = cx.update(|cx| {
2872            let selection_state = cx.new(|_| WindowSelectionState::default());
2873            let participant = FakeParticipant::new("gone", cx);
2874            selection_state.update(cx, |selection_state, cx| {
2875                participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx)
2876            });
2877            selection_state
2878        });
2879        cx.update(|cx| {
2880            selection_state.update(cx, |selection_state, cx| {
2881                selection_state.begin(point(px(1.), px(1.)), false, cx);
2882                selection_state.update(point(px(8.), px(1.)), cx);
2883                selection_state.end(cx);
2884
2885                assert_eq!(selection_state.selected_text(cx), "");
2886                assert!(!selection_state.has_selection(cx));
2887            });
2888        });
2889    }
2890
2891    #[gpui::test]
2892    fn text_selection_namespace_reports_copies_ends_and_clears_selection(cx: &mut TestAppContext) {
2893        let (view, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
2894            selection: TextSelectionHandle::new("copied", cx),
2895        });
2896        cx.update(|window, cx| {
2897            let selection = view.read(cx).selection.clone();
2898            let selection_state = WindowSelectionState::ensure(window, cx);
2899            selection_state.update(cx, |selection_state, cx| {
2900                FakeParticipant { selection }.register(
2901                    selection_state,
2902                    0.,
2903                    TextSelectionScopeId::default(),
2904                    0,
2905                    cx,
2906                );
2907                selection_state.begin(point(px(1.), px(1.)), false, cx);
2908                selection_state.update(point(px(8.), px(1.)), cx);
2909            });
2910
2911            assert!(TextSelection::has_selection(window, cx));
2912            assert_eq!(TextSelection::selected_text(window, cx), "copied");
2913            TextSelection::end(window, cx);
2914            assert!(TextSelection::has_selection(window, cx));
2915            TextSelection::clear(window, cx);
2916            assert!(!TextSelection::has_selection(window, cx));
2917            assert_eq!(TextSelection::selected_text(window, cx), "");
2918        });
2919    }
2920
2921    #[gpui::test]
2922    fn two_windows_isolate_selection_copy_clear_and_release_ownership(cx: &mut TestAppContext) {
2923        let first = cx.add_window(|_, cx| WindowOwnedSelectionView {
2924            selection: TextSelectionHandle::new("first", cx),
2925        });
2926        let second = cx.add_window(|_, cx| WindowOwnedSelectionView {
2927            selection: TextSelectionHandle::new("second", cx),
2928        });
2929        let first_selection = cx.update(|cx| first.read(cx).unwrap().selection.clone());
2930        let second_selection = cx.update(|cx| second.read(cx).unwrap().selection.clone());
2931
2932        let first_state = cx
2933            .update_window(*first, |_, window, cx| {
2934                let _ = window.draw(cx);
2935                first_selection.set_local_selection(true, cx);
2936                assert_eq!(TextSelection::selected_text(window, cx), "first");
2937                WindowSelectionState::existing(window, cx)
2938                    .unwrap()
2939                    .downgrade()
2940            })
2941            .unwrap();
2942        cx.update_window(*second, |_, window, cx| {
2943            let _ = window.draw(cx);
2944            second_selection.set_local_selection(true, cx);
2945            assert_eq!(TextSelection::selected_text(window, cx), "second");
2946        })
2947        .unwrap();
2948
2949        cx.update_window(*first, |_, window, cx| {
2950            TextSelection::clear(window, cx);
2951            assert_eq!(TextSelection::selected_text(window, cx), "");
2952        })
2953        .unwrap();
2954        cx.update_window(*second, |_, window, cx| {
2955            assert_eq!(TextSelection::selected_text(window, cx), "second");
2956        })
2957        .unwrap();
2958
2959        cx.update_window(*first, |_, window, _| window.remove_window())
2960            .unwrap();
2961        cx.run_until_parked();
2962
2963        assert!(first_state.upgrade().is_none());
2964        cx.update_window(*second, |_, window, cx| {
2965            assert_eq!(TextSelection::selected_text(window, cx), "second");
2966        })
2967        .unwrap();
2968        cx.update(|cx| {
2969            assert_eq!(cx.global::<SelectionStateRegistry>().0.len(), 1);
2970        });
2971    }
2972
2973    #[gpui::test]
2974    fn copy_callback_can_reenter_window_and_handle_selection(cx: &mut TestAppContext) {
2975        let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2976        cx.update(|window, cx| {
2977            let _ = window.draw(cx);
2978            let state = WindowSelectionState::existing(window, cx).unwrap();
2979            let selection = TextSelectionHandle::new("fallback", cx);
2980            let state_for_copy = state.clone();
2981            let selection_for_copy = selection.clone();
2982            selection.copy_with(
2983                move |cx: &mut App| {
2984                    state_for_copy.update(cx, |state, _| {
2985                        assert!(state.snapshot().is_some());
2986                    });
2987                    assert!(selection_for_copy.snapshot(cx).is_some());
2988                    selection_for_copy.set_fallback_copy_text("reentered", cx);
2989                    "reentrant copy".to_string()
2990                },
2991                cx,
2992            );
2993            state.update(cx, |state, cx| {
2994                FakeParticipant {
2995                    selection: selection.clone(),
2996                }
2997                .register(state, 0., TextSelectionScopeId::default(), 0, cx);
2998                state.begin(point(px(1.), px(1.)), false, cx);
2999                state.update(point(px(8.), px(1.)), cx);
3000                state.end(cx);
3001            });
3002
3003            assert_eq!(TextSelection::selected_text(window, cx), "reentrant copy");
3004        });
3005    }
3006
3007    #[gpui::test]
3008    fn cross_participant_selection_excludes_participants_outside_its_document_interval(
3009        cx: &mut TestAppContext,
3010    ) {
3011        cx.update(|cx| {
3012            let mut selection_state = WindowSelectionState::default();
3013            let first = FakeParticipant::new("first", cx);
3014            let second = FakeParticipant::new("second", cx);
3015            let third = FakeParticipant::new("third", cx);
3016            first.register(
3017                &mut selection_state,
3018                0.,
3019                TextSelectionScopeId::default(),
3020                0,
3021                cx,
3022            );
3023            second.register(
3024                &mut selection_state,
3025                20.,
3026                TextSelectionScopeId::default(),
3027                1,
3028                cx,
3029            );
3030            third.register(
3031                &mut selection_state,
3032                40.,
3033                TextSelectionScopeId::default(),
3034                2,
3035                cx,
3036            );
3037
3038            selection_state.begin(point(px(1.), px(1.)), false, cx);
3039            selection_state.update(point(px(1.), px(25.)), cx);
3040            selection_state.end(cx);
3041
3042            assert_eq!(selection_state.selected_text(cx), "first\nsecond");
3043            assert!(third.selection.snapshot(cx).is_none());
3044        });
3045    }
3046
3047    #[gpui::test]
3048    fn changing_scope_clears_the_previous_scope_selection(cx: &mut TestAppContext) {
3049        cx.update(|cx| {
3050            let mut selection_state = WindowSelectionState::default();
3051            let base = FakeParticipant::new("base", cx);
3052            let modal = FakeParticipant::new("modal", cx);
3053            base.register(
3054                &mut selection_state,
3055                0.,
3056                TextSelectionScopeId::default(),
3057                0,
3058                cx,
3059            );
3060            modal.register(
3061                &mut selection_state,
3062                20.,
3063                TextSelectionScopeId::from_raw(1),
3064                1,
3065                cx,
3066            );
3067
3068            selection_state.begin(point(px(1.), px(1.)), false, cx);
3069            selection_state.update(point(px(8.), px(1.)), cx);
3070            selection_state.end(cx);
3071            selection_state.set_active_scope(TextSelectionScopeId::from_raw(1), cx);
3072
3073            assert!(!selection_state.has_selection(cx));
3074            assert!(base.selection.snapshot(cx).is_none());
3075        });
3076    }
3077
3078    #[gpui::test]
3079    fn blank_only_drag_never_publishes_or_copies_selection(cx: &mut TestAppContext) {
3080        cx.update(|cx| {
3081            let mut selection_state = WindowSelectionState::default();
3082            let participant = FakeParticipant::new("participant", cx);
3083            participant.register(
3084                &mut selection_state,
3085                0.,
3086                TextSelectionScopeId::default(),
3087                0,
3088                cx,
3089            );
3090
3091            selection_state.begin(point(px(200.), px(1.)), false, cx);
3092            selection_state.update(point(px(200.), px(8.)), cx);
3093            selection_state.end(cx);
3094
3095            assert!(!selection_state.has_selection(cx));
3096            assert_eq!(selection_state.selected_text(cx), "");
3097            assert!(participant.selection.snapshot(cx).is_none());
3098        });
3099    }
3100
3101    #[gpui::test]
3102    fn stale_live_participants_are_removed_when_the_next_frame_begins(cx: &mut TestAppContext) {
3103        cx.update(|cx| {
3104            let mut selection_state = WindowSelectionState::default();
3105            let participant = FakeParticipant::new("stale", cx);
3106            participant.register(
3107                &mut selection_state,
3108                0.,
3109                TextSelectionScopeId::default(),
3110                0,
3111                cx,
3112            );
3113            selection_state.begin(point(px(1.), px(1.)), false, cx);
3114            selection_state.update(point(px(8.), px(1.)), cx);
3115            selection_state.end(cx);
3116
3117            selection_state.finish_frame(cx);
3118            selection_state.finish_frame(cx);
3119            assert_eq!(selection_state.selected_text(cx), "");
3120            assert!(participant.selection.snapshot(cx).is_none());
3121        });
3122    }
3123
3124    #[gpui::test]
3125    fn clear_stops_anchor_auto_scroll_before_discarding_the_anchor(cx: &mut TestAppContext) {
3126        let commands = Rc::new(RefCell::new(Vec::new()));
3127        let observed = commands.clone();
3128        let (mut selection_state, participant) = cx.update(|cx| {
3129            let selection_state = WindowSelectionState::default();
3130            let participant = FakeParticipant::new("scroll", cx);
3131            participant
3132                .selection
3133                .subscribe(
3134                    move |event, _| {
3135                        if let TextSelectionEvent::AutoScroll(delta) = event {
3136                            observed.borrow_mut().push(*delta);
3137                        }
3138                    },
3139                    cx,
3140                )
3141                .detach();
3142            (selection_state, participant)
3143        });
3144        cx.run_until_parked();
3145        cx.update(|cx| {
3146            participant.register(
3147                &mut selection_state,
3148                0.,
3149                TextSelectionScopeId::default(),
3150                0,
3151                cx,
3152            );
3153
3154            selection_state.begin(point(px(1.), px(1.)), false, cx);
3155            selection_state.update(point(px(1.), px(25.)), cx);
3156            selection_state.clear(cx);
3157        });
3158        cx.run_until_parked();
3159        assert!(commands.borrow().iter().any(Option::is_some));
3160        assert_eq!(commands.borrow().last(), Some(&None));
3161    }
3162
3163    #[gpui::test]
3164    fn drag_auto_scroll_stops_when_the_content_mask_collapses(cx: &mut TestAppContext) {
3165        let window = cx.add_window(|_, cx| WindowSelectionView {
3166            selection: TextSelectionHandle::new("unused", cx),
3167        });
3168        window
3169            .update(cx, |_, window, cx| {
3170                let state = cx.new(|_| WindowSelectionState::default());
3171                let participant = FakeParticipant::new("participant", cx);
3172                state.update(cx, |state, cx| {
3173                    participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
3174                    state.begin(point(px(1.), px(1.)), false, cx);
3175                });
3176                // The scrollable ancestor got clipped away mid-drag, so the
3177                // refreshed registration carries a collapsed content mask.
3178                let collapsed = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(0.)));
3179                state.update(cx, |state, cx| {
3180                    state.register_participant(
3181                        participant.selection.clone(),
3182                        TextSelectionRegistration::new(
3183                            Hitbox {
3184                                id: HitboxId::placeholder(),
3185                                bounds: collapsed,
3186                                content_mask: ContentMask { bounds: collapsed },
3187                                behavior: HitboxBehavior::Normal,
3188                            },
3189                            collapsed,
3190                        )
3191                        .with_text_bounds(vec![collapsed]),
3192                        cx,
3193                    );
3194                    state.update_in_window(point(px(1.), px(50.)), window, cx);
3195                    assert!(!state.auto_scroll.is_active());
3196                    assert!(state.auto_scroll.last_drag_position.is_none());
3197                });
3198            })
3199            .unwrap();
3200    }
3201
3202    #[gpui::test]
3203    fn pointer_moves_after_a_click_do_not_auto_scroll(cx: &mut TestAppContext) {
3204        let window = cx.add_window(|_, cx| WindowSelectionView {
3205            selection: TextSelectionHandle::new("unused", cx),
3206        });
3207        window
3208            .update(cx, |_, window, cx| {
3209                let state = cx.new(|_| WindowSelectionState::default());
3210                let participant = FakeParticipant::new("participant", cx);
3211                state.update(cx, |state, cx| {
3212                    participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
3213                    // A click on text keeps its anchor so shift-click can extend it.
3214                    state.begin(point(px(1.), px(1.)), false, cx);
3215                    state.end(cx);
3216                    assert!(state.anchor.is_some());
3217
3218                    state.update_in_window(point(px(1.), px(50.)), window, cx);
3219                    assert!(!state.auto_scroll.is_active());
3220                });
3221            })
3222            .unwrap();
3223    }
3224
3225    #[gpui::test]
3226    fn proxy_endpoints_break_equal_position_ties_by_document_order(cx: &mut TestAppContext) {
3227        cx.update(|cx| {
3228            let mut selection_state = WindowSelectionState::default();
3229            let later = FakeParticipant::new("later", cx);
3230            let earlier = FakeParticipant::new("earlier", cx);
3231            later.register(
3232                &mut selection_state,
3233                0.,
3234                TextSelectionScopeId::default(),
3235                2,
3236                cx,
3237            );
3238            earlier.register(
3239                &mut selection_state,
3240                0.,
3241                TextSelectionScopeId::default(),
3242                1,
3243                cx,
3244            );
3245
3246            selection_state.begin(point(px(1.), px(1.)), false, cx);
3247            selection_state.update(point(px(200.), px(25.)), cx);
3248            let endpoint = selection_state.snapshot().unwrap().cursor();
3249
3250            assert_eq!(endpoint.entity_id(), Some(earlier.selection.entity_id()));
3251        });
3252    }
3253
3254    #[gpui::test]
3255    fn equal_area_hovered_participants_break_ties_by_document_order(cx: &mut TestAppContext) {
3256        cx.update(|cx| {
3257            for _ in 0..64 {
3258                let mut selection_state = WindowSelectionState::default();
3259                let later = FakeParticipant::new("later", cx);
3260                let earliest = FakeParticipant::new("earliest", cx);
3261                let middle = FakeParticipant::new("middle", cx);
3262                later.register(
3263                    &mut selection_state,
3264                    0.,
3265                    TextSelectionScopeId::default(),
3266                    30,
3267                    cx,
3268                );
3269                earliest.register(
3270                    &mut selection_state,
3271                    0.,
3272                    TextSelectionScopeId::default(),
3273                    10,
3274                    cx,
3275                );
3276                middle.register(
3277                    &mut selection_state,
3278                    0.,
3279                    TextSelectionScopeId::default(),
3280                    20,
3281                    cx,
3282                );
3283
3284                selection_state.begin(point(px(1.), px(1.)), false, cx);
3285                selection_state.update(point(px(8.), px(1.)), cx);
3286
3287                assert_eq!(
3288                    selection_state.snapshot().unwrap().anchor().entity_id(),
3289                    Some(earliest.selection.entity_id())
3290                );
3291            }
3292        });
3293    }
3294
3295    #[gpui::test]
3296    fn text_selection_namespace_is_a_safe_no_op_until_the_element_is_rendered(
3297        cx: &mut TestAppContext,
3298    ) {
3299        let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3300            selection: TextSelectionHandle::new("not enabled", cx),
3301        });
3302        cx.update(|window, cx| {
3303            assert!(!TextSelection::has_selection(window, cx));
3304            assert_eq!(TextSelection::selected_text(window, cx), "");
3305            TextSelection::clear(window, cx);
3306            TextSelection::end(window, cx);
3307            assert!(!TextSelection::has_selection(window, cx));
3308        });
3309    }
3310
3311    #[gpui::test]
3312    fn unit_selection_element_supports_scope_and_registration_on_the_first_frame(
3313        cx: &mut TestAppContext,
3314    ) {
3315        let (view, cx) = cx.add_window_view(|_, cx| FirstFrameScopedSelectionView {
3316            selection: TextSelectionHandle::new("first frame", cx),
3317        });
3318        let selection = cx.update(|_, cx| view.read(cx).selection.clone());
3319
3320        cx.update(|window, cx| {
3321            let _ = window.draw(cx);
3322            let state = WindowSelectionState::existing(window, cx).unwrap();
3323            assert_eq!(
3324                state.read(cx).active_scope,
3325                TextSelectionScopeId::from_raw(23)
3326            );
3327            assert!(
3328                state
3329                    .read(cx)
3330                    .participants
3331                    .contains_key(&selection.entity_id())
3332            );
3333        });
3334    }
3335
3336    #[gpui::test]
3337    fn lazy_registration_does_not_enable_queries_without_the_element(cx: &mut TestAppContext) {
3338        let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3339            selection: TextSelectionHandle::new("registered", cx),
3340        });
3341        cx.update(|window, cx| {
3342            let selection = TextSelectionHandle::new("registered", cx);
3343            selection.set_local_selection(true, cx);
3344            let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(20.)));
3345            let hitbox = Hitbox {
3346                id: HitboxId::placeholder(),
3347                bounds,
3348                content_mask: ContentMask { bounds },
3349                behavior: HitboxBehavior::Normal,
3350            };
3351            selection.register(
3352                TextSelectionRegistration::new(hitbox, bounds).with_text_bounds(vec![bounds]),
3353                window,
3354                cx,
3355            );
3356            assert_eq!(TextSelection::selected_text(window, cx), "");
3357            assert!(!TextSelection::has_selection(window, cx));
3358            TextSelection::clear(window, cx);
3359            assert_eq!(TextSelection::selected_text(window, cx), "");
3360        });
3361    }
3362
3363    #[gpui::test]
3364    fn retained_selection_state_releases_and_does_not_resurrect_selection(cx: &mut TestAppContext) {
3365        let (view, cx) = cx.add_window_view(|_, cx| ToggleSelectionElementView {
3366            enabled: true,
3367            selection: TextSelectionHandle::new("local", cx),
3368        });
3369        let selection = cx.update(|_, cx| view.read(cx).selection.clone());
3370        cx.update(|window, cx| {
3371            let _ = window.draw(cx);
3372            selection.set_local_selection(true, cx);
3373            assert!(TextSelection::has_selection(window, cx));
3374
3375            window.simulate_next_frame(cx);
3376            assert!(TextSelection::has_selection(window, cx));
3377            let _ = window.draw(cx);
3378            assert!(TextSelection::has_selection(window, cx));
3379        });
3380        view.update(cx, |view, cx| {
3381            view.enabled = false;
3382            cx.notify();
3383        });
3384        cx.update(|window, cx| {
3385            let _ = window.draw(cx);
3386        });
3387        cx.update(|window, cx| {
3388            window.simulate_next_frame(cx);
3389        });
3390        cx.update(|window, cx| {
3391            window.simulate_next_frame(cx);
3392        });
3393        cx.run_until_parked();
3394        cx.update(|window, cx| {
3395            assert!(!TextSelection::has_selection(window, cx));
3396            assert_eq!(TextSelection::selected_text(window, cx), "");
3397            assert!(!selection.has_local_selection(cx));
3398            TextSelection::clear(window, cx);
3399        });
3400
3401        view.update(cx, |view, cx| {
3402            view.enabled = true;
3403            cx.notify();
3404        });
3405        cx.update(|window, cx| {
3406            let _ = window.draw(cx);
3407            assert!(!TextSelection::has_selection(window, cx));
3408            assert_eq!(TextSelection::selected_text(window, cx), "");
3409        });
3410    }
3411
3412    #[gpui::test]
3413    fn mounted_selection_element_does_not_keep_an_idle_frame_queue_alive(cx: &mut TestAppContext) {
3414        let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3415        cx.update(|window, cx| {
3416            let _ = window.draw(cx);
3417            assert_eq!(window.simulate_next_frame(cx), 0);
3418            assert_eq!(window.simulate_next_frame(cx), 0);
3419            assert!(live_text_selection_state(window, cx).is_some());
3420        });
3421    }
3422
3423    #[gpui::test]
3424    fn selection_element_initializes_suppression_and_respects_bubble_suppression(
3425        cx: &mut TestAppContext,
3426    ) {
3427        let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3428        cx.update(|window, cx| {
3429            let _ = window.draw(cx);
3430        });
3431        cx.simulate_mouse_down(
3432            point(px(1.), px(1.)),
3433            MouseButton::Left,
3434            gpui::Modifiers::default(),
3435        );
3436        cx.simulate_mouse_up(
3437            point(px(1.), px(1.)),
3438            MouseButton::Left,
3439            gpui::Modifiers::default(),
3440        );
3441        cx.update(|window, cx| {
3442            assert!(GlobalState::is_text_selection_suppressed(cx));
3443            assert!(!TextSelection::has_selection(window, cx));
3444        });
3445    }
3446
3447    #[gpui::test]
3448    fn frame_sweep_keeps_a_participant_registered_before_the_selection_element_paints(
3449        cx: &mut TestAppContext,
3450    ) {
3451        cx.update(|cx| {
3452            let mut selection_state = WindowSelectionState::default();
3453            let participant = FakeParticipant::new("painted first", cx);
3454            participant.register(
3455                &mut selection_state,
3456                0.,
3457                TextSelectionScopeId::default(),
3458                0,
3459                cx,
3460            );
3461            selection_state.begin(point(px(1.), px(1.)), false, cx);
3462            selection_state.update(point(px(8.), px(1.)), cx);
3463            selection_state.end(cx);
3464
3465            selection_state.finish_frame(cx);
3466
3467            assert_eq!(selection_state.selected_text(cx), "painted first");
3468            assert!(participant.selection.snapshot(cx).is_some());
3469        });
3470    }
3471
3472    #[gpui::test]
3473    fn two_selection_elements_schedule_only_one_post_frame_sweep(cx: &mut TestAppContext) {
3474        let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
3475            selection: TextSelectionHandle::new("once", cx),
3476        });
3477        cx.update(|window, cx| {
3478            let selection_state = WindowSelectionState::ensure(window, cx);
3479            let selection = view.read(cx).selection.clone();
3480            selection_state.update(cx, |selection_state, cx| {
3481                FakeParticipant { selection }.register(
3482                    selection_state,
3483                    0.,
3484                    TextSelectionScopeId::default(),
3485                    0,
3486                    cx,
3487                );
3488                selection_state.begin(point(px(1.), px(1.)), false, cx);
3489                selection_state.update(point(px(8.), px(1.)), cx);
3490                selection_state.end(cx);
3491            });
3492
3493            let _ = window.draw(cx);
3494            window.simulate_next_frame(cx);
3495
3496            let items = selection_state.read(cx).copy_items(cx);
3497            assert_eq!(resolve_copy_items(items, cx), "once");
3498        });
3499    }
3500
3501    #[gpui::test]
3502    fn duplicate_selection_elements_gate_real_pointer_gestures_and_reentrant_clear(
3503        cx: &mut TestAppContext,
3504    ) {
3505        let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
3506            selection: TextSelectionHandle::new("once", cx),
3507        });
3508        let clear_count = Rc::new(Cell::new(0));
3509        cx.update(|window, cx| {
3510            let state = WindowSelectionState::ensure(window, cx);
3511            let state_for_clear = state.clone();
3512            let count = clear_count.clone();
3513            let selection = view.read(cx).selection.clone();
3514            selection
3515                .subscribe(
3516                    move |event, cx| {
3517                        if matches!(event, TextSelectionEvent::Cleared) {
3518                            count.set(count.get() + 1);
3519                            let _ = state_for_clear.read(cx).snapshot();
3520                        }
3521                    },
3522                    cx,
3523                )
3524                .detach();
3525            let _ = window.draw(cx);
3526        });
3527
3528        cx.simulate_mouse_down(
3529            point(px(10.), px(10.)),
3530            MouseButton::Left,
3531            gpui::Modifiers::default(),
3532        );
3533        cx.simulate_mouse_up(
3534            point(px(10.), px(10.)),
3535            MouseButton::Left,
3536            gpui::Modifiers::default(),
3537        );
3538        cx.simulate_mouse_down(
3539            point(px(70.), px(10.)),
3540            MouseButton::Left,
3541            gpui::Modifiers {
3542                shift: true,
3543                ..Default::default()
3544            },
3545        );
3546        cx.simulate_mouse_up(
3547            point(px(70.), px(10.)),
3548            MouseButton::Left,
3549            gpui::Modifiers::default(),
3550        );
3551        cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
3552
3553        cx.simulate_mouse_down(
3554            point(px(15.), px(10.)),
3555            MouseButton::Left,
3556            gpui::Modifiers::default(),
3557        );
3558        cx.simulate_mouse_move(
3559            point(px(85.), px(10.)),
3560            Some(MouseButton::Left),
3561            gpui::Modifiers::default(),
3562        );
3563        cx.simulate_mouse_up(
3564            point(px(85.), px(10.)),
3565            MouseButton::Left,
3566            gpui::Modifiers::default(),
3567        );
3568        cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
3569        assert_eq!(clear_count.get(), 3);
3570    }
3571
3572    #[gpui::test]
3573    fn selection_layer_handles_real_double_and_triple_click_events(cx: &mut TestAppContext) {
3574        let (text, layout) = laid_out_runs(&["alpha beta"], cx).pop().unwrap();
3575        let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
3576            selection: TextSelectionHandle::new("", cx),
3577        });
3578        cx.update(|window, cx| {
3579            let _ = window.draw(cx);
3580            let selection = view.read(cx).selection.clone();
3581            selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(17)), cx);
3582            selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
3583        });
3584
3585        let position = layout.position_for_index(7).unwrap();
3586        cx.simulate_event(MouseDownEvent {
3587            position,
3588            modifiers: gpui::Modifiers::default(),
3589            button: MouseButton::Left,
3590            click_count: 2,
3591            first_mouse: false,
3592        });
3593        cx.simulate_event(MouseUpEvent {
3594            position,
3595            modifiers: gpui::Modifiers::default(),
3596            button: MouseButton::Left,
3597            click_count: 2,
3598        });
3599        cx.update(|window, cx| {
3600            let selection = view.read(cx).selection.clone();
3601            selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
3602            assert_eq!(TextSelection::selected_text(window, cx), "beta");
3603            let snapshot = selection.snapshot(cx).unwrap();
3604            assert_eq!(
3605                snapshot.anchor().content_key(),
3606                Some(TextSelectionContentKey::new(17))
3607            );
3608            assert_eq!(
3609                snapshot.cursor().content_key(),
3610                Some(TextSelectionContentKey::new(17))
3611            );
3612        });
3613
3614        cx.simulate_event(MouseDownEvent {
3615            position,
3616            modifiers: gpui::Modifiers::default(),
3617            button: MouseButton::Left,
3618            click_count: 3,
3619            first_mouse: false,
3620        });
3621        cx.simulate_event(MouseUpEvent {
3622            position,
3623            modifiers: gpui::Modifiers::default(),
3624            button: MouseButton::Left,
3625            click_count: 3,
3626        });
3627        cx.update(|window, cx| {
3628            let selection = view.read(cx).selection.clone();
3629            selection.update_runs(&[text_run(0, text, layout)], cx);
3630            assert_eq!(TextSelection::selected_text(window, cx), "alpha beta");
3631        });
3632    }
3633}