Skip to main content

markdown/
selectable.rs

1//! Text you can select with the pointer and copy out of.
2//!
3//! The two hard halves are already here: [`render_with`] paints a [`Selection`]
4//! it is handed, and fills a [`BlockLayouts`] whose [`BlockLayouts::hit`] turns
5//! a point back into a [`Cursor`]. What was missing is the *gesture* — press,
6//! drag, release — which belonged to whoever owns the selection, and until now
7//! only an editor ever did.
8//!
9//! This is that gesture and nothing else. Which item holds the selection, and
10//! what copying means, stay the caller's.
11
12use crate::{BlockLayouts, Cursor, Doc, Editing, Selection, render_with};
13use gpui::{
14    AnyElement, Context, CursorStyle, ElementId, MouseButton, MouseDownEvent, MouseMoveEvent,
15    Window, div, prelude::*,
16};
17use std::rc::Rc;
18
19/// What the pointer did over the text.
20pub enum Pointer {
21    /// Pressed here — the start of a selection.
22    Down(Cursor),
23    /// Moved here with the button still down.
24    Move(Cursor),
25    /// Let go. Whatever the selection had become is what it is.
26    Up,
27}
28
29/// Render `doc` with `selection` painted in it, reporting what the pointer does.
30///
31/// `dragging` is the caller's: a move only extends a selection that a press
32/// started, and which item that press landed in is not something one block of
33/// text can know.
34///
35/// Releasing is answered twice over — on the text and off it — because a drag
36/// that ends past the edge of a paragraph is the ordinary way to select to the
37/// end of one.
38#[expect(
39    clippy::too_many_arguments,
40    reason = "a document, its selection, and a gesture"
41)]
42pub fn render<V: 'static>(
43    id: impl Into<ElementId>,
44    doc: &Doc,
45    layouts: &BlockLayouts,
46    selection: Option<Selection>,
47    dragging: bool,
48    window: &mut Window,
49    cx: &mut Context<V>,
50    on_pointer: impl Fn(&mut V, Pointer, &mut Context<V>) + 'static,
51) -> AnyElement {
52    let on_pointer = Rc::new(on_pointer);
53    let (down, moved, up, off) = (
54        on_pointer.clone(),
55        on_pointer.clone(),
56        on_pointer.clone(),
57        on_pointer,
58    );
59    let (at_down, at_move) = (layouts.clone(), layouts.clone());
60    div()
61        .id(id)
62        .cursor(CursorStyle::IBeam)
63        .on_mouse_down(
64            MouseButton::Left,
65            cx.listener(move |view, event: &MouseDownEvent, _, cx| {
66                if let Some(cursor) = at_down.hit(event.position) {
67                    down(view, Pointer::Down(cursor), cx);
68                }
69            }),
70        )
71        .on_mouse_move(cx.listener(move |view, event: &MouseMoveEvent, _, cx| {
72            if dragging && let Some(cursor) = at_move.hit(event.position) {
73                moved(view, Pointer::Move(cursor), cx);
74            }
75        }))
76        .on_mouse_up(
77            MouseButton::Left,
78            cx.listener(move |view, _, _, cx| up(view, Pointer::Up, cx)),
79        )
80        .on_mouse_up_out(
81            MouseButton::Left,
82            cx.listener(move |view, _, _, cx| off(view, Pointer::Up, cx)),
83        )
84        .child(render_with(
85            doc,
86            Editing {
87                selection,
88                // Read-only text has no caret. Without this a collapsed
89                // selection — every press that starts one — would blink an
90                // insertion point in text nobody can type into.
91                caret_on: false,
92                layouts: Some(layouts),
93                ..Editing::default()
94            },
95            window,
96            cx,
97        ))
98        .into_any_element()
99}
100
101/// The text a selection covers, as it would be pasted.
102///
103/// [`Doc::spans`] answers in parts — a paragraph, a cell, a line of a fence —
104/// and a newline between them is what puts a multi-block selection back
105/// together.
106pub fn copied(doc: &Doc, selection: Selection) -> String {
107    doc.spans(selection)
108        .into_iter()
109        .filter_map(|(at, range)| {
110            let text = &doc.blocks.get(at.block)?.text_at(at.part)?.text;
111            text.get(range).map(str::to_owned)
112        })
113        .collect::<Vec<_>>()
114        .join("\n")
115}