From 1baf1f5569565944f77b1c6be2775b77c2c9f419 Mon Sep 17 00:00:00 2001 From: Zetenypapp Date: Tue, 13 Oct 2026 18:45:38 +0200 Subject: [PATCH] Copy and Cut buttons and menus put the text on the system clipboard (#431) Ctrl+C and Ctrl+X ran edit.copy / edit.cut and handed the returned text to egui's clipboard; the ribbon's Copy and Cut, the slide's right-click menu, the menus, the palette and the control channel ran the same commands through SlideApp::run, which dropped the text, so it never left DeckCraft. SlideApp::run now keeps the text of a successful Copy or Cut and hands it to the clipboard on the next frame, like pending URLs. Regression test: copy_and_cut_commands_reach_the_system_clipboard runs both commands and checks the frame's CopyText output (it fails without the fix). Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/lib.rs | 56 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 46a87add..e372fb9e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-13 | Copy and Cut from the ribbon, the slide's right-click menu and the menus put the text on the system clipboard, as Ctrl+C and Ctrl+X do (#431) | | 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless | | 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline | | 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting | diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ec97e6e6..9ab7d048 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -209,6 +209,8 @@ pub struct SlideApp { /// creating or closing a presentation leaves it. pub backstage: Option, pending_urls: Vec, + /// Text for the system clipboard from a Copy or Cut command, handed over on the next frame. + pending_copy: Option, shot_token: u64, queued_shots: Vec<(u64, f64, u32)>, pending_shots: Vec<(u64, Option, Sender, f64)>, @@ -254,6 +256,7 @@ impl SlideApp { integrated_titlebar: false, backstage: None, pending_urls: vec![], + pending_copy: None, shot_token: 0, queued_shots: vec![], pending_shots: vec![], @@ -287,8 +290,16 @@ impl SlideApp { } else { self.session.execute(id, ¶ms).map_err(|e| e.to_string()) }; - if let Err(e) = &r { - self.set_status(e.clone()); + match &r { + // Copy and Cut from the ribbon, menus, the slide's context menu or the control channel + // put the text on the system clipboard, as Ctrl+C and Ctrl+X do (#431). + Ok(v) if matches!(id, "edit.copy" | "edit.cut") => { + if let Some(t) = v.get("text").and_then(Value::as_str) { + self.pending_copy = Some(t.to_string()); + } + } + Ok(_) => {} + Err(e) => self.set_status(e.clone()), } self.drain_requests(); r @@ -985,6 +996,9 @@ impl SlideApp { for url in std::mem::take(&mut self.pending_urls) { ctx.open_url(egui::OpenUrl::new_tab(url)); } + if let Some(t) = self.pending_copy.take() { + ctx.copy_text(t); + } self.drain_requests(); self.perf.frame_ms = now_ms() - t0; } @@ -1408,4 +1422,42 @@ mod tests { harness.run_steps(3); assert_eq!(harness.state().textures.canvas_renders, n, "an unchanged slide isn't rendered again"); } + + /// #431: the ribbon's Copy and Cut, the slide's context menu, the menus and the control + /// channel all run the commands; they put the text on the system clipboard, as Ctrl+C and + /// Ctrl+X do. + #[test] + fn copy_and_cut_commands_reach_the_system_clipboard() { + 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); + let copied = |h: &egui_kittest::Harness<'_, SlideApp>| { + h.output().platform_output.commands.iter().find_map(|c| match c { + egui::OutputCommand::CopyText(t) => Some(t.clone()), + _ => None, + }) + }; + let title = harness.state().session.active().unwrap().doc.slides[0].shapes[0].id.0; + harness.state_mut().session.execute("text.edit", &json!({"id": title})).unwrap(); + harness.state_mut().session.type_text("Aurora").unwrap(); + harness.state_mut().session.key("Escape", Mods::default()).unwrap(); + harness.run_steps(1); + assert_eq!(copied(&harness), None); + + harness.state_mut().run("edit.copy", json!({})).unwrap(); + harness.run_steps(1); + assert_eq!(copied(&harness).as_deref(), Some("Aurora"), "Copy puts the text on the clipboard"); + harness.run_steps(1); + assert_eq!(copied(&harness), None, "only once"); + + harness.state_mut().run("edit.cut", json!({})).unwrap(); + harness.run_steps(1); + 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"); + } }