From c6dd115e78067ab3e22ff6dc6a103fc752ef034e Mon Sep 17 00:00:00 2001 From: Susiee The Linux User <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:09:42 +0200 Subject: [PATCH] UI: Route text clipboard writes through egui (#1239) Co-Authored-By: GPT-6 Luna --- apps/figcraft/src/main.rs | 5 +++-- apps/figcraft/src/services.rs | 10 +++++++--- crates/ui-egui/src/lib.rs | 7 ++++--- crates/ui-egui/src/text_enter_tests.rs | 24 ++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/apps/figcraft/src/main.rs b/apps/figcraft/src/main.rs index 0b71149a..64d34e48 100644 --- a/apps/figcraft/src/main.rs +++ b/apps/figcraft/src/main.rs @@ -244,14 +244,15 @@ fn main() -> eframe::Result { let result = eframe::run_native( "FigCraft", options, - Box::new(move |_cc| { + Box::new(move |cc| { + with_app(|fig| fig.services.set_egui_context(&cc.egui_ctx)); Ok(Box::new(App { queue, ctx_slot, title: String::new(), recovery, #[cfg(target_os = "macos")] - apple_events: apple_events.connect(&_cc.egui_ctx), + apple_events: apple_events.connect(&cc.egui_ctx), })) }), ); diff --git a/apps/figcraft/src/services.rs b/apps/figcraft/src/services.rs index 7ee41ada..4eb63611 100644 --- a/apps/figcraft/src/services.rs +++ b/apps/figcraft/src/services.rs @@ -11,6 +11,7 @@ use serde_json::{Value, json}; pub struct Desktop { clipboard: Option, + egui_ctx: Option, /// Where the preferences live ([`config_dir`]). dir: Option, pub prefs: Option, @@ -54,7 +55,7 @@ impl Desktop { let reduce_motion = Arc::new(AtomicBool::new(false)); let flag = reduce_motion.clone(); std::thread::spawn(move || flag.store(system_reduce_motion(), Ordering::Relaxed)); - Self { clipboard: arboard::Clipboard::new().ok(), dir, prefs, reduce_motion, language: system_language(), recovery } + Self { clipboard: arboard::Clipboard::new().ok(), egui_ctx: None, dir, prefs, reduce_motion, language: system_language(), recovery } } } @@ -277,10 +278,13 @@ impl Services for Desktop { picked(dialog(title).pick_folder()) } fn clipboard_set_text(&mut self, s: &str) { - if let Some(c) = self.clipboard.as_mut() { - let _ = c.set_text(s.to_string()); + if let Some(ctx) = &self.egui_ctx { + ctx.copy_text(s.to_string()); } } + fn set_egui_context(&mut self, ctx: &egui::Context) { + self.egui_ctx = Some(ctx.clone()); + } fn clipboard_get_text(&mut self) -> Option { read_clipboard(self.clipboard.as_mut()?, |c| c.get_text().ok()) } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 11fc307a..59faf5aa 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -272,6 +272,8 @@ pub trait Services { Box::pin(std::future::ready(None)) } fn clipboard_set_text(&mut self, _s: &str) {} + /// Give native services access to the window context for platform clipboard output. + fn set_egui_context(&mut self, _ctx: &egui::Context) {} fn clipboard_get_text(&mut self) -> Option { None } @@ -387,9 +389,8 @@ impl Wake for RepaintWaker { pub struct NoServices; impl Services for NoServices {} -/// Put `text` on the system clipboard through egui's output, which the browser shell writes while -/// it handles the copy event (the only time a page may write it). For shells without a clipboard -/// of their own. +/// Put `text` on the system clipboard through egui's platform output. Native integrations handle +/// it directly; the browser shell writes it while handling the copy event. pub fn egui_copy_text(ctx: &egui::Context, text: &str) { ctx.copy_text(text.to_string()); } diff --git a/crates/ui-egui/src/text_enter_tests.rs b/crates/ui-egui/src/text_enter_tests.rs index 5de0522d..89129a7e 100644 --- a/crates/ui-egui/src/text_enter_tests.rs +++ b/crates/ui-egui/src/text_enter_tests.rs @@ -59,6 +59,30 @@ fn enter_edits_a_selected_text_with_all_of_it_selected_and_escape_keeps_the_laye assert!(u.app.session.selection().is_empty()); } +#[test] +fn copying_and_cutting_selected_text_use_clipboard_service() { + use std::{cell::RefCell, rc::Rc}; + + struct Clipboard(Rc>>); + impl crate::Services for Clipboard { + fn clipboard_set_text(&mut self, text: &str) { + self.0.borrow_mut().push(text.to_string()); + } + } + + let mut s = Session::new(); + let t = NodeId(s.execute("text.create", &json!({"text": "Hello world", "x": 20, "y": 20})).unwrap()["id"].as_u64().unwrap()); + s.execute("select.set", &json!({"ids": [t]})).unwrap(); + let mut u = Ui::new(s); + let copied = Rc::new(RefCell::new(Vec::new())); + u.app.services = Box::new(Clipboard(copied.clone())); + press(&mut u, Key::Enter); + u.frame_output(vec![Event::Copy]); + u.frame_output(vec![Event::Cut]); + assert_eq!(&*copied.borrow(), &["Hello world", "Hello world"]); + assert_eq!(u.app.session.doc().unwrap().node(t).unwrap().kind.text().unwrap().characters, ""); +} + #[test] fn a_new_file_takes_the_page_colour_of_the_theme_and_keeps_it() { let mut s = Session::new();