mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 19:06:47 -06:00
fix(ui): Shift+Cmd/Ctrl+V pastes over the selection and Alt+Cmd/Ctrl+V pastes properties (#716)
egui-winit swallows every Cmd/Ctrl+V press, Shift or Alt held or not, and sends a paste event only for text. Shift+Cmd+V and Alt+Cmd+V became a plain paste (text) or did nothing (image). The keymap now decides what a swallowed chord means, from the modifiers tracked in the raw events: its paste event or its release runs paste over or paste properties instead. In the browser the key press runs the command, and the paste event no longer pastes its text as well. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4adbd6cee9
commit
9f52819fe9
@@ -502,6 +502,8 @@ pub struct UiState {
|
||||
pub paste_event: bool,
|
||||
/// The window got the focus back since the last V release.
|
||||
pub refocused: bool,
|
||||
/// The modifiers held, as of the last raw event that told: a paste event doesn't carry them.
|
||||
pub held: egui::Modifiers,
|
||||
/// When (UI clock, seconds) open documents next get an autosave checkpoint.
|
||||
pub next_autosave: f64,
|
||||
/// With auto-save on: when (UI clock, seconds) files with unsaved changes are next saved, and
|
||||
@@ -598,6 +600,7 @@ impl Default for UiState {
|
||||
v_pressed: false,
|
||||
paste_event: false,
|
||||
refocused: false,
|
||||
held: egui::Modifiers::NONE,
|
||||
next_autosave: 0.0,
|
||||
auto_save_at: None,
|
||||
auto_saving: Vec::new(),
|
||||
@@ -1294,7 +1297,7 @@ impl FigApp {
|
||||
// even if it lets go during the frame (Enter commits it), or as it began (egui drops the
|
||||
// focus on Escape before the frame, which cancels the field's edit and nothing else).
|
||||
let typing = std::mem::take(&mut self.field_focus) || a11y::field_focused(&ctx);
|
||||
let paste_release = shortcuts::swallowed_paste(self, &ctx);
|
||||
let swallowed = shortcuts::swallowed_paste(self, &ctx);
|
||||
// A dialog that closed acts first, in the window it was opened from.
|
||||
self.poll_dialog(&ctx);
|
||||
// Control requests run first so their results reflect this frame.
|
||||
@@ -1410,7 +1413,7 @@ impl FigApp {
|
||||
branch_ui::keys(self, &ctx);
|
||||
live::dialog_keys(self, &ctx);
|
||||
dialogs::keys(self, &ctx);
|
||||
shortcuts::handle(self, &ctx, typing, paste_release);
|
||||
shortcuts::handle(self, &ctx, typing, swallowed);
|
||||
menus::popups(self, &ctx);
|
||||
variable_ui::picker(self, &ctx);
|
||||
style_picker::show(self, &ctx);
|
||||
|
||||
@@ -6,11 +6,25 @@ 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>,
|
||||
@@ -87,6 +101,55 @@ impl Win {
|
||||
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()
|
||||
}
|
||||
@@ -256,3 +319,92 @@ fn paste_here_prefers_a_newer_image_over_copied_layers() {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -94,8 +94,8 @@ fn cached_tip(label: &'static str, key: Option<&'static str>) -> &'static str {
|
||||
|
||||
/// Run the shortcuts this frame's keys press. `typing`: a field had the keyboard when the frame
|
||||
/// began.
|
||||
/// `paste_release`: this frame released a Cmd/Ctrl+V the shell swallowed ([`swallowed_paste`]).
|
||||
pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, paste_release: bool) {
|
||||
/// `swallowed`: what this frame's Cmd/Ctrl+V chords the shell swallowed ask for ([`swallowed_paste`]).
|
||||
pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, swallowed: Swallowed) {
|
||||
note_platform(ctx);
|
||||
// Text fields in panels own the keyboard (also the key that ends their editing).
|
||||
if typing || crate::a11y::field_focused(ctx) || app.ui.palette.is_some() {
|
||||
@@ -113,16 +113,18 @@ pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, paste_release: bool
|
||||
// An image-only clipboard brings no paste event: paste from the key then. The browser sends
|
||||
// the press; the desktop shell swallows it and sends only the release.
|
||||
let paste_event = events.iter().any(|e| matches!(e, Event::Paste(_)));
|
||||
let paste_key = events.iter().any(|e| matches!(e, Event::Key { key: Key::V, pressed: true, repeat: false, modifiers, .. } if modifiers.command && !modifiers.shift && !modifiers.alt));
|
||||
if (paste_key && !paste_event) || paste_release {
|
||||
crate::menus::paste(app, None, None);
|
||||
let paste_key = events.iter().any(|e| matches!(e, Event::Key { key: Key::V, pressed: true, repeat: false, modifiers, .. } if modifiers.command && v_binding(*modifiers) == Some("edit.paste")));
|
||||
match swallowed.run.or((paste_key && !paste_event).then_some("edit.paste")) {
|
||||
Some("edit.paste") => crate::menus::paste(app, None, None),
|
||||
Some(id) => run_binding(app, id),
|
||||
None => {}
|
||||
}
|
||||
let now = ctx.input(|i| i.time);
|
||||
for ev in &events {
|
||||
match ev {
|
||||
Event::Copy => crate::menus::invoke(app, "edit.copy"),
|
||||
Event::Cut => crate::menus::invoke(app, "edit.cut"),
|
||||
Event::Paste(text) => crate::menus::paste(app, Some(text), None),
|
||||
Event::Paste(text) if !swallowed.drop_text => crate::menus::paste(app, Some(text), None),
|
||||
Event::Key { key, physical_key, pressed: true, modifiers, repeat }
|
||||
if key_event(app, *key, *physical_key, *modifiers, *repeat, now, ctx) =>
|
||||
{
|
||||
@@ -133,23 +135,59 @@ pub fn handle(app: &mut FigApp, ctx: &Context, typing: bool, paste_release: bool
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this frame releases a Cmd/Ctrl+V whose press never arrived and that brought no paste
|
||||
/// event: egui-winit swallows the press and sends a paste event only when the clipboard holds
|
||||
/// text, so an image on the clipboard leaves just this release. Call at the start of every frame:
|
||||
/// it reads the raw events, which keys consumed by panels or the presenter still carry.
|
||||
pub fn swallowed_paste(app: &mut FigApp, ctx: &Context) -> bool {
|
||||
/// The command Cmd/Ctrl+V runs with `m` held too: `edit.paste`, `edit.pasteOver` (Shift),
|
||||
/// `edit.pasteProperties` (Alt), or `None` for a chord nothing is bound to. The keymap decides,
|
||||
/// whether the key press arrived (browser) or only a paste event and the release (desktop).
|
||||
fn v_binding(m: Modifiers) -> Option<&'static str> {
|
||||
lookup(keymap::Context::Canvas, Key::V, Some(Key::V), Modifiers { command: true, ..m }).map(|b| b.id)
|
||||
}
|
||||
|
||||
/// What this frame's Cmd/Ctrl+V chords ask for when the desktop shell swallowed their press
|
||||
/// ([`swallowed_paste`]).
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Swallowed {
|
||||
/// The command to run ([`v_binding`]).
|
||||
pub run: Option<&'static str>,
|
||||
/// This frame's paste event belongs to another command or an unbound chord: its text isn't
|
||||
/// pasted.
|
||||
pub drop_text: bool,
|
||||
}
|
||||
|
||||
/// egui-winit swallows every Cmd/Ctrl+V press, Shift or Alt held or not, and sends a paste event
|
||||
/// only when the clipboard holds text. So a chord brings a paste event (text) or just its release
|
||||
/// (an image, or nothing). The browser sends the press as well, which runs the command itself.
|
||||
/// Call at the start of every frame: it reads the raw events, which keys consumed by panels or the
|
||||
/// presenter still carry.
|
||||
pub fn swallowed_paste(app: &mut FigApp, ctx: &Context) -> Swallowed {
|
||||
let ui = &mut app.ui;
|
||||
let mut paste = false;
|
||||
let mut out = Swallowed::default();
|
||||
ctx.input(|i| {
|
||||
for ev in &i.raw.events {
|
||||
if let Event::Key { modifiers, .. } = ev {
|
||||
ui.held = *modifiers;
|
||||
}
|
||||
match ev {
|
||||
Event::ModifiersChanged(m) => ui.held = *m,
|
||||
// Without Cmd/Ctrl held it's another paste key (Shift+Insert, a Paste key).
|
||||
Event::Paste(_) if ui.held.command => {
|
||||
let id = v_binding(ui.held);
|
||||
if id != Some("edit.paste") {
|
||||
out.drop_text = true;
|
||||
// Held down, the chord repeats its paste event: run once, as keys do.
|
||||
if !ui.v_pressed && !ui.paste_event {
|
||||
out.run = out.run.or(id);
|
||||
}
|
||||
}
|
||||
ui.paste_event = true;
|
||||
}
|
||||
Event::Paste(_) => ui.paste_event = true,
|
||||
Event::Key { key: Key::V, pressed: true, .. } => ui.v_pressed = true,
|
||||
Event::Key { key: Key::V, pressed: false, modifiers, .. } => {
|
||||
// Shift or Alt with it is another command (paste over, paste properties). A V
|
||||
// held while the window was away comes back as a release alone: without Cmd
|
||||
// held, that's no paste.
|
||||
paste = !ui.v_pressed && !ui.paste_event && !modifiers.shift && !modifiers.alt && (modifiers.command || !ui.refocused);
|
||||
// Cmd was down when V went down, maybe let go first. A V held while the window
|
||||
// was away comes back as a release alone: without Cmd held, that's no paste.
|
||||
if !ui.v_pressed && !ui.paste_event && (modifiers.command || !ui.refocused) {
|
||||
out.run = out.run.or(v_binding(*modifiers));
|
||||
}
|
||||
ui.v_pressed = false;
|
||||
ui.paste_event = false;
|
||||
ui.refocused = false;
|
||||
@@ -160,13 +198,14 @@ pub fn swallowed_paste(app: &mut FigApp, ctx: &Context) -> bool {
|
||||
Event::WindowFocused(false) => {
|
||||
ui.v_pressed = false;
|
||||
ui.paste_event = false;
|
||||
ui.held = Modifiers::NONE;
|
||||
}
|
||||
Event::WindowFocused(true) => ui.refocused = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
paste
|
||||
out
|
||||
}
|
||||
|
||||
/// Run the command a shortcut is bound to, with the keyboard's own twists (Escape steps out
|
||||
|
||||
@@ -86,6 +86,26 @@ impl Page {
|
||||
self.frame(vec![Self::key(Key::V, false)]);
|
||||
}
|
||||
|
||||
/// Cmd+V with `m` added, as the browser delivers it: the key press, then the paste event.
|
||||
fn chord(&mut self, m: Modifiers) {
|
||||
let m = m | Modifiers::MAC_CMD | Modifiers::COMMAND;
|
||||
let v = |pressed| Event::Key { key: Key::V, physical_key: Some(Key::V), pressed, repeat: false, modifiers: m };
|
||||
let text = self.clipboard.clone();
|
||||
self.frame(vec![Event::ModifiersChanged(m), v(true), Event::Paste(text)]);
|
||||
self.frame(vec![v(false)]);
|
||||
self.frame(vec![Event::ModifiersChanged(Modifiers::NONE)]);
|
||||
}
|
||||
|
||||
/// Copy the selected rectangle A (made red) and its properties, then select a new rectangle B.
|
||||
fn copy_a_select_b(&mut self) -> figcraft_doc::NodeId {
|
||||
self.app.session.execute("fill.set", &json!({"color": "#e53935"})).unwrap();
|
||||
self.copy();
|
||||
self.app.session.execute("edit.copyProperties", &json!({})).unwrap();
|
||||
let b = self.app.session.execute("shape.rectangle", &json!({"x": 400, "y": 250, "width": 100, "height": 50})).unwrap();
|
||||
self.app.session.execute("select.set", &json!({"ids": [b["id"]]})).unwrap();
|
||||
figcraft_doc::NodeId(b["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn count(&self, kind: fn(&NodeKind) -> bool) -> usize {
|
||||
self.app.session.doc().unwrap().nodes().filter(|n| kind(&n.kind)).count()
|
||||
}
|
||||
@@ -121,6 +141,52 @@ fn text_copied_elsewhere_after_our_copy_still_pastes_as_text() {
|
||||
assert_eq!(p.count(text), 1, "newer text from elsewhere becomes a text layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cmd_v_pastes_over_once_and_no_text() {
|
||||
for foreign in [false, true] {
|
||||
let mut p = Page::new();
|
||||
let b = p.copy_a_select_b();
|
||||
if foreign {
|
||||
p.clipboard = "hello from another app".into();
|
||||
}
|
||||
p.chord(Modifiers::SHIFT);
|
||||
assert_eq!(p.count(rect), 3, "foreign text {foreign}: one copy pasted over B");
|
||||
assert_eq!(p.count(text), 0, "foreign text {foreign}: the paste event pasted nothing");
|
||||
let doc = p.app.session.doc().unwrap();
|
||||
let over = doc.nodes().filter(|n| n.id != b).filter_map(|n| doc.world_bounds(n.id)).any(|r| r.x0 == 400.0 && r.y0 == 250.0);
|
||||
assert!(over, "foreign text {foreign}: the copy sits at B's top-left");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_cmd_v_pastes_properties_once_and_no_text() {
|
||||
for foreign in [false, true] {
|
||||
let mut p = Page::new();
|
||||
let b = p.copy_a_select_b();
|
||||
if foreign {
|
||||
p.clipboard = "hello from another app".into();
|
||||
}
|
||||
p.chord(Modifiers::ALT);
|
||||
assert_eq!(p.count(rect), 2, "foreign text {foreign}: no layer pasted");
|
||||
assert_eq!(p.count(text), 0, "foreign text {foreign}: the paste event pasted nothing");
|
||||
let fill = figcraft_engine::inspect::node_json(p.app.session.doc().unwrap(), b, 0)["fills"][0]["color"].clone();
|
||||
assert_eq!(fill, "#E53935", "foreign text {foreign}: B took A's fill");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_chord_tapped_within_one_frame_runs_once() {
|
||||
let mut p = Page::new();
|
||||
p.copy_a_select_b();
|
||||
let m = Modifiers::SHIFT | Modifiers::MAC_CMD | Modifiers::COMMAND;
|
||||
let v = |pressed| Event::Key { key: Key::V, physical_key: Some(Key::V), pressed, repeat: false, modifiers: m };
|
||||
let clip = p.clipboard.clone();
|
||||
p.frame(vec![Event::ModifiersChanged(m), v(true), Event::Paste(clip), v(false), Event::ModifiersChanged(Modifiers::NONE)]);
|
||||
p.frame(vec![]);
|
||||
assert_eq!(p.count(rect), 3, "one copy pasted over B");
|
||||
assert_eq!(p.count(text), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_as_png_puts_an_image_on_the_clipboard() {
|
||||
let mut p = Page::new();
|
||||
|
||||
Reference in New Issue
Block a user