mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 02:34:33 +00:00
M1: Cmd/Ctrl+C and Cmd/Ctrl+V copy and paste the highlighted fill, stroke or effect (#1494)
With a fill, stroke or effect row highlighted, Cmd/Ctrl+C (or Edit ▸ Copy) copies that paint or effect instead of the layers, through the same `fill.copy`, `stroke.copy` and `effect.copy` the row's menu runs. Cmd/Ctrl+V then pastes it onto every selected layer with `fill.paste`, `stroke.paste` or `effect.paste`, one undo step: on top of each layer's list, or in place of the highlighted row when it is of the same kind. - A copy keeps its kind. A stroke goes to the strokes and an effect to the effects whatever row is highlighted, and the last row copied is the one pasted. - A solid colour goes to the system clipboard as its hex code, anything else as a marker. A paste takes the property only while the system clipboard still holds that copy and no layers were copied since, so text or an image from another app, or a later layer copy, pastes as before. The row menus' Copy writes the same, so Cmd/Ctrl+V pastes it. - A programmatic copy never writes the system clipboard, and forgets a property mirror it replaced. A mirror whose engine copy is gone pastes nothing rather than the layers copied before it. - The copy is shared by every window, as the layer copy is. Tests: property_rows_tests (15 more, 22 in all); the replace index and the Escape key were each removed once to check the tests fail.
This commit is contained in:
@@ -592,8 +592,10 @@ pub struct UiState {
|
||||
/// copy changes the engine clipboard without writing the OS clipboard; the generation then
|
||||
/// no longer matches, so the stale mirror is not mistaken for the user's own copy.
|
||||
pub last_copy_generation: u64,
|
||||
/// The fill, stroke or effect row highlighted in the Design panel.
|
||||
/// The fill, stroke or effect row highlighted in the Design panel: Cmd/Ctrl+C copies it.
|
||||
pub property_row: Option<property_rows::Highlight>,
|
||||
/// A paint or effect copied from its row: Cmd/Ctrl+V pastes it while it's the last copy.
|
||||
pub property_copy: Option<property_rows::Copied>,
|
||||
/// A save shortcut pressed while a panel field had the keyboard: the field lets go (which
|
||||
/// commits what was typed) and the command runs next frame for that document/window.
|
||||
pub after_field: Option<shortcuts::AfterFieldSave>,
|
||||
@@ -738,6 +740,7 @@ impl Default for UiState {
|
||||
last_copy: None,
|
||||
last_copy_generation: 0,
|
||||
property_row: None,
|
||||
property_copy: None,
|
||||
after_field: None,
|
||||
held: egui::Modifiers::NONE,
|
||||
next_autosave: 0.0,
|
||||
|
||||
@@ -193,6 +193,10 @@ pub fn invoke(app: &mut FigApp, id: &str) {
|
||||
app.toast(tr("Properties copied"));
|
||||
}
|
||||
}
|
||||
// A fill, stroke or effect row highlighted in the Design panel: that paint or effect.
|
||||
"edit.copy" if crate::property_rows::current(app).is_some() => {
|
||||
crate::property_rows::copy_highlighted(app);
|
||||
}
|
||||
"edit.copy" | "edit.cut" => {
|
||||
let ids = app.session.selection();
|
||||
// For other apps: text layers' text, other layers as SVG; taken before a cut removes them.
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>)
|
||||
if app.control_synthetic {
|
||||
// Synthetic input is user-like: Dev Mode refuses the paste here too, and only the engine's
|
||||
// own copied layers are pasted (the system clipboard is never read).
|
||||
if !dev_mode_refuses(app, "edit.paste") {
|
||||
if !(at.is_none() && crate::property_rows::paste(app)) && !dev_mode_refuses(app, "edit.paste") {
|
||||
app.run("edit.paste", at.map_or(json!({}), |(x, y)| json!({"x": x, "y": y})));
|
||||
}
|
||||
return;
|
||||
@@ -81,6 +81,11 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>)
|
||||
return paste_text(app, &text, (cx, cy), at.is_none());
|
||||
}
|
||||
}
|
||||
// Nothing newer from another app: a fill, stroke or effect copied from its row, when that was
|
||||
// the last copy, else our copied layers.
|
||||
if at.is_none() && crate::property_rows::paste(app) {
|
||||
return;
|
||||
}
|
||||
if !dev_mode_refuses(app, "edit.paste") {
|
||||
app.run("edit.paste", at.map_or(json!({}), |(x, y)| json!({"x": x, "y": y})));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(super) fn effects(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[Node
|
||||
let selected_row = crate::theme::Tokens::get(ui.ctx()).selected_row;
|
||||
for (i, e) in list.iter().enumerate().rev() {
|
||||
let mut open = effect_open(ui.ctx(), i);
|
||||
// A click on the row's background highlights it (`property_rows`).
|
||||
// A click on the row's background highlights it for Cmd/Ctrl+C (`property_rows`).
|
||||
let highlight = crate::property_rows::fill(app, PropertyKind::Effect, i, selected_row);
|
||||
let header = row_target(ui, 32.0, highlight, |ui| {
|
||||
if icons::button(ui, Icon::Effects, 24.0, open, tr("Effect settings")).clicked() {
|
||||
@@ -72,7 +72,7 @@ pub(super) fn effects(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[Node
|
||||
let can_paste = app.session.clipboard.effect.is_some();
|
||||
row_context_menu(ui, header, Id::unique(("effect_menu", i))).show(|ui| {
|
||||
if menu_item(ui, tr("Copy effect"), "") {
|
||||
app.run("effect.copy", json!({"ids": ids, "index": i}));
|
||||
crate::property_rows::copy(app, PropertyKind::Effect, i, &ids);
|
||||
ui.close();
|
||||
}
|
||||
if can_paste && menu_item(ui, tr("Paste effect"), "") {
|
||||
|
||||
@@ -456,7 +456,7 @@ pub(super) fn paints(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeI
|
||||
/// Paint `i`'s row (swatch, colour or kind, opacity, visibility, remove); returns its area.
|
||||
/// One paint's row; `region`: a row of the regions selected in vector edit mode (#1875), which
|
||||
/// leaves out what belongs to the layer (its variable binding, the image crop). A click on its
|
||||
/// background highlights it ([`crate::property_rows`]).
|
||||
/// background highlights it for Cmd/Ctrl+C ([`crate::property_rows`]).
|
||||
fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint, fills: bool, region: bool) -> Rect {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ids = ids.to_vec();
|
||||
@@ -565,7 +565,7 @@ fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint,
|
||||
let can_paste = app.session.clipboard.paint.as_ref().is_some_and(|(f, _)| *f == fills);
|
||||
row_context_menu(ui, r, Id::unique(("paint_menu", fills, i))).show(|ui| {
|
||||
if menu_item(ui, copy, "") {
|
||||
app.run(if fills { "fill.copy" } else { "stroke.copy" }, json!({"ids": ids, "index": i}));
|
||||
crate::property_rows::copy(app, kind, i, &ids);
|
||||
ui.close();
|
||||
}
|
||||
if can_paste && menu_item(ui, paste, "") {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
//! Fill, stroke and effect rows of the Design panel that highlight, the first step of the help
|
||||
//! center's "Copy and paste properties between layers": click to highlight the property in the
|
||||
//! right sidebar.
|
||||
//! Fill, stroke and effect rows of the Design panel as things to copy, as the help center's "Copy
|
||||
//! and paste properties between layers" describes: click to highlight the property in the right
|
||||
//! sidebar, press Cmd/Ctrl+C, select another layer, press Cmd/Ctrl+V.
|
||||
//!
|
||||
//! A click on a row's background (beside and between its controls, or on its reorder grip)
|
||||
//! highlights it, one row at a time. The highlight belongs to the selection it was made on, and
|
||||
//! goes with Escape or a press on the canvas or elsewhere in the Design panel.
|
||||
//! goes with Escape or a press on the canvas or elsewhere in the Design panel. Cmd/Ctrl+C then
|
||||
//! copies that one paint or effect (`fill.copy`, `stroke.copy`, `effect.copy`, as the row's menu
|
||||
//! does) instead of the layers, and Cmd/Ctrl+V pastes it onto every selected layer (`fill.paste`,
|
||||
//! `stroke.paste`, `effect.paste`) while it is still the last thing copied: on top of their list,
|
||||
//! or in place of their highlighted row when it is of the same kind. A copy keeps its kind: a
|
||||
//! stroke never lands in the fills, whatever row is highlighted.
|
||||
|
||||
use egui::{Color32, Context, Rect, Response};
|
||||
use figcraft_doc::NodeId;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{FigApp, RightTab};
|
||||
|
||||
@@ -19,6 +25,46 @@ pub enum PropertyKind {
|
||||
Effect,
|
||||
}
|
||||
|
||||
impl PropertyKind {
|
||||
/// The command that copies row `index` of this list.
|
||||
fn copy_command(self) -> &'static str {
|
||||
match self {
|
||||
PropertyKind::Fill => "fill.copy",
|
||||
PropertyKind::Stroke => "stroke.copy",
|
||||
PropertyKind::Effect => "effect.copy",
|
||||
}
|
||||
}
|
||||
|
||||
/// The command that pastes the copied paint or effect.
|
||||
fn paste_command(self) -> &'static str {
|
||||
match self {
|
||||
PropertyKind::Fill => "fill.paste",
|
||||
PropertyKind::Stroke => "stroke.paste",
|
||||
PropertyKind::Effect => "effect.paste",
|
||||
}
|
||||
}
|
||||
|
||||
/// What the system clipboard gets when the copy has no text of its own: the marker that also
|
||||
/// stands for copied layers, so a paste never makes a text layer of it.
|
||||
fn marker(self) -> &'static str {
|
||||
match self {
|
||||
PropertyKind::Fill => "figcraft://clipboard/fill",
|
||||
PropertyKind::Stroke => "figcraft://clipboard/stroke",
|
||||
PropertyKind::Effect => "figcraft://clipboard/effect",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the engine clipboard still holds a copy of this kind.
|
||||
fn on_clipboard(self, app: &FigApp) -> bool {
|
||||
let clip = &app.session.clipboard;
|
||||
match self {
|
||||
PropertyKind::Fill => clip.paint.as_ref().is_some_and(|(fills, _)| *fills),
|
||||
PropertyKind::Stroke => clip.paint.as_ref().is_some_and(|(fills, _)| !*fills),
|
||||
PropertyKind::Effect => clip.effect.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The row highlighted in the Design panel. It belongs to the layers selected when it was clicked
|
||||
/// and goes when they are no longer the selection.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -35,6 +81,18 @@ pub struct Highlight {
|
||||
pub rect: Option<Rect>,
|
||||
}
|
||||
|
||||
/// A paint or effect copied from its row: Cmd/Ctrl+V pastes it rather than the copied layers while
|
||||
/// it is the last thing copied.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Copied {
|
||||
pub kind: PropertyKind,
|
||||
/// The engine clipboard's layers generation at the copy: a layer copy since moves it on.
|
||||
pub layers_generation: u64,
|
||||
/// What the copy put on the system clipboard (`None`: a programmatic copy, which never writes
|
||||
/// it). A user's paste takes the property only while the system clipboard is still that.
|
||||
pub mirror: Option<String>,
|
||||
}
|
||||
|
||||
/// The highlighted row, when it still belongs to the selection in the Design panel.
|
||||
pub(crate) fn current(app: &FigApp) -> Option<&Highlight> {
|
||||
let h = app.ui.property_row.as_ref()?;
|
||||
@@ -45,7 +103,7 @@ pub(crate) fn current(app: &FigApp) -> Option<&Highlight> {
|
||||
/// Let the highlight go once it no longer belongs to what the Design panel shows (another file or
|
||||
/// tab, another selection, the panel not `shown`), or when the pointer is pressed on the canvas or
|
||||
/// elsewhere in the Design panel. A press on its own row keeps it; so does one on a menu, a popup
|
||||
/// or the toolbar, so a menu command can still act on it. Called each frame before
|
||||
/// or the toolbar, so Edit ▸ Copy still copies the highlighted property. Called each frame before
|
||||
/// the right panel draws its rows.
|
||||
pub(crate) fn reconcile(app: &mut FigApp, ctx: &Context, shown: bool) {
|
||||
if app.ui.property_row.is_none() {
|
||||
@@ -115,3 +173,79 @@ pub(crate) fn follow(app: &mut FigApp, kind: PropertyKind, from: usize, to: Opti
|
||||
None => app.ui.property_row = None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Cmd/Ctrl+C with a highlighted row: copy its paint or effect. `false` when no row is
|
||||
/// highlighted (the layers are copied as usual).
|
||||
pub(crate) fn copy_highlighted(app: &mut FigApp) -> bool {
|
||||
let Some(h) = current(app).cloned() else { return false };
|
||||
copy(app, h.kind, h.index, &h.ids);
|
||||
true
|
||||
}
|
||||
|
||||
/// Copy paint or effect `index` of the `kind` list of `ids` (a highlighted row, or the row's menu)
|
||||
/// and remember it as the last thing copied. A solid colour goes to the system clipboard as its
|
||||
/// hex code, as other apps read colours; anything else as a marker of the copy.
|
||||
pub(crate) fn copy(app: &mut FigApp, kind: PropertyKind, index: usize, ids: &[NodeId]) {
|
||||
let Some(copied) = app.run(kind.copy_command(), json!({"ids": ids, "index": index})) else { return };
|
||||
let layers_generation = app.session.clipboard.layers_generation;
|
||||
let mirror = if app.control_synthetic {
|
||||
// The engine's copy is no longer what the system clipboard mirrors: a user's paste must
|
||||
// not take that text for this copy (`menus::paste` would paste stale layers).
|
||||
if app.ui.property_copy.as_ref().is_some_and(|c| c.mirror.is_some() && c.mirror == app.ui.last_copy) {
|
||||
app.ui.last_copy = None;
|
||||
}
|
||||
None
|
||||
} else {
|
||||
let text = match kind {
|
||||
PropertyKind::Effect => None,
|
||||
PropertyKind::Fill | PropertyKind::Stroke => {
|
||||
serde_json::from_value::<figcraft_color::Paint>(copied).ok().and_then(|p| p.solid_color()).map(figcraft_color::Color::to_css_hex)
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|| kind.marker().to_string());
|
||||
app.clipboard_set_text(&text);
|
||||
// The system clipboard holds our copy now: a paste reads it as ours (`menus::paste`).
|
||||
app.ui.last_copy = Some(text.clone());
|
||||
app.ui.last_copy_generation = layers_generation;
|
||||
Some(text)
|
||||
};
|
||||
app.ui.property_copy = Some(Copied { kind, layers_generation, mirror });
|
||||
}
|
||||
|
||||
/// Cmd/Ctrl+V when nothing newer than our own copy is on the system clipboard: when a paint or
|
||||
/// effect copied from its row is the last thing copied, paste it onto the selected layers (in
|
||||
/// place of their highlighted row of that kind, else on top) and return `true`; `false` leaves
|
||||
/// the paste to the copied layers.
|
||||
pub(crate) fn paste(app: &mut FigApp) -> bool {
|
||||
let Some(c) = app.ui.property_copy.clone() else { return false };
|
||||
let still_last = c.layers_generation == app.session.clipboard.layers_generation
|
||||
&& c.kind.on_clipboard(app)
|
||||
// A user's paste: the system clipboard must still be what this copy put there. A
|
||||
// programmatic paste reads only the engine clipboard.
|
||||
&& (app.control_synthetic || (c.mirror.is_some() && app.ui.last_copy == c.mirror));
|
||||
if !still_last {
|
||||
// The system clipboard still holds this copy's mirror, but the engine's copy of it is gone
|
||||
// (a command copied a paint or effect of another kind since): nothing to paste, rather
|
||||
// than the layers copied before it.
|
||||
return !app.control_synthetic
|
||||
&& c.mirror.is_some()
|
||||
&& app.ui.last_copy == c.mirror
|
||||
&& app.ui.last_copy_generation == app.session.clipboard.layers_generation
|
||||
&& c.layers_generation == app.session.clipboard.layers_generation;
|
||||
}
|
||||
let ids = app.session.selection();
|
||||
if ids.is_empty() {
|
||||
// Nothing to paste onto: a paste of the layers copied before would be surprising.
|
||||
return true;
|
||||
}
|
||||
let cmd = c.kind.paste_command();
|
||||
if crate::menus::dev_mode_refuses(app, cmd) {
|
||||
return true;
|
||||
}
|
||||
let mut params = json!({"ids": ids});
|
||||
if let Some(h) = current(app).filter(|h| h.kind == c.kind) {
|
||||
params["index"] = json!(h.index);
|
||||
}
|
||||
app.run(cmd, params);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! A fill, stroke or effect row of the Design panel highlights on a click on its background or its
|
||||
//! grip ("Copy and paste properties between layers": click to highlight the property in the right
|
||||
//! sidebar).
|
||||
//! A fill, stroke or effect row of the Design panel highlights on a click on its background, and
|
||||
//! the keyboard copies and pastes it ("Copy and paste properties between layers": click to
|
||||
//! highlight the property in the right sidebar, Cmd/Ctrl+C, select another layer, Cmd/Ctrl+V).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
@@ -70,12 +70,144 @@ fn click_row(u: &mut Ui, label: &str) {
|
||||
click(u, pos2(panel.min.x + 6.0, y));
|
||||
}
|
||||
|
||||
/// Cmd/Ctrl+C, as the desktop shell delivers it.
|
||||
fn copy(u: &mut Ui) {
|
||||
u.frame(vec![Event::Copy]);
|
||||
}
|
||||
|
||||
/// Cmd/Ctrl+V with the system clipboard's text, as the desktop shell delivers it.
|
||||
fn paste(u: &mut Ui, clip: &Rc<RefCell<Option<String>>>) {
|
||||
let text = clip.borrow().clone().unwrap_or_default();
|
||||
u.frame(vec![Event::Paste(text)]);
|
||||
}
|
||||
|
||||
/// Select `id` only, and draw the panel for it.
|
||||
fn select(u: &mut Ui, id: NodeId) {
|
||||
u.app.run("select.set", json!({"ids": [id]}));
|
||||
u.frame(vec![]);
|
||||
}
|
||||
|
||||
/// The hex codes of `id`'s fills, bottom to top.
|
||||
fn fills(u: &Ui, id: NodeId) -> Vec<String> {
|
||||
let doc = u.app.session.doc().unwrap();
|
||||
doc.node(id).unwrap().fills.iter().map(|p| p.solid_color().unwrap().to_hex()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_on_a_fill_rows_background_highlights_it_and_the_keyboard_copies_and_pastes_it() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
click_row(&mut u, "FF0000");
|
||||
assert_eq!(
|
||||
u.app.ui.property_row.as_ref().map(|h| (h.kind, h.index, h.ids.clone())),
|
||||
Some((PropertyKind::Fill, 0, vec![a])),
|
||||
"the row is highlighted for A"
|
||||
);
|
||||
let layers_before = u.app.session.clipboard.layers_generation;
|
||||
copy(&mut u);
|
||||
let (fills_flag, paint) = u.app.session.clipboard.paint.clone().expect("the fill is on the clipboard");
|
||||
assert!(fills_flag);
|
||||
assert_eq!(paint, u.app.session.doc().unwrap().node(a).unwrap().fills[0]);
|
||||
assert_eq!(u.app.session.clipboard.layers_generation, layers_before, "the layers were not copied");
|
||||
assert!(u.app.session.clipboard.nodes.is_empty());
|
||||
assert_eq!(clip.borrow().as_deref(), Some("#FF0000"), "the colour goes to the system clipboard as its hex code");
|
||||
// Another layer selected, the paste puts the fill on top of its own.
|
||||
select(&mut u, b);
|
||||
assert!(u.app.ui.property_row.is_none(), "the highlight belonged to A");
|
||||
let layers = u.app.session.doc().unwrap().node_count();
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(fills(&u, b), ["00FF00", "FF0000"]);
|
||||
assert_eq!(u.app.session.doc().unwrap().node_count(), layers, "no layer was pasted");
|
||||
assert_eq!(fills(&u, a), ["FF0000"], "A keeps its fill");
|
||||
// One undo step takes it back off.
|
||||
u.app.run("edit.undo", json!({}));
|
||||
assert_eq!(fills(&u, b), ["00FF00"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paste_replaces_the_highlighted_row_of_the_target() {
|
||||
let (mut u, _, b, clip) = setup();
|
||||
click_row(&mut u, "FF0000");
|
||||
copy(&mut u);
|
||||
select(&mut u, b);
|
||||
click_row(&mut u, "00FF00");
|
||||
assert_eq!(u.app.ui.property_row.as_ref().map(|h| (h.kind, h.index)), Some((PropertyKind::Fill, 0)));
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(fills(&u, b), ["FF0000"], "in place of B's highlighted fill");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stroke_row_copies_its_paint_and_settings() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
click_row(&mut u, "0000FF");
|
||||
assert_eq!(u.app.ui.property_row.as_ref().map(|h| h.kind), Some(PropertyKind::Stroke));
|
||||
copy(&mut u);
|
||||
select(&mut u, b);
|
||||
paste(&mut u, &clip);
|
||||
let doc = u.app.session.doc().unwrap();
|
||||
let (na, nb) = (doc.node(a).unwrap(), doc.node(b).unwrap());
|
||||
assert_eq!(nb.strokes, na.strokes);
|
||||
assert_eq!(nb.stroke.weight, 3.0, "the weight comes with the stroke");
|
||||
assert_eq!(fills(&u, b), ["00FF00"], "the fills are untouched");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_effect_row_copies_and_pastes_its_effect() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
click_row(&mut u, "Layer blur");
|
||||
assert_eq!(u.app.ui.property_row.as_ref().map(|h| (h.kind, h.index)), Some((PropertyKind::Effect, 0)));
|
||||
copy(&mut u);
|
||||
assert_eq!(clip.borrow().as_deref(), Some("figcraft://clipboard/effect"), "an effect has no text: a marker of our copy");
|
||||
select(&mut u, b);
|
||||
paste(&mut u, &clip);
|
||||
let doc = u.app.session.doc().unwrap();
|
||||
let blur = doc.node(a).unwrap().effects[0].clone();
|
||||
assert_eq!(doc.node(b).unwrap().effects.len(), 2);
|
||||
assert_eq!(doc.node(b).unwrap().effects[1], blur, "the blur goes on top of B's shadow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_highlighted_row_the_keyboard_copies_and_pastes_layers() {
|
||||
let (mut u, _, b, clip) = setup();
|
||||
copy(&mut u);
|
||||
assert!(u.app.session.clipboard.paint.is_none(), "no fill was copied");
|
||||
assert_eq!(u.app.session.clipboard.nodes.len(), 1, "A was copied");
|
||||
select(&mut u, b);
|
||||
let layers = u.app.session.doc().unwrap().node_count();
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(u.app.session.doc().unwrap().node_count(), layers + 1, "a copy of A was pasted");
|
||||
assert_eq!(fills(&u, b), ["00FF00"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copying_layers_after_a_fill_pastes_the_layers() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
click_row(&mut u, "FF0000");
|
||||
copy(&mut u);
|
||||
// Another selection lets the highlight go; the next copy takes the layers.
|
||||
select(&mut u, b);
|
||||
select(&mut u, a);
|
||||
assert!(u.app.ui.property_row.is_none(), "selecting A again doesn't bring the highlight back");
|
||||
copy(&mut u);
|
||||
select(&mut u, b);
|
||||
let layers = u.app.session.doc().unwrap().node_count();
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(u.app.session.doc().unwrap().node_count(), layers + 1, "the layers were the last copy");
|
||||
assert_eq!(fills(&u, b), ["00FF00"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_copied_in_another_app_afterwards_pastes_as_text() {
|
||||
let (mut u, _, b, clip) = setup();
|
||||
click_row(&mut u, "FF0000");
|
||||
copy(&mut u);
|
||||
select(&mut u, b);
|
||||
*clip.borrow_mut() = Some("Hello".into());
|
||||
let layers = u.app.session.doc().unwrap().node_count();
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(fills(&u, b), ["00FF00"], "the fill isn't what the clipboard holds now");
|
||||
assert_eq!(u.app.session.doc().unwrap().node_count(), layers + 1, "the text became a text layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_elsewhere_lets_the_highlight_go() {
|
||||
let (mut u, a, _, _) = setup();
|
||||
@@ -89,6 +221,9 @@ fn a_press_elsewhere_lets_the_highlight_go() {
|
||||
click(&mut u, text_center(&shapes, "Effects"));
|
||||
assert_eq!(u.app.session.selection(), [a]);
|
||||
assert!(u.app.ui.property_row.is_none());
|
||||
// With nothing highlighted Cmd/Ctrl+C copies the layer again.
|
||||
copy(&mut u);
|
||||
assert!(u.app.session.clipboard.paint.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -141,6 +276,126 @@ fn a_click_on_the_canvas_lets_the_highlight_go() {
|
||||
assert!(u.app.ui.property_row.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_copy_from_the_menu_copies_the_highlighted_row() {
|
||||
let (mut u, _, _, _) = setup();
|
||||
click_row(&mut u, "FF0000");
|
||||
crate::menus::invoke(&mut u.app, "edit.copy");
|
||||
assert!(u.app.session.clipboard.paint.as_ref().is_some_and(|(fills, _)| *fills), "the fill was copied");
|
||||
assert!(u.app.session.clipboard.nodes.is_empty(), "not the layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paste_onto_several_layers_adds_the_fill_to_each_in_one_undo_step() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
let c = rect(&mut u.app.session, 200.0, "#0000FF");
|
||||
// A new rectangle is selected: A again.
|
||||
select(&mut u, a);
|
||||
click_row(&mut u, "FF0000");
|
||||
copy(&mut u);
|
||||
// B and C have different fills: the panel shows mixed content, no row to highlight.
|
||||
u.app.run("select.set", json!({"ids": [b, c]}));
|
||||
u.frame(vec![]);
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(fills(&u, b), ["00FF00", "FF0000"]);
|
||||
assert_eq!(fills(&u, c), ["0000FF", "FF0000"]);
|
||||
u.app.run("edit.undo", json!({}));
|
||||
assert_eq!((fills(&u, b), fills(&u, c)), (vec!["00FF00".to_string()], vec!["0000FF".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paste_replaces_the_highlighted_row_on_every_selected_layer() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
let c = rect(&mut u.app.session, 200.0, "#00FF00");
|
||||
// A new rectangle is selected: A again.
|
||||
select(&mut u, a);
|
||||
click_row(&mut u, "FF0000");
|
||||
copy(&mut u);
|
||||
// B and C share their fill: its row shows for both.
|
||||
u.app.run("select.set", json!({"ids": [b, c]}));
|
||||
u.frame(vec![]);
|
||||
click_row(&mut u, "00FF00");
|
||||
assert_eq!(u.app.ui.property_row.as_ref().map(|h| h.ids.clone()), Some(vec![b, c]));
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!((fills(&u, b), fills(&u, c)), (vec!["FF0000".to_string()], vec!["FF0000".to_string()]));
|
||||
u.app.run("edit.undo", json!({}));
|
||||
assert_eq!((fills(&u, b), fills(&u, c)), (vec!["00FF00".to_string()], vec!["00FF00".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_copied_stroke_goes_to_the_strokes_with_a_fill_row_highlighted() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
click_row(&mut u, "0000FF");
|
||||
copy(&mut u);
|
||||
select(&mut u, b);
|
||||
click_row(&mut u, "00FF00");
|
||||
assert_eq!(u.app.ui.property_row.as_ref().map(|h| h.kind), Some(PropertyKind::Fill));
|
||||
paste(&mut u, &clip);
|
||||
let doc = u.app.session.doc().unwrap();
|
||||
assert_eq!(doc.node(b).unwrap().strokes, doc.node(a).unwrap().strokes, "the stroke went to the strokes");
|
||||
assert_eq!(fills(&u, b), ["00FF00"], "the highlighted fill wasn't replaced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_effect_copied_after_a_fill_is_what_pastes() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
click_row(&mut u, "FF0000");
|
||||
copy(&mut u);
|
||||
click_row(&mut u, "Layer blur");
|
||||
copy(&mut u);
|
||||
select(&mut u, b);
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(fills(&u, b), ["00FF00"], "the fill was copied first: not pasted");
|
||||
let doc = u.app.session.doc().unwrap();
|
||||
assert_eq!(doc.node(b).unwrap().effects.get(1), doc.node(a).unwrap().effects.first(), "the blur went on top of B's shadow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_menu_copy_is_pasted_by_the_keyboard() {
|
||||
let (mut u, _, b, clip) = setup();
|
||||
let shapes = flat(&u.frame_output(vec![]).shapes);
|
||||
let p = text_center(&shapes, "FF0000");
|
||||
for pressed in [true, false] {
|
||||
u.frame(vec![Event::PointerButton { pos: p, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE }]);
|
||||
}
|
||||
let menu = flat(&u.frame_output(vec![]).shapes);
|
||||
click(&mut u, text_center(&menu, crate::i18n::tr("Copy fill")));
|
||||
assert_eq!(clip.borrow().as_deref(), Some("#FF0000"));
|
||||
select(&mut u, b);
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(fills(&u, b), ["00FF00", "FF0000"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn programmatic_copy_and_paste_of_a_row_leave_the_system_clipboard_alone() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
*clip.borrow_mut() = Some("the user's text".into());
|
||||
let doc = u.app.session.ds().unwrap().uid;
|
||||
u.app.ui.property_row = Some(Highlight { kind: PropertyKind::Fill, index: 0, ids: vec![a], doc, rect: None });
|
||||
u.app.control_synthetic = true;
|
||||
crate::menus::invoke(&mut u.app, "edit.copy");
|
||||
assert_eq!(clip.borrow().as_deref(), Some("the user's text"), "never written");
|
||||
u.app.run("select.set", json!({"ids": [b]}));
|
||||
crate::menus::paste(&mut u.app, None, None);
|
||||
assert_eq!(fills(&u, b), ["00FF00", "FF0000"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_property_copy_whose_engine_copy_is_gone_pastes_no_stale_layers() {
|
||||
let (mut u, a, b, clip) = setup();
|
||||
// A layer copy first, then the fill row's.
|
||||
copy(&mut u);
|
||||
click_row(&mut u, "FF0000");
|
||||
copy(&mut u);
|
||||
// A stroke copied without its row (the Actions palette): the fill is no longer on the clipboard.
|
||||
u.app.run("stroke.copy", json!({"id": a}));
|
||||
select(&mut u, b);
|
||||
let layers = u.app.session.doc().unwrap().node_count();
|
||||
paste(&mut u, &clip);
|
||||
assert_eq!(u.app.session.doc().unwrap().node_count(), layers, "the layer copied before wasn't pasted");
|
||||
assert_eq!(fills(&u, b), ["00FF00"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_highlighted_row_whose_paint_an_undo_took_away_goes() {
|
||||
let (mut u, a, _, _) = setup();
|
||||
|
||||
@@ -236,6 +236,8 @@ fn exchange(app: &mut FigApp, p: &mut Pane) {
|
||||
std::mem::swap(&mut app.ui, &mut p.ui);
|
||||
std::mem::swap(&mut app.ui.last_copy, &mut p.ui.last_copy);
|
||||
std::mem::swap(&mut app.ui.last_copy_generation, &mut p.ui.last_copy_generation);
|
||||
// A paint or effect copied from its row is on the one clipboard every window pastes from.
|
||||
std::mem::swap(&mut app.ui.property_copy, &mut p.ui.property_copy);
|
||||
std::mem::swap(&mut app.ui.quit, &mut p.ui.quit);
|
||||
std::mem::swap(&mut app.ui.dark, &mut p.ui.dark);
|
||||
std::mem::swap(&mut app.canvas, &mut p.canvas);
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user