1use crate::{BlockLayouts, Cursor, Doc, Editing, Selection};
7use gpui::{
8 AnyElement, Context, CursorStyle, DispatchPhase, ElementId, MouseButton, MouseDownEvent,
9 MouseMoveEvent, Window, canvas, div, prelude::*,
10};
11use std::rc::Rc;
12
13pub enum Pointer {
15 Down(Cursor),
17 Move(Cursor),
19 Up,
21}
22
23#[expect(
40 clippy::too_many_arguments,
41 reason = "a document, its selection, and a gesture"
42)]
43pub fn render<V: 'static>(
44 id: impl Into<ElementId>,
45 doc: &Doc,
46 layouts: &BlockLayouts,
47 selection: Option<Selection>,
48 dragging: bool,
49 window: &mut Window,
50 cx: &mut Context<V>,
51 on_pointer: impl Fn(&mut V, Pointer, &mut Context<V>) + 'static,
52) -> AnyElement {
53 render_with(
54 id,
55 doc,
56 layouts,
57 selection,
58 dragging,
59 Editing::default(),
60 window,
61 cx,
62 on_pointer,
63 )
64}
65
66#[expect(
69 clippy::too_many_arguments,
70 reason = "a document, its selection, what else it paints, and a gesture"
71)]
72pub fn render_with<V: 'static>(
73 id: impl Into<ElementId>,
74 doc: &Doc,
75 layouts: &BlockLayouts,
76 selection: Option<Selection>,
77 dragging: bool,
78 editing: Editing,
79 window: &mut Window,
80 cx: &mut Context<V>,
81 on_pointer: impl Fn(&mut V, Pointer, &mut Context<V>) + 'static,
82) -> AnyElement {
83 let on_pointer = Rc::new(on_pointer);
84 let (down, moved, up, off) = (
85 on_pointer.clone(),
86 on_pointer.clone(),
87 on_pointer.clone(),
88 on_pointer,
89 );
90 let (at_down, at_move) = (layouts.clone(), layouts.clone());
91 div()
92 .id(id)
93 .cursor(CursorStyle::IBeam)
94 .on_mouse_down(
95 MouseButton::Left,
96 cx.listener(move |view, event: &MouseDownEvent, _, cx| {
97 if let Some(cursor) = at_down.hit(event.position) {
98 down(view, Pointer::Down(cursor), cx);
99 }
100 }),
101 )
102 .children(dragging.then(|| {
106 let view = cx.entity();
107 canvas(
108 |_, _, _| (),
109 move |_, _, window, _| {
110 window.on_mouse_event(move |event: &MouseMoveEvent, phase, _, cx| {
111 if phase != DispatchPhase::Bubble
112 || event.pressed_button != Some(MouseButton::Left)
113 {
114 return;
115 }
116 if let Some(cursor) = at_move.hit(event.position) {
117 view.update(cx, |view, cx| {
118 moved(view, Pointer::Move(cursor), cx);
119 });
120 }
121 });
122 },
123 )
124 .absolute()
125 .size_0()
126 }))
127 .on_mouse_up(
128 MouseButton::Left,
129 cx.listener(move |view, _, _, cx| up(view, Pointer::Up, cx)),
130 )
131 .on_mouse_up_out(
132 MouseButton::Left,
133 cx.listener(move |view, _, _, cx| off(view, Pointer::Up, cx)),
134 )
135 .child(crate::render_with(
136 doc,
137 Editing {
138 selection,
139 caret_on: false,
143 layouts: Some(layouts),
144 ..editing
145 },
146 window,
147 cx,
148 ))
149 .into_any_element()
150}
151
152pub fn copied(doc: &Doc, selection: Selection) -> String {
158 doc.spans(selection)
159 .into_iter()
160 .filter_map(|(at, range)| {
161 let text = &doc.blocks.get(at.block)?.text_at(at.part)?.text;
162 text.get(range).map(str::to_owned)
163 })
164 .collect::<Vec<_>>()
165 .join("\n")
166}
167
168gpui::actions!(selectable, [Copy]);
169
170struct Bindings;
171impl gpui::Global for Bindings {}
172
173pub fn surface(
176 focus: &gpui::FocusHandle,
177 doc: &Doc,
178 selection: Option<Selection>,
179 cx: &mut gpui::App,
180) -> gpui::Div {
181 if !cx.has_global::<Bindings>() {
182 let chord = if cfg!(target_os = "macos") {
183 "cmd-c"
184 } else {
185 "ctrl-c"
186 };
187 cx.bind_keys([gpui::KeyBinding::new(chord, Copy, Some("SelectableText"))]);
188 cx.set_global(Bindings);
189 }
190 let text = selection
191 .map(|selection| copied(doc, selection))
192 .unwrap_or_default();
193 let focus = focus.clone();
194 div()
195 .key_context("SelectableText")
196 .track_focus(&focus)
197 .on_mouse_down(MouseButton::Left, move |_, window, cx| {
198 window.focus(&focus, cx)
199 })
200 .on_action(move |_: &Copy, _, cx| {
201 if !text.is_empty() {
202 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.clone()));
203 }
204 })
205}