1use crate::{BlockLayouts, Cursor, Doc, Editing, Selection, render_with};
7use gpui::{
8 AnyElement, Context, CursorStyle, ElementId, MouseButton, MouseDownEvent, MouseMoveEvent,
9 Window, div, prelude::*,
10};
11use std::rc::Rc;
12
13pub enum Pointer {
15 Down(Cursor),
17 Move(Cursor),
19 Up,
21}
22
23#[expect(
33 clippy::too_many_arguments,
34 reason = "a document, its selection, and a gesture"
35)]
36pub fn render<V: 'static>(
37 id: impl Into<ElementId>,
38 doc: &Doc,
39 layouts: &BlockLayouts,
40 selection: Option<Selection>,
41 dragging: bool,
42 window: &mut Window,
43 cx: &mut Context<V>,
44 on_pointer: impl Fn(&mut V, Pointer, &mut Context<V>) + 'static,
45) -> AnyElement {
46 let on_pointer = Rc::new(on_pointer);
47 let (down, moved, up, off) = (
48 on_pointer.clone(),
49 on_pointer.clone(),
50 on_pointer.clone(),
51 on_pointer,
52 );
53 let (at_down, at_move) = (layouts.clone(), layouts.clone());
54 div()
55 .id(id)
56 .cursor(CursorStyle::IBeam)
57 .on_mouse_down(
58 MouseButton::Left,
59 cx.listener(move |view, event: &MouseDownEvent, _, cx| {
60 if let Some(cursor) = at_down.hit(event.position) {
61 down(view, Pointer::Down(cursor), cx);
62 }
63 }),
64 )
65 .on_mouse_move(cx.listener(move |view, event: &MouseMoveEvent, _, cx| {
66 if dragging && let Some(cursor) = at_move.hit(event.position) {
67 moved(view, Pointer::Move(cursor), cx);
68 }
69 }))
70 .on_mouse_up(
71 MouseButton::Left,
72 cx.listener(move |view, _, _, cx| up(view, Pointer::Up, cx)),
73 )
74 .on_mouse_up_out(
75 MouseButton::Left,
76 cx.listener(move |view, _, _, cx| off(view, Pointer::Up, cx)),
77 )
78 .child(render_with(
79 doc,
80 Editing {
81 selection,
82 caret_on: false,
86 layouts: Some(layouts),
87 ..Editing::default()
88 },
89 window,
90 cx,
91 ))
92 .into_any_element()
93}
94
95pub fn copied(doc: &Doc, selection: Selection) -> String {
101 doc.spans(selection)
102 .into_iter()
103 .filter_map(|(at, range)| {
104 let text = &doc.blocks.get(at.block)?.text_at(at.part)?.text;
105 text.get(range).map(str::to_owned)
106 })
107 .collect::<Vec<_>>()
108 .join("\n")
109}
110
111gpui::actions!(selectable, [Copy]);
112
113struct Bindings;
114impl gpui::Global for Bindings {}
115
116pub fn surface(
119 focus: &gpui::FocusHandle,
120 doc: &Doc,
121 selection: Option<Selection>,
122 cx: &mut gpui::App,
123) -> gpui::Div {
124 if !cx.has_global::<Bindings>() {
125 let chord = if cfg!(target_os = "macos") {
126 "cmd-c"
127 } else {
128 "ctrl-c"
129 };
130 cx.bind_keys([gpui::KeyBinding::new(chord, Copy, Some("SelectableText"))]);
131 cx.set_global(Bindings);
132 }
133 let text = selection
134 .map(|selection| copied(doc, selection))
135 .unwrap_or_default();
136 let focus = focus.clone();
137 div()
138 .key_context("SelectableText")
139 .track_focus(&focus)
140 .on_mouse_down(MouseButton::Left, move |_, window, cx| {
141 window.focus(&focus, cx)
142 })
143 .on_action(move |_: &Copy, _, cx| {
144 if !text.is_empty() {
145 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.clone()));
146 }
147 })
148}