gpui_kit_grid/state/copy.rs
1//! Copying rows to the clipboard: `Copy` writes the command scope, `CopyAll`
2//! every shown line under a line of column titles, with the pinned rows and
3//! the totals row of the top band before the lines and those of the bottom
4//! band after them; [`LiveGridState::copy_scope`] takes either, or the view
5//! with every group and tree row expanded.
6//!
7//! The lines of a grouped view are summary rows too ([`ExportLine`]): a
8//! group caption holds its group's key value under the level's column and
9//! its aggregates under theirs, a group footer and the totals hold
10//! aggregates. No delegate hook runs for a copy: a caption is its key value,
11//! not the caption text a frame paints (per-frame hooks run only in
12//! prepaint).
13//!
14//! A copy is taken when the command runs, as an export is (one
15//! `ExportCapture` for both): the snapshot, the shown order (a held one
16//! included) with its expansion, the visible columns in screen order,
17//! the totals label and the optimistic values of waiting edits, so a cell is
18//! copied as the screen shows it. Rows that arrive or change later are not
19//! in it. The grid's [`Exporter`] and [`CellExport`] write the text. A small
20//! copy is written at once; a larger one is written by a [`TaskRunner`] job
21//! (by default in the background), and only the clipboard write runs on the
22//! UI thread. A new copy cancels one still being written.
23//!
24//! [`ExportLine`]: crate::ExportLine
25//! [`TaskRunner`]: crate::TaskRunner
26
27use std::sync::Arc;
28
29use gpui_kit::{ClipboardItem, Context, Task, Window};
30
31use super::{LiveGridDelegate, LiveGridState, export::ExportCapture};
32use crate::{
33 actions,
34 export::{CellExport, ExportScope, Exporter, TsvExporter},
35 model::GridRow,
36};
37
38#[cfg(doc)]
39use crate::export::RawCells;
40
41/// A copy of at most this many cells (1k rows of 40 columns) is written at
42/// once on the UI thread, in about 2 ms with raw values; a larger one runs as
43/// a task runner job.
44const INLINE_COPY_CELLS: usize = 40_000;
45
46/// How a grid copies, and the copy being written.
47pub(super) struct CopySlot<R: GridRow> {
48 exporter: Arc<dyn Exporter<R>>,
49 cells: CellExport<R>,
50 /// Waits for the job writing the latest copy; dropping it cancels the
51 /// copy, so its text never reaches the clipboard.
52 job: Option<Task<()>>,
53}
54
55impl<R: GridRow> Default for CopySlot<R> {
56 fn default() -> Self {
57 Self {
58 exporter: Arc::new(TsvExporter),
59 cells: CellExport::default(),
60 job: None,
61 }
62 }
63}
64
65impl<D: LiveGridDelegate> LiveGridState<D> {
66 /// Sets how one cell becomes text when copied, and when exported unless
67 /// the export asks otherwise (`ExportRequest::with_cell_export`). The
68 /// default is `CellExport::Raw(RawCells::new())`: raw numbers with a
69 /// `.`, `TRUE` and `FALSE`; see also [`RawCells::with_decimal_separator`]
70 /// and `CellExport::Displayed`.
71 pub fn with_cell_export(mut self, export: CellExport<D::Row>) -> Self {
72 self.copy.cells = export;
73 self
74 }
75
76 /// Sets the clipboard format of a copy (default [`TsvExporter`]).
77 pub fn with_exporter(mut self, exporter: impl Exporter<D::Row>) -> Self {
78 self.copy.exporter = Arc::new(exporter);
79 self
80 }
81
82 /// Copies the command scope of the focused line in the shown order,
83 /// without a header line (`Copy`, Cmd/Ctrl+C): on a group caption every
84 /// row of its group that passes the view, those of collapsed subgroups
85 /// included; otherwise [`Self::scoped_selection`], which holds a row of
86 /// a collapsed group only if it is selected. Summary rows are never in
87 /// it. The visible columns are copied in screen order; in the cell modes
88 /// only the columns that hold a selected cell, and the cells of that
89 /// rectangle that are not selected are empty. Returns false, leaving the
90 /// clipboard alone, if there is nothing to copy.
91 ///
92 /// The same as `copy_scope(ExportScope::Selection)`. A large copy is
93 /// written by the task runner, and its text reaches the clipboard when
94 /// the job is done.
95 pub fn copy(&mut self, cx: &mut Context<Self>) -> bool {
96 self.copy_scope(ExportScope::Selection, cx)
97 }
98
99 /// Copies every shown line in the shown order (a held order as it is on
100 /// screen), every visible column in screen order, under a line of column
101 /// titles (`CopyAll`): group captions, the rows of expanded groups and
102 /// tree rows, group footers. The top band comes first, the totals row
103 /// before its pinned rows, and the bottom band last, the totals row
104 /// after its pinned rows. Returns false, leaving the clipboard alone, if
105 /// there is no line, no pinned row and no totals row to copy.
106 ///
107 /// The same as `copy_scope(ExportScope::Visible)`. A large copy is
108 /// written by the task runner, and its text reaches the clipboard when
109 /// the job is done.
110 pub fn copy_all(&mut self, cx: &mut Context<Self>) -> bool {
111 self.copy_scope(ExportScope::Visible, cx)
112 }
113
114 /// Copies `scope` to the clipboard: `Selection` as [`Self::copy`],
115 /// `Visible` as [`Self::copy_all`], `Expanded` as `copy_all` with every
116 /// group and tree row expanded (the rows of collapsed groups under their
117 /// captions, the footers of collapsed groups, every descendant of a
118 /// collapsed tree row). Returns false, leaving the clipboard alone, if
119 /// there is nothing to copy.
120 ///
121 /// A group caption is written as its group's key value under the
122 /// level's column, or under the first copied column when that column is
123 /// hidden or the level groups by a `GroupKey`, and its aggregates under
124 /// theirs; a footer and the totals row as their aggregates, the totals
125 /// label in the first copied column when it has no aggregate. A totals
126 /// row is copied when the grid shows one and the view has aggregates.
127 ///
128 /// A large copy is written by the task runner, and its text reaches the
129 /// clipboard when the job is done.
130 pub fn copy_scope(&mut self, scope: ExportScope, cx: &mut Context<Self>) -> bool {
131 // A copy of the selection has no header line.
132 let header = scope != ExportScope::Selection;
133 let capture = self.capture(scope, None, header, self.copy.cells.clone());
134 if capture.is_empty() {
135 return false;
136 }
137 self.start_copy(capture, cx)
138 }
139
140 /// Cmd/Ctrl+C; with nothing to copy the key goes on.
141 pub(super) fn on_copy(&mut self, _: &actions::Copy, _: &mut Window, cx: &mut Context<Self>) {
142 if !self.copy(cx) {
143 cx.propagate();
144 }
145 }
146
147 /// "Copy all"; with nothing to copy the command goes on.
148 pub(super) fn on_copy_all(
149 &mut self,
150 _: &actions::CopyAll,
151 _: &mut Window,
152 cx: &mut Context<Self>,
153 ) {
154 if !self.copy_all(cx) {
155 cx.propagate();
156 }
157 }
158
159 /// How the grid writes a cell's text: an export's default too.
160 pub(super) fn copy_cells(&self) -> &CellExport<D::Row> {
161 &self.copy.cells
162 }
163
164 /// Writes a copy at once if it is small, else starts a job for it. A
165 /// copy still being written is cancelled either way.
166 fn start_copy(&mut self, capture: ExportCapture<D::Row>, cx: &mut Context<Self>) -> bool {
167 self.copy.job = None;
168 let exporter = self.copy.exporter.clone();
169 let write = move |capture: &ExportCapture<D::Row>| {
170 capture.write(|table, cells| {
171 // Every line's row was removed since the order was built.
172 if table.rows().is_empty() {
173 return None;
174 }
175 let mut text = String::new();
176 exporter.export(table, cells, &mut text);
177 Some(text)
178 })
179 };
180 if capture.cell_count() <= INLINE_COPY_CELLS {
181 let Some(text) = write(&capture) else {
182 return false;
183 };
184 cx.write_to_clipboard(ClipboardItem::new_string(text));
185 return true;
186 }
187 let (done, result) = async_channel::bounded(1);
188 let job = self.runner.run(
189 Box::new(move || {
190 let _ = done.try_send(write(&capture));
191 }),
192 cx,
193 );
194 self.copy.job = Some(cx.spawn(async move |this, cx| {
195 job.await;
196 let Ok(text) = result.recv().await else {
197 return;
198 };
199 let _ = this.update(cx, |state, cx| {
200 state.copy.job = None;
201 if let Some(text) = text {
202 cx.write_to_clipboard(ClipboardItem::new_string(text));
203 }
204 });
205 }));
206 true
207 }
208}