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