Type: a copy elsewhere during a refused write wins the paste

When another app copies while VectorCraft's write is trying to open the
clipboard and the write then fails, Paste takes the other app's copy. A
change during the failed write counts as the write's own only when the
clipboard then holds this copy's text, or no text and either nothing to
paste or one of this copy's formats.
This commit is contained in:
dotCooCoo
2026-11-05 08:17:22 +07:00
committed by bflatastic
parent 644acb89cc
commit 8a81c95685
3 changed files with 89 additions and 8 deletions
+12 -2
View File
@@ -208,15 +208,25 @@ impl VectorcraftApp {
let formats = r.and_then(|_| made.try_recv().map_err(|e| e.to_string()));
let written = formats.and_then(|(f, warnings)| {
let Some(cb) = app.services.system_clipboard.as_mut() else { return Ok(warnings) };
let before_write = Held::now(cb.as_mut());
// Nothing copied elsewhere since the Copy or Cut.
let current = before.as_ref().is_some_and(|b| *b == Held::now(cb.as_mut()));
let current = before.as_ref() == Some(&before_write);
if let Err(e) = cb.write(&f) {
// Another app may hold the clipboard open. It keeps what it held (or what the
// failed write left), and a paste into text takes this copy's text until
// something else is copied.
let held = Held::now(cb.as_mut());
let text = f.iter().find(|t| t.mime == TEXT).map(|t| String::from_utf8_lossy(&t.data).into_owned());
app.clipboard_publishing.unwritten = text.filter(|_| current).map(|text| (held, text));
// A change during the write is the failed write emptying the clipboard and
// writing some of the formats, or another app's copy: the clipboard then holds
// nothing to paste, this copy's text, or no text and one of this copy's
// formats only after the former.
let left_by_write = held == before_write
|| match cb.read(&[TEXT]) {
Some(now) => text.as_deref().is_some_and(|ours| now.data == ours.as_bytes()),
None => !cb.has(&PASTE_ORDER) || f.iter().any(|ours| cb.read(&[ours.mime]).is_some_and(|now| now.data == ours.data)),
};
app.clipboard_publishing.unwritten = text.filter(|_| current && left_by_write).map(|text| (held, text));
return Err(e);
}
Ok(warnings)
+45 -2
View File
@@ -411,9 +411,17 @@ fn a_copy_elsewhere_before_a_refused_write_wins_the_type_paste() {
#[test]
fn type_paste_keeps_a_copy_whose_write_failed_after_emptying_the_clipboard() {
let _guard = TEST_CLIP_LOCK.lock().unwrap();
assert_eq!(paste_after_a_write_that_failed_after_emptying(false), "Own: Hello");
// The formats other than text were written before the failure.
assert_eq!(paste_after_a_write_that_failed_after_emptying(true), "Own: Hello");
}
/// Copy "Hello" in the Type tool with a write that empties the clipboard and fails (`keep_others`:
/// after writing the formats other than text), then paste into "Own: ".
fn paste_after_a_write_that_failed_after_emptying(keep_others: bool) -> String {
let (mut app, board) = super::sysclip::app();
super::sysclip::copy_elsewhere(&board, vec![(vectorcraft_engine::cmd::clipboard::TEXT, b"Old".to_vec())]);
super::sysclip::fail_writes_after_emptying(&board);
super::sysclip::fail_writes_after_emptying(&board, keep_others);
let source = run(&mut app, "text.create", json!({"x": 20, "y": 40, "text": "Hello", "size": 36, "color": "#ff0000"}))["id"].as_u64().unwrap();
let destination =
run(&mut app, "text.create", json!({"x": 20, "y": 60, "text": "Own: ", "size": 12, "color": "#000000"}))["id"].as_u64().unwrap();
@@ -429,7 +437,42 @@ fn type_paste_keeps_a_copy_whose_write_failed_after_emptying_the_clipboard() {
crate::panels::character::intercept_text_command(&mut app, "edit.paste");
let node = app.session.doc().unwrap().doc.node(NodeId(destination)).unwrap();
let vectorcraft_doc::NodeKind::Text(t) = &node.kind else { panic!() };
assert_eq!(t.plain_text(), "Own: Hello");
t.plain_text()
}
#[test]
fn a_copy_elsewhere_during_a_refused_write_wins_the_type_paste() {
let _guard = TEST_CLIP_LOCK.lock().unwrap();
use vectorcraft_engine::cmd::clipboard::{PNG, TEXT};
assert_eq!(paste_after_a_copy_during_a_refused_write(TEXT, b"Race".to_vec(), false), "Own: Race");
// Its other formats may match this copy's; its text still differs.
assert_eq!(paste_after_a_copy_during_a_refused_write(TEXT, b"Race".to_vec(), true), "Own: Race");
// A picture has no text to paste into type.
assert_eq!(paste_after_a_copy_during_a_refused_write(PNG, super::sysclip::blue_png(), false), "Own: ");
}
/// Copy in the Type tool while another app copies `data` of type `mime` (with `our_formats`, also
/// the copy's formats other than text) and the write fails, then paste into "Own: ".
fn paste_after_a_copy_during_a_refused_write(mime: &'static str, data: Vec<u8>, our_formats: bool) -> String {
let (mut app, board) = super::sysclip::app();
super::sysclip::copy_elsewhere(&board, vec![(vectorcraft_engine::cmd::clipboard::TEXT, b"Old".to_vec())]);
super::sysclip::copy_during_next_write(&board, mime, data, our_formats);
let source = run(&mut app, "text.create", json!({"x": 20, "y": 40, "text": "Hello", "size": 36, "color": "#ff0000"}))["id"].as_u64().unwrap();
let destination =
run(&mut app, "text.create", json!({"x": 20, "y": 60, "text": "Own: ", "size": 12, "color": "#000000"}))["id"].as_u64().unwrap();
let v = app.view_info();
app.session.select_tool("type", v).unwrap();
app.session.set_tool_option("textSelection", &json!({"id": source, "start": 0, "end": 5}));
crate::panels::character::intercept_text_command(&mut app, "edit.copy");
crate::background::wait_all(&mut app);
app.session.select_tool("selection", v).unwrap();
app.session.select_tool("type", v).unwrap();
app.session.set_tool_option("textSelection", &json!({"id": destination, "start": 5, "end": 5}));
crate::panels::character::intercept_text_command(&mut app, "edit.paste");
let node = app.session.doc().unwrap().doc.node(NodeId(destination)).unwrap();
let vectorcraft_doc::NodeKind::Text(t) = &node.kind else { panic!() };
t.plain_text()
}
/// https://helpx.adobe.com/illustrator/using/format-text.html
+32 -4
View File
@@ -31,6 +31,13 @@ pub(super) struct Board {
uncounted: bool,
/// The app's writes empty the clipboard and then fail.
partial: bool,
/// With `partial`, the formats other than text are written before the failure.
partial_keeps_others: bool,
/// Another app copies this while the app's write tries to open the clipboard, and the write
/// fails.
race: Option<Flavour>,
/// With `race`, the other app's copy also carries the app's formats other than text.
race_with_our_formats: bool,
}
struct Fake(Rc<RefCell<Board>>);
@@ -52,8 +59,17 @@ impl SystemClipboard for Fake {
if b.locked {
return Err("another app has the clipboard open".into());
}
if let Some(copied) = b.race.take() {
b.flavours = vec![copied];
if b.race_with_our_formats {
b.flavours.extend(flavours.iter().filter(|f| f.mime != TEXT).cloned());
}
b.ours = false;
b.changes += 1;
return Err("another app has the clipboard open".into());
}
if b.partial {
b.flavours.clear();
b.flavours = if b.partial_keeps_others { flavours.iter().filter(|f| f.mime != TEXT).cloned().collect() } else { vec![] };
b.changes += 1;
return Err("a format could not be written".into());
}
@@ -103,9 +119,21 @@ pub(super) fn without_change_count(board: &Rc<RefCell<Board>>) {
board.borrow_mut().uncounted = true;
}
/// The app's writes empty the clipboard and then fail, as when one format can't be written.
pub(super) fn fail_writes_after_emptying(board: &Rc<RefCell<Board>>) {
board.borrow_mut().partial = true;
/// The app's writes empty the clipboard and then fail, as when one format can't be written; with
/// `keep_others`, the formats other than text are written.
pub(super) fn fail_writes_after_emptying(board: &Rc<RefCell<Board>>, keep_others: bool) {
let mut b = board.borrow_mut();
b.partial = true;
b.partial_keeps_others = keep_others;
}
/// Another app copies `data` of type `mime` while the app's next write tries to open the
/// clipboard, and that write fails. With `our_formats`, its copy also carries the app's formats
/// other than text.
pub(super) fn copy_during_next_write(board: &Rc<RefCell<Board>>, mime: &'static str, data: Vec<u8>, our_formats: bool) {
let mut b = board.borrow_mut();
b.race = Some(Flavour { mime, data });
b.race_with_our_formats = our_formats;
}
pub(super) fn run(app: &mut VectorcraftApp, id: &str, p: Value) -> Value {