mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 04:46:50 -06:00
482 lines
18 KiB
Rust
482 lines
18 KiB
Rust
//! Pasting an image copied in another app (#663). On the desktop, egui-winit swallows Cmd/Ctrl+V's
|
|
//! press and sends a paste event only when the clipboard holds text: an image leaves just the
|
|
//! key's release. The browser sends the press and a paste event (`web_clipboard_tests`).
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use egui::{Event, Key, Modifiers, PointerButton, Pos2, RawInput, Rect, Shape, vec2};
|
|
use figcraft_doc::NodeId;
|
|
use figcraft_engine::Session;
|
|
use serde_json::json;
|
|
|
|
use crate::{FigApp, Services};
|
|
|
|
/// What the system clipboard holds when a paste chord is pressed (#716).
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum Held {
|
|
/// FigCraft's own copy (its SVG text).
|
|
Ours,
|
|
/// Text copied in another app.
|
|
Text,
|
|
/// An image copied in another app.
|
|
Image,
|
|
}
|
|
|
|
const HELD: [Held; 3] = [Held::Ours, Held::Text, Held::Image];
|
|
|
|
#[derive(Default)]
|
|
struct Clip {
|
|
text: Option<String>,
|
|
image: Option<(u32, u32, Vec<u8>)>,
|
|
}
|
|
|
|
/// The desktop clipboard.
|
|
struct Desktop(Rc<RefCell<Clip>>);
|
|
|
|
impl Services for Desktop {
|
|
fn clipboard_set_text(&mut self, s: &str) {
|
|
*self.0.borrow_mut() = Clip { text: Some(s.into()), image: None };
|
|
}
|
|
fn clipboard_get_text(&mut self) -> Option<String> {
|
|
self.0.borrow().text.clone()
|
|
}
|
|
fn clipboard_get_image(&mut self) -> Option<(u32, u32, Vec<u8>)> {
|
|
self.0.borrow().image.clone()
|
|
}
|
|
}
|
|
|
|
struct Win {
|
|
ctx: egui::Context,
|
|
app: FigApp,
|
|
time: f64,
|
|
clip: Rc<RefCell<Clip>>,
|
|
shapes: Vec<Shape>,
|
|
}
|
|
|
|
impl Win {
|
|
fn new() -> Self {
|
|
let mut s = Session::new();
|
|
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
|
|
s.execute("select.none", &json!({})).unwrap();
|
|
let ctx = egui::Context::default();
|
|
let clip = Rc::new(RefCell::new(Clip::default()));
|
|
let app = FigApp::new(s, Box::new(Desktop(clip.clone())));
|
|
let mut w = Self { ctx, app, time: 0.0, clip, shapes: Vec::new() };
|
|
for _ in 0..3 {
|
|
w.frame(vec![]);
|
|
}
|
|
w
|
|
}
|
|
|
|
fn frame(&mut self, events: Vec<Event>) {
|
|
self.frame_with(events, false);
|
|
}
|
|
|
|
/// `take_v`: something ahead of the app (a prototype's key trigger) consumes a V press.
|
|
fn frame_with(&mut self, events: Vec<Event>, take_v: bool) {
|
|
self.time += 1.0 / 60.0;
|
|
let input =
|
|
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1440.0, 900.0))), time: Some(self.time), events, ..Default::default() };
|
|
let app = &mut self.app;
|
|
let mut out = self.ctx.run_ui(input, |ui| {
|
|
if take_v {
|
|
ui.ctx().input_mut(|i| i.consume_key(Modifiers::NONE, Key::V));
|
|
}
|
|
app.ui(ui);
|
|
});
|
|
out.textures_delta.clear();
|
|
self.shapes = out.shapes.into_iter().map(|s| s.shape).collect();
|
|
}
|
|
|
|
fn copy_image_elsewhere(&mut self) {
|
|
*self.clip.borrow_mut() = Clip { text: None, image: Some((2, 2, vec![255; 16])) };
|
|
}
|
|
|
|
/// Cmd/Ctrl+V as egui-winit delivers it: the press brings a paste event when the clipboard
|
|
/// holds text and nothing otherwise; the release comes as a key event.
|
|
fn cmd_v(&mut self, m: Modifiers) {
|
|
let text = self.clip.borrow().text.clone().filter(|t| !t.is_empty());
|
|
self.frame(text.map(Event::Paste).into_iter().collect());
|
|
self.frame(vec![key(Key::V, false, m)]);
|
|
self.frame(vec![]);
|
|
}
|
|
|
|
/// A Cmd/Ctrl+V chord with its modifiers, as egui-winit delivers it: they go down first, the V
|
|
/// press brings a paste event only for text, its release comes as a key event, then they're
|
|
/// let go.
|
|
fn chord(&mut self, m: Modifiers) {
|
|
let text = self.clip.borrow().text.clone().filter(|t| !t.is_empty());
|
|
self.frame(vec![Event::ModifiersChanged(m)]);
|
|
self.frame(text.map(Event::Paste).into_iter().collect());
|
|
self.frame(vec![key(Key::V, false, m)]);
|
|
self.frame(vec![Event::ModifiersChanged(Modifiers::NONE)]);
|
|
}
|
|
|
|
/// A red rectangle A copied in FigCraft, and a grey rectangle B selected; then `held` on the
|
|
/// system clipboard. Returns (A, B).
|
|
fn copy_a_select_b(&mut self, held: Held) -> (NodeId, NodeId) {
|
|
let s = &mut self.app.session;
|
|
let a = s.doc().unwrap().nodes().find(|n| n.name == "Rectangle").unwrap().id;
|
|
s.execute("fill.set", &json!({"ids": [a.0], "color": "#e53935"})).unwrap();
|
|
let b = s.execute("shape.rectangle", &json!({"x": 400, "y": 250, "width": 100, "height": 50})).unwrap();
|
|
let b = NodeId(b["id"].as_u64().unwrap());
|
|
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
|
|
crate::menus::invoke(&mut self.app, "edit.copy");
|
|
self.app.session.execute("edit.copyProperties", &json!({})).unwrap();
|
|
self.app.session.execute("select.set", &json!({"ids": [b.0]})).unwrap();
|
|
match held {
|
|
Held::Ours => {}
|
|
Held::Text => self.clip.borrow_mut().text = Some("hello from another app".into()),
|
|
Held::Image => self.copy_image_elsewhere(),
|
|
}
|
|
(a, b)
|
|
}
|
|
|
|
fn fill(&self, id: NodeId) -> String {
|
|
let n = figcraft_engine::inspect::node_json(self.app.session.doc().unwrap(), id, 0);
|
|
n["fills"][0]["color"].as_str().unwrap_or_default().to_string()
|
|
}
|
|
|
|
/// Top-lefts of the rectangles other than `b`.
|
|
fn rects_besides(&self, b: NodeId) -> Vec<(i64, i64)> {
|
|
let doc = self.app.session.doc().unwrap();
|
|
let mut at: Vec<_> = doc
|
|
.nodes()
|
|
.filter(|n| n.name == "Rectangle" && n.id != b)
|
|
.filter_map(|n| doc.world_bounds(n.id))
|
|
.map(|r| (r.x0.round() as i64, r.y0.round() as i64))
|
|
.collect();
|
|
at.sort();
|
|
at
|
|
}
|
|
|
|
fn named(&self, name: &str) -> usize {
|
|
self.app.session.doc().unwrap().nodes().filter(|n| n.name == name).count()
|
|
}
|
|
|
|
fn texts(&self) -> usize {
|
|
self.app.session.doc().unwrap().nodes().filter(|n| matches!(n.kind, figcraft_doc::NodeKind::Text(_))).count()
|
|
}
|
|
|
|
fn rects(&self) -> usize {
|
|
self.named("Rectangle")
|
|
}
|
|
}
|
|
|
|
// egui sets `command` for Cmd on macOS and for Ctrl elsewhere; macOS Ctrl is a separate key.
|
|
const CMD: Modifiers = if cfg!(target_os = "macos") { Modifiers::COMMAND } else { Modifiers::COMMAND.plus(Modifiers::CTRL) };
|
|
|
|
fn key(key: Key, pressed: bool, modifiers: Modifiers) -> Event {
|
|
Event::Key { key, physical_key: Some(key), pressed, repeat: false, modifiers }
|
|
}
|
|
|
|
#[test]
|
|
fn cmd_v_pastes_an_image_copied_elsewhere() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.cmd_v(CMD);
|
|
assert_eq!(w.named("Pasted image"), 1, "the image was pasted from the key's release");
|
|
w.app.session.execute("select.none", &json!({})).unwrap();
|
|
w.cmd_v(CMD);
|
|
assert_eq!(w.named("Pasted image"), 2, "and again");
|
|
}
|
|
|
|
#[test]
|
|
fn cmd_v_fills_a_selected_oval_without_adding_a_layer() {
|
|
let mut w = Win::new();
|
|
let oval =
|
|
NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap());
|
|
let before = w.app.session.doc().unwrap().node_count();
|
|
let original_fill = w.app.session.doc().unwrap().node(oval).unwrap().fills.clone();
|
|
w.copy_image_elsewhere();
|
|
w.cmd_v(CMD);
|
|
|
|
let doc = w.app.session.doc().unwrap();
|
|
let node = doc.node(oval).unwrap();
|
|
assert_eq!(doc.node_count(), before);
|
|
assert!(matches!(node.kind, figcraft_doc::NodeKind::Ellipse { .. }));
|
|
// Added on top: the oval's own fill stays under it.
|
|
assert_eq!(node.fills.len(), original_fill.len() + 1);
|
|
assert_eq!(node.fills[..original_fill.len()], original_fill[..]);
|
|
assert!(matches!(node.fills.last().unwrap().kind, figcraft_color::PaintKind::Image(_)));
|
|
assert_eq!((node.width, node.height), (120.0, 80.0));
|
|
assert_eq!(w.app.session.selection(), vec![oval]);
|
|
let png = figcraft_engine::cmd::file::export_bytes(doc, &[oval], figcraft_doc::ExportFormat::Png, 1.0).unwrap();
|
|
let pixels = image::load_from_memory(&png).unwrap().to_rgba8();
|
|
assert_eq!(pixels.get_pixel(0, 0)[3], 0, "the picture stays inside the oval");
|
|
assert_eq!(pixels.get_pixel(60, 40)[3], 255, "the oval contains the picture");
|
|
|
|
w.app.session.undo().unwrap();
|
|
assert_eq!(w.app.session.doc().unwrap().node(oval).unwrap().fills, original_fill);
|
|
}
|
|
|
|
#[test]
|
|
fn pasting_an_image_keeps_the_other_fills_variables() {
|
|
let mut w = Win::new();
|
|
let oval =
|
|
NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap());
|
|
w.app.session.execute("variable.createCollection", &json!({"name": "Theme"})).unwrap();
|
|
w.app.session.execute("variable.create", &json!({"collection": "Theme", "name": "brand", "type": "color", "value": "#FF0000"})).unwrap();
|
|
w.app.session.execute("variable.bind", &json!({"ids": [oval.0], "field": "fill", "variable": "brand"})).unwrap();
|
|
assert!(!w.app.session.doc().unwrap().node(oval).unwrap().bound.is_empty());
|
|
|
|
w.copy_image_elsewhere();
|
|
w.cmd_v(CMD);
|
|
let node = w.app.session.doc().unwrap().node(oval).unwrap();
|
|
assert!(matches!(node.fills.last().unwrap().kind, figcraft_color::PaintKind::Image(_)));
|
|
assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Solid { .. }));
|
|
assert!(!node.bound.is_empty(), "the colour fill under the image keeps its variable");
|
|
}
|
|
|
|
/// An image layer copied in FigCraft pastes as a layer, a selected shape or not.
|
|
#[test]
|
|
fn an_image_layer_copied_in_figcraft_pastes_as_a_layer() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.cmd_v(CMD);
|
|
let source = w.app.session.selection()[0];
|
|
crate::menus::invoke(&mut w.app, "edit.copy");
|
|
|
|
let oval =
|
|
NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap());
|
|
let fills = w.app.session.doc().unwrap().node(oval).unwrap().fills.clone();
|
|
let before = w.app.session.doc().unwrap().node_count();
|
|
w.cmd_v(CMD);
|
|
|
|
let doc = w.app.session.doc().unwrap();
|
|
assert_eq!(doc.node_count(), before + 1);
|
|
assert!(doc.contains(source));
|
|
assert_eq!(doc.node(oval).unwrap().fills, fills);
|
|
assert_ne!(w.app.session.selection(), vec![oval]);
|
|
}
|
|
|
|
#[test]
|
|
fn cmd_released_before_v_still_pastes_the_image() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.cmd_v(Modifiers::NONE);
|
|
assert_eq!(w.named("Pasted image"), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn text_pastes_once_from_its_paste_event() {
|
|
let mut w = Win::new();
|
|
*w.clip.borrow_mut() = Clip { text: Some("hello from another app".into()), image: None };
|
|
w.cmd_v(CMD);
|
|
assert_eq!(w.texts(), 1, "the paste event pasted the text, the release nothing more");
|
|
assert_eq!(w.rects(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn text_pastes_once_when_another_key_is_let_go_first() {
|
|
let mut w = Win::new();
|
|
*w.clip.borrow_mut() = Clip { text: Some("hello".into()), image: None };
|
|
// Cmd+A then Cmd+V typed fast: A comes up after V went down.
|
|
w.frame(vec![key(Key::A, true, CMD)]);
|
|
w.frame(vec![Event::Paste("hello".into())]);
|
|
w.frame(vec![key(Key::A, false, CMD)]);
|
|
w.frame(vec![key(Key::V, false, CMD)]);
|
|
w.frame(vec![]);
|
|
assert_eq!(w.texts(), 1);
|
|
assert_eq!(w.rects(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_v_press_consumed_elsewhere_does_not_paste_on_release() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.frame_with(vec![key(Key::V, true, Modifiers::NONE)], true);
|
|
w.frame(vec![key(Key::V, false, Modifiers::NONE)]);
|
|
w.frame(vec![]);
|
|
assert_eq!(w.named("Pasted image"), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn the_browser_sequence_pastes_once() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.frame(vec![key(Key::V, true, CMD)]);
|
|
w.frame(vec![key(Key::V, false, CMD)]);
|
|
w.frame(vec![]);
|
|
assert_eq!(w.named("Pasted image"), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn v_alone_pastes_nothing() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.frame(vec![key(Key::V, true, Modifiers::NONE)]);
|
|
w.frame(vec![key(Key::V, false, Modifiers::NONE)]);
|
|
assert_eq!(w.named("Pasted image"), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_paste_event_from_another_key_does_not_block_the_next_cmd_v() {
|
|
let mut w = Win::new();
|
|
*w.clip.borrow_mut() = Clip { text: Some("text".into()), image: None };
|
|
// Shift+Insert on Windows.
|
|
w.frame(vec![Event::Paste("text".into())]);
|
|
w.frame(vec![key(Key::Insert, false, Modifiers::SHIFT)]);
|
|
w.app.session.execute("select.none", &json!({})).unwrap();
|
|
w.copy_image_elsewhere();
|
|
w.cmd_v(CMD);
|
|
assert_eq!(w.named("Pasted image"), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_v_press_lost_to_another_window_does_not_block_the_next_cmd_v() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.frame(vec![key(Key::V, true, Modifiers::NONE), Event::WindowFocused(false)]);
|
|
w.frame(vec![Event::WindowFocused(true)]);
|
|
w.cmd_v(CMD);
|
|
assert_eq!(w.named("Pasted image"), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn v_held_while_the_window_was_away_pastes_nothing_on_release() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
// Pressed in another window: only the focus and the release arrive here.
|
|
w.frame(vec![Event::WindowFocused(true)]);
|
|
w.frame(vec![key(Key::V, false, Modifiers::NONE)]);
|
|
w.frame(vec![]);
|
|
assert_eq!(w.named("Pasted image"), 0);
|
|
w.cmd_v(CMD);
|
|
assert_eq!(w.named("Pasted image"), 1, "the next Cmd+V still pastes");
|
|
}
|
|
|
|
#[test]
|
|
fn cmd_v_in_a_panel_field_does_not_paste_on_the_canvas() {
|
|
let mut w = Win::new();
|
|
w.copy_image_elsewhere();
|
|
w.frame(vec![]);
|
|
w.app.field_focus = true;
|
|
w.frame(vec![key(Key::V, false, CMD)]);
|
|
w.frame(vec![]);
|
|
assert_eq!(w.named("Pasted image"), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn paste_here_prefers_a_newer_image_over_copied_layers() {
|
|
let mut w = Win::new();
|
|
w.app.session.execute("select.all", &json!({})).unwrap();
|
|
crate::menus::invoke(&mut w.app, "edit.copy");
|
|
assert!(!w.app.session.clipboard.nodes.is_empty(), "layers copied");
|
|
w.app.session.execute("select.none", &json!({})).unwrap();
|
|
w.copy_image_elsewhere();
|
|
let at = Pos2::new(900.0, 600.0);
|
|
for pressed in [true, false] {
|
|
w.frame(vec![
|
|
Event::PointerMoved(at),
|
|
Event::PointerButton { pos: at, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE },
|
|
]);
|
|
}
|
|
w.frame(vec![]);
|
|
let item = crate::live_tests::text_center(&w.shapes, "Paste here");
|
|
for pressed in [true, false] {
|
|
w.frame(vec![
|
|
Event::PointerMoved(item),
|
|
Event::PointerButton { pos: item, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE },
|
|
]);
|
|
}
|
|
w.frame(vec![]);
|
|
assert_eq!(w.named("Pasted image"), 1, "the newer image was pasted");
|
|
assert_eq!(w.rects(), 1, "not the copied rectangle");
|
|
let p = w.app.to_page(at);
|
|
let doc = w.app.session.doc().unwrap();
|
|
let img = doc.nodes().find(|n| n.name == "Pasted image").unwrap();
|
|
let b = doc.world_bounds(img.id).unwrap();
|
|
assert!((b.x0 - p.x).abs() < 0.01 && (b.y0 - p.y).abs() < 0.01, "top-left where the menu opened: {b:?} vs {p:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn shift_cmd_v_pastes_over_the_selection_whatever_the_clipboard_holds() {
|
|
for held in HELD {
|
|
let mut w = Win::new();
|
|
let (_, b) = w.copy_a_select_b(held);
|
|
w.chord(CMD.plus(Modifiers::SHIFT));
|
|
assert_eq!(w.rects_besides(b), [(10, 10), (400, 250)], "{held:?}: A's copy at B's top-left, nothing else");
|
|
assert_eq!(w.texts(), 0, "{held:?}: no text pasted");
|
|
assert_eq!(w.named("Pasted image"), 0, "{held:?}: no image pasted");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn alt_cmd_v_pastes_properties_whatever_the_clipboard_holds() {
|
|
for held in HELD {
|
|
let mut w = Win::new();
|
|
let (_, b) = w.copy_a_select_b(held);
|
|
w.chord(CMD.plus(Modifiers::ALT));
|
|
assert_eq!(w.fill(b), "#E53935", "{held:?}: B took A's fill");
|
|
assert_eq!(w.rects_besides(b), [(10, 10)], "{held:?}: no layer pasted");
|
|
assert_eq!(w.texts(), 0, "{held:?}: no text pasted");
|
|
assert_eq!(w.named("Pasted image"), 0, "{held:?}: no image pasted");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_chord_let_go_within_one_frame_still_reads_its_modifiers() {
|
|
let mut w = Win::new();
|
|
let (_, b) = w.copy_a_select_b(Held::Text);
|
|
let m = CMD.plus(Modifiers::SHIFT);
|
|
w.frame(vec![
|
|
Event::ModifiersChanged(m),
|
|
Event::Paste("hello from another app".into()),
|
|
key(Key::V, false, m),
|
|
Event::ModifiersChanged(Modifiers::NONE),
|
|
]);
|
|
w.frame(vec![]);
|
|
assert_eq!(w.rects_besides(b), [(10, 10), (400, 250)]);
|
|
assert_eq!(w.texts(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cmd_let_go_before_v_still_pastes_over() {
|
|
let mut w = Win::new();
|
|
let (_, b) = w.copy_a_select_b(Held::Image);
|
|
w.frame(vec![Event::ModifiersChanged(CMD.plus(Modifiers::SHIFT))]);
|
|
w.frame(vec![Event::ModifiersChanged(Modifiers::SHIFT)]);
|
|
w.frame(vec![key(Key::V, false, Modifiers::SHIFT)]);
|
|
w.frame(vec![Event::ModifiersChanged(Modifiers::NONE)]);
|
|
assert_eq!(w.rects_besides(b), [(10, 10), (400, 250)], "pasted over, not flipped or a plain paste");
|
|
assert_eq!(w.named("Pasted image"), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_held_chord_pastes_over_once() {
|
|
let mut w = Win::new();
|
|
let (_, b) = w.copy_a_select_b(Held::Text);
|
|
let m = CMD.plus(Modifiers::SHIFT);
|
|
w.frame(vec![Event::ModifiersChanged(m)]);
|
|
// Auto-repeat: egui-winit sends a paste event for every repeated press.
|
|
for _ in 0..3 {
|
|
w.frame(vec![Event::Paste("hello from another app".into())]);
|
|
}
|
|
w.frame(vec![key(Key::V, false, m)]);
|
|
w.frame(vec![Event::ModifiersChanged(Modifiers::NONE)]);
|
|
assert_eq!(w.rects_besides(b), [(10, 10), (400, 250)]);
|
|
assert_eq!(w.texts(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn shift_insert_still_pastes_text() {
|
|
let mut w = Win::new();
|
|
let (_, b) = w.copy_a_select_b(Held::Text);
|
|
w.frame(vec![Event::ModifiersChanged(Modifiers::SHIFT), Event::Paste("hello from another app".into())]);
|
|
w.frame(vec![key(Key::Insert, false, Modifiers::SHIFT), Event::ModifiersChanged(Modifiers::NONE)]);
|
|
assert_eq!(w.texts(), 1);
|
|
assert_eq!(w.rects_besides(b), [(10, 10)]);
|
|
}
|
|
|
|
#[test]
|
|
fn cmd_v_after_a_paste_chord_is_a_plain_paste_again() {
|
|
let mut w = Win::new();
|
|
let (_, b) = w.copy_a_select_b(Held::Text);
|
|
w.chord(CMD.plus(Modifiers::SHIFT));
|
|
w.chord(CMD);
|
|
assert_eq!(w.texts(), 1, "the text was pasted by the plain Cmd+V");
|
|
assert_eq!(w.rects_besides(b), [(10, 10), (400, 250)], "Shift+Cmd+V pasted over once");
|
|
}
|