Skip to main content

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}