Skip to main content

blockworx_editor/widget/
clipboard.rs

1//! Copy, cut, and paste, as the drawing surface sees them.
2//!
3//! The payload model itself lives in [`crate::edit::clipboard`] — a value
4//! snapshot of the copied closure with the gesture that made it, versioned
5//! as `Clipboard::V2` and carried over the OS clipboard as JSON. This module is the [`Drawing`] side: which
6//! selection a gesture copied, where a paste lands, and the emitter call each
7//! turns into. Nothing here decides *what* a copy contains — that is the
8//! delete closure's own walk, shared so a cut and a delete cascade agree.
9
10use blockworx_doc::{
11    document::Document,
12    geometry::GridVec,
13    id::{BlockId, PinId},
14};
15use blockworx_geom::Pos2;
16use blockworx_store::doc::DocumentNonce;
17
18use crate::grid::grid_point;
19use crate::{
20    edit::{
21        clipboard::{Clipboard, Paste, PasteTarget, PinPaste, Snapshot},
22        delete::Target,
23        geometry::Shape,
24    },
25    shape::ShapeId,
26    widget::drawing::Drawing,
27};
28
29/// The whole-cell diagonal a targetless paste lands at, so a duplicate never
30/// hides the original it was copied from.
31const UNTARGETED_OFFSET: GridVec = GridVec::new(2, 2);
32
33/// Whether `text` is a copied selection rather than plain text to insert into
34/// a focused editor.
35pub fn is_object_clipboard(text: &str) -> bool {
36    Clipboard::from_json(text).is_some()
37}
38
39/// The entity a copied shape names. An icon has none: it is a value on its
40/// block, and travels with it.
41fn copy_target(id: ShapeId) -> Option<Target> {
42    Some(match id {
43        ShapeId::Rect(id) => Target::Block(id),
44        ShapeId::Port(id) => Target::Pin(id),
45        ShapeId::Text(id) => Target::Text(id),
46        ShapeId::Area(id) => Target::Area(id),
47        ShapeId::Image(id) => Target::Image(id),
48        ShapeId::Icon(_) => return None,
49    })
50}
51
52/// What the tools name a pasted root.
53fn pasted_shape(shape: Shape) -> ShapeId {
54    match shape {
55        Shape::Block(id) => ShapeId::Rect(id),
56        Shape::Port(id) => ShapeId::Port(id),
57        Shape::Text(id) => ShapeId::Text(id),
58        Shape::Area(id) => ShapeId::Area(id),
59        Shape::Image(id) => ShapeId::Image(id),
60        Shape::Icon(id) => ShapeId::Icon(id),
61    }
62}
63
64impl Drawing<'_> {
65    /// The value snapshot of `shapes` — each with the subtree it owns, the
66    /// wires landing on any of their pins, and those wires' labels. `None`
67    /// when the selection holds nothing copyable.
68    pub fn copy_selection(&self, shapes: &[ShapeId]) -> Option<Clipboard> {
69        let targets: Vec<Target> = shapes.iter().filter_map(|&id| copy_target(id)).collect();
70        let clip = crate::edit::clipboard::copy(&self.indexed(), &targets);
71        (!clip.snapshot().is_empty()).then_some(clip)
72    }
73
74    /// The value snapshot of a pin selection — the payload a paste slots onto
75    /// another block's boundary.
76    pub fn copy_pins(&self, pins: &[PinId]) -> Option<Clipboard> {
77        let targets: Vec<Target> = pins.iter().copied().map(Target::Pin).collect();
78        let clip = crate::edit::clipboard::copy(&self.indexed(), &targets);
79        (!clip.snapshot().pins.is_empty()).then(|| clip.into_pins())
80    }
81
82    /// Copy `shapes`, then delete them — one commit, so the cut and the
83    /// cascade it implies undo together. What the delete declines the copy
84    /// declines too, which is what makes the paste that follows a *move*.
85    ///
86    /// `from` is the session document the cut is coming out of: the paste
87    /// that completes it keeps its ids only where it lands back there.
88    pub fn cut_selection(&mut self, shapes: &[ShapeId], from: DocumentNonce) -> Option<Clipboard> {
89        let targets: Vec<Target> = shapes.iter().filter_map(|&id| copy_target(id)).collect();
90        let mut clip = None;
91        self.author("cut_selection", |indexed, sink| {
92            clip = Some(crate::edit::clipboard::cut(indexed, &targets, from, sink));
93        });
94        clip.filter(|clip| !clip.snapshot().is_empty())
95    }
96
97    /// Copy a pin selection, then delete it — the pin half of a cut.
98    pub fn cut_pins(&mut self, pins: &[PinId], from: DocumentNonce) -> Option<Clipboard> {
99        let targets: Vec<Target> = pins.iter().copied().map(Target::Pin).collect();
100        let mut clip = None;
101        self.author("cut_pins", |indexed, sink| {
102            clip = Some(crate::edit::clipboard::cut(indexed, &targets, from, sink));
103        });
104        clip.filter(|clip| !clip.snapshot().pins.is_empty())
105            .map(Clipboard::into_pins)
106    }
107
108    /// Insert `clip` into this scope, returning the roots so the caller can
109    /// select them. A payload this document's own cut made, none of whose
110    /// sources it still holds, is the cut this paste completes and moves in
111    /// place; anything else duplicates.
112    ///
113    /// `into` is the session document being pasted into — a paste into any
114    /// other document mints, however free the ids look.
115    ///
116    /// `target_top_left` lands the group's own top-left corner there; with no
117    /// target the group is offset diagonally so it does not hide its source.
118    pub fn paste_snapshot(
119        &mut self,
120        clip: &Clipboard,
121        into: DocumentNonce,
122        target_top_left: Option<Pos2>,
123    ) -> Vec<ShapeId> {
124        let snapshot = clip.snapshot();
125        let offset = match (target_top_left, snapshot.origin()) {
126            (Some(target), Some(origin)) => {
127                let target = grid_point(target);
128                GridVec::new(target.x - origin.x, target.y - origin.y)
129            }
130            _ => UNTARGETED_OFFSET,
131        };
132        let scope = self.current_scope();
133        let mut ids = self.ids();
134        let mut roots = Vec::new();
135        self.author("paste", |indexed, sink| {
136            roots = crate::edit::clipboard::paste(
137                indexed,
138                Paste {
139                    clip,
140                    into,
141                    target: PasteTarget { scope, offset },
142                },
143                &mut ids,
144                sink,
145            );
146        });
147        roots.into_iter().map(pasted_shape).collect()
148    }
149
150    /// Embed `source` as a block of this document, at `at` in the current
151    /// scope. The document's own ports arrive as that block's pins; see
152    /// [`crate::edit::embed`].
153    pub fn embed(&mut self, source: &Document, title: &str, at: Pos2) -> BlockId {
154        let scope = self.current_scope();
155        let mut ids = self.ids();
156        let mut embedded = BlockId::NULL;
157        self.author("embed", |indexed, sink| {
158            embedded = crate::edit::embed::embed(
159                indexed,
160                crate::edit::embed::Embed {
161                    source,
162                    title,
163                    into: scope,
164                    at,
165                },
166                &mut ids,
167                sink,
168            );
169        });
170        embedded
171    }
172
173    /// Paste copied pins onto `owner`'s boundary, each at the next free slot,
174    /// growing the block until they fit. A frozen interface takes none.
175    pub fn paste_pins(&mut self, snapshot: &Snapshot, owner: crate::path::Scope) -> Vec<PinId> {
176        let mut ids = self.ids();
177        let mut pasted = Vec::new();
178        self.author("paste_pins", |indexed, sink| {
179            pasted = crate::edit::clipboard::paste_pins(
180                indexed,
181                PinPaste {
182                    pins: &snapshot.pins,
183                    owner,
184                },
185                &mut ids,
186                sink,
187            );
188        });
189        pasted
190    }
191}