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