Merge PR #189 from Claudio911015: Ctrl+C then Ctrl+V pastes objects that carry no text (Fixes #182)

This commit is contained in:
Brandon Thomas
2026-10-14 12:57:20 -04:00
+44 -2
View File
@@ -132,6 +132,17 @@ impl Default for UiState {
}
}
/// Text put on the system clipboard when the copied objects (a rectangle, a picture, slides…)
/// carry no text. egui-winit drops Ctrl+V when the system clipboard is empty, so without it the
/// shortcut never reached the session and only the context menu could paste objects (#182). An
/// invisible word joiner, so a paste into another app adds nothing visible.
const OBJECTS_MARKER: &str = "\u{2060}";
/// What a copy/cut leaves on the system clipboard for the session's plain-text rendering `t`.
fn system_clipboard_text(t: &str) -> String {
if t.is_empty() { OBJECTS_MARKER.to_string() } else { t.to_string() }
}
/// UI-only commands (id, label, shortcut, params).
pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
("view.normal", "Normal", None, "{}"),
@@ -900,17 +911,19 @@ impl SlideApp {
if let Ok(v) = self.session.execute("edit.copy", &json!({}))
&& let Some(t) = v.get("text").and_then(Value::as_str)
{
ctx.copy_text(t.to_string());
ctx.copy_text(system_clipboard_text(t));
}
}
egui::Event::Cut => {
if let Ok(v) = self.session.execute("edit.cut", &json!({}))
&& let Some(t) = v.get("text").and_then(Value::as_str)
{
ctx.copy_text(t.to_string());
ctx.copy_text(system_clipboard_text(t));
}
}
egui::Event::Paste(text) => {
// Our own marker for objects without text: paste what the session copied.
let text = if text == OBJECTS_MARKER { String::new() } else { text };
// An image on the clipboard (screenshots, copied pictures) becomes a picture.
let img = if text.is_empty() { self.services.clipboard_image.as_mut().and_then(|f| f()) } else { None };
let r = match img {
@@ -1467,4 +1480,33 @@ mod tests {
assert_eq!(copied(&harness).as_deref(), Some("Aurora"), "Cut does too");
assert!(harness.state().session.active().unwrap().doc.slides[0].shapes.iter().all(|s| s.id.0 != title), "and removes the shape");
}
/// #182: Ctrl+C then Ctrl+V did nothing for objects without text (a rectangle), because
/// egui-winit drops Ctrl+V when the system clipboard is empty; the context menu worked.
#[test]
fn copy_then_paste_shortcut_duplicates_an_object_without_text() {
let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
SlideApp::new(Session::with_new(), Services::default()),
);
harness.run_steps(2);
harness.state_mut().session.execute("shape.insert", &json!({"preset": "rect", "rect": [10, 10, 50, 50]})).unwrap();
let count = |h: &egui_kittest::Harness<'_, SlideApp>| h.state().session.doc().unwrap().current_slide().unwrap().shapes.len();
let before = count(&harness);
// What the copy leaves on the system clipboard is what egui-winit hands back on Ctrl+V.
assert_eq!(system_clipboard_text(""), OBJECTS_MARKER);
assert_eq!(system_clipboard_text("hello"), "hello");
harness.event(egui::Event::Copy);
harness.run_steps(2);
harness.event(egui::Event::Paste(OBJECTS_MARKER.to_string()));
harness.run_steps(2);
assert_eq!(count(&harness), before + 1, "the shortcut pastes a copy of the rectangle");
let d = harness.state().session.active().unwrap();
let pasted = d.doc.slides[0].shapes.last().unwrap();
assert!(pasted.text.as_ref().is_none_or(|t| t.text().is_empty()), "a copy of the rectangle, not a text box holding the marker");
}
}