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}