Merge pull request #5696 from dotCooCoo/fix/external-plain-text-paste-style

Use destination style for external same-text paste on Windows
This commit is contained in:
BFlat
2026-11-05 04:36:56 +07:00
committed by GitHub
4 changed files with 66 additions and 17 deletions
+4 -12
View File
@@ -176,8 +176,7 @@ mod win {
use clipboard_win::{formats, options::NoClear, raw};
use super::{
BITMAP, ClipboardRead, Cursor, Flavour, PNG, SVG, SystemClipboard, TEXT, copied_file, decode_png, dib_image, png_flavour, text_of,
windows_formats,
BITMAP, ClipboardRead, Cursor, Flavour, PNG, SVG, SystemClipboard, TEXT, copied_file, decode_png, dib_image, png_flavour, windows_formats,
};
/// The device-independent bitmaps, the one that carries alpha first. Each is there whenever the
@@ -211,8 +210,6 @@ mod win {
pub struct Clipboard {
/// The clipboard's sequence number right after our last write.
seq: Option<NonZeroU32>,
/// The text of our last write.
text: Option<String>,
}
impl SystemClipboard for Clipboard {
@@ -243,18 +240,13 @@ mod win {
}
}
self.seq = raw::seq_num();
self.text = text_of(flavours);
result
}
fn holds_ours(&mut self) -> bool {
if self.seq.is_some() && raw::seq_num() == self.seq {
return true;
}
// A clipboard manager may have copied our data again as its own.
let Some(ours) = self.text.as_deref() else { return false };
let Ok(_open) = clipboard_win::Clipboard::new_attempts(10) else { return false };
read_one(TEXT).and_then(ClipboardRead::flavour).is_some_and(|f| f.data == ours.as_bytes())
// The same text copied later by another app is a different clipboard entry. A
// clipboard manager that republishes our data is also a new entry.
self.seq.is_some() && raw::seq_num() == self.seq
}
fn read(&mut self, mimes: &[&'static str]) -> Option<Flavour> {
+15 -5
View File
@@ -590,18 +590,28 @@ fn text_key(app: &mut VectorcraftApp, k: ToolKey, m: Mods) {
}
fn text_paste(app: &mut VectorcraftApp, s: Option<String>, plain: bool) {
let text_from_clip = CLIP.lock().unwrap_or_else(|e| e.into_inner()).clone();
let s = s.unwrap_or(text_from_clip);
let s = match s {
Some(text) => text,
None if app.services.system_clipboard.is_some() => app.written_clipboard_text().unwrap_or_default(),
None => CLIP.lock().unwrap_or_else(|e| e.into_inner()).clone(),
};
if !s.is_empty() {
if plain {
app.session.set_tool_option("copy", &json!([]));
} else if let Some(runs) = CLIP_RUNS.lock().unwrap_or_else(|e| e.into_inner()).clone() {
let internal = app.type_paste_has_our_clipboard();
let mut styled = false;
if !plain
&& internal
&& let Some(runs) = CLIP_RUNS.lock().unwrap_or_else(|e| e.into_inner()).clone()
{
let clip_text: String =
serde_json::from_value::<Vec<vectorcraft_doc::TextRun>>(runs.clone()).unwrap_or_default().into_iter().map(|r| r.text).collect();
if clip_text == s {
app.session.set_tool_option("copy", &runs);
styled = true;
}
}
if !styled {
app.session.set_tool_option("copy", &json!([]));
}
let v = app.view_info();
let target_before = crate::ime::target(&mut app.session, v);
let before = crate::ime::stamp(&mut app.session, v);
+6
View File
@@ -113,6 +113,12 @@ pub(crate) fn import_command(f: &Flavour, center: Option<[f64; 2]>) -> (&'static
}
impl VectorcraftApp {
/// A Type paste may reuse its saved styled runs only while our desktop copy still owns the
/// clipboard. Pending formats also belong to that copy; web hosts have no ownership service.
pub(crate) fn type_paste_has_our_clipboard(&mut self) -> bool {
self.clipboard_publishing.pending || self.services.system_clipboard.as_mut().is_none_or(|cb| cb.holds_ours())
}
/// After Copy or Cut: offer the copied objects to other apps. With the system clipboard
/// service, the background worker makes the formats from a copy of the clipboard
/// ([`background::run`]) and they are written when they are ready. While the worker has a
+41
View File
@@ -232,6 +232,47 @@ fn paste_without_formatting_takes_surrounding_style() {
assert!(t2.runs.iter().all(|r| r.style.size == 10.0), "all runs take 10 pt size: {:?}", t2.runs);
}
#[test]
fn external_plain_text_matching_an_old_copy_uses_destination_style() {
let _guard = TEST_CLIP_LOCK.lock().unwrap();
let (mut app, board) = super::sysclip::app();
let source = run(&mut app, "text.create", json!({"x": 20, "y": 40, "text": "Hello", "size": 36, "color": "#ff0000"}))["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);
let own_destination =
run(&mut app, "text.create", json!({"x": 20, "y": 60, "text": "Own: ", "size": 12, "color": "#000000"}))["id"].as_u64().unwrap();
app.session.select_tool("type", v).unwrap();
app.session.set_tool_option("textSelection", &json!({"id": own_destination, "start": 5, "end": 5}));
crate::panels::character::intercept_text_command(&mut app, "edit.paste");
let node = app.session.doc().unwrap().doc.node(NodeId(own_destination)).unwrap();
let vectorcraft_doc::NodeKind::Text(t) = &node.kind else { panic!() };
assert_eq!(t.plain_text(), "Own: Hello");
assert_eq!(t.runs.last().unwrap().style.size, 36.0, "our copy keeps its style");
// Another app later copies the same characters as plain text. Their style is unrelated.
super::sysclip::copy_elsewhere(&board, vec![(vectorcraft_engine::cmd::clipboard::TEXT, b"Hello".to_vec())]);
let destination =
run(&mut app, "text.create", json!({"x": 20, "y": 80, "text": "Before ", "size": 12, "color": "#000000"}))["id"].as_u64().unwrap();
app.session.select_tool("type", v).unwrap();
app.session.set_tool_option("textSelection", &json!({"id": destination, "start": 7, "end": 7}));
super::sysclip::frame(&mut app, vec![egui::Event::Paste("Hello".into())]);
assert_eq!(app.system_clipboard_text().as_deref(), Some("Hello"));
app.session.select_tool("selection", v).unwrap();
app.session.select_tool("type", v).unwrap();
app.session.set_tool_option("textSelection", &json!({"id": destination, "start": 12, "end": 12}));
assert!(app.session.tool_wants_text());
assert!(crate::panels::character::intercept_text_command(&mut app, "edit.paste").is_some());
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(), "Before HelloHello");
assert!(t.runs.iter().all(|run| run.style.size == 12.0 && run.style.fill == Paint::solid(Color::rgb8(0, 0, 0))));
}
/// https://helpx.adobe.com/illustrator/using/format-text.html
#[test]
fn copying_single_character_carries_formatting_across_documents() {