#201: route macOS text clipboard commands through egui

Forward native Copy/Cut/Paste for active canvas text through egui so the desktop integration can use the system clipboard. Add regression coverage for the native clipboard event path.

Co-authored-by: GitHub Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Michael Townshend
2026-10-10 17:41:14 +02:00
co-authored by GitHub Copilot
parent 92649ffdb0
commit f05e8f5d98
+34 -3
View File
@@ -1928,9 +1928,15 @@ fn clipped_submenu<'a, R>(
/// ⌘A, ⌘C, ⌘X, ⌘V, ⌘Z and ⇧⌘Z before the window sees them, so while a text field has keyboard
/// focus, Select All, Copy, Cut, Paste, Undo and Redo act on that field, as in any Mac app.
pub fn activate_native(app: &mut DesignApp, ctx: &egui::Context, id: &str, params: &Value) {
if !(ctx.text_edit_focused() && params.is_null() && forward_to_text_field(app, ctx, id)) {
activate(app, id, params);
if params.is_null() && ctx.text_edit_focused() && forward_to_text_edit(app, ctx, id) {
return;
}
// The canvas text editor is not an egui TextEdit, so its clipboard events must pass through
// egui for the desktop integration to read/write the macOS clipboard.
if params.is_null() && app.session.wants_text() && matches!(id, "edit.copy" | "edit.cut" | "edit.paste") && forward_to_text_edit(app, ctx, id) {
return;
}
activate(app, id, params);
}
/// Whether a native menu bar item can be chosen: as in the in-window menus, but the editing
@@ -1944,7 +1950,7 @@ const TEXT_FIELD_COMMANDS: [&str; 6] = ["edit.selectAll", "edit.copy", "edit.cut
/// Hand a standard editing command to the focused text field as next frame's input; false when
/// `id` isn't one of them.
fn forward_to_text_field(app: &mut DesignApp, ctx: &egui::Context, id: &str) -> bool {
fn forward_to_text_edit(app: &mut DesignApp, ctx: &egui::Context, id: &str) -> bool {
let key = |key: egui::Key, modifiers: egui::Modifiers| {
[true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers })
};
@@ -2957,6 +2963,31 @@ mod tests {
assert_eq!(document_items(&app), (2, 0), "nothing was pasted into the document");
}
#[test]
fn native_copy_and_paste_use_the_system_clipboard_for_canvas_text() {
let ctx = egui::Context::default();
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", &json!({})).unwrap();
app.session.execute("frame.create", &json!({"rect": [36, 36, 200, 200], "content": "text", "text": "hello"})).unwrap();
let story = *app.session.active().unwrap().doc.stories.keys().next().unwrap();
app.select_tool("type");
app.run("text.select", json!({"story": story.0, "anchor": 0, "focus": 5})).unwrap();
for _ in 0..3 {
native_frame(&mut app, &ctx, vec![], None);
}
assert!(app.session.wants_text());
native_frame(&mut app, &ctx, vec![], Some("edit.copy"));
let copied = native_frame(&mut app, &ctx, vec![], None);
assert!(copied.platform_output.commands.contains(&egui::OutputCommand::CopyText("hello".into())));
let paste = native_frame(&mut app, &ctx, vec![], Some("edit.paste"));
let commands = &paste.viewport_output[&egui::ViewportId::ROOT].commands;
assert!(commands.contains(&egui::ViewportCommand::RequestPaste), "{commands:?}");
native_frame(&mut app, &ctx, vec![egui::Event::Paste("world".into())], None);
assert_eq!(app.session.active().unwrap().doc.story(story).unwrap().text, "world");
}
#[test]
fn native_undo_and_redo_act_on_the_focused_field() {
let ctx = egui::Context::default();