diff --git a/Cargo.lock b/Cargo.lock index 734e3d7..efea6db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,25 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arboard" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot", + "thiserror", + "winapi", + "x11rb 0.10.1", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -335,6 +354,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -446,6 +476,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "cursor-icon" version = "1.1.0" @@ -529,14 +568,24 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", "windows-sys 0.48.0", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "etagere" version = "0.2.8" @@ -562,6 +611,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" +[[package]] +name = "fdeflate" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +dependencies = [ + "simd-adler32", +] + [[package]] name = "figures" version = "0.1.0" @@ -574,6 +632,16 @@ dependencies = [ "winit", ] +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float_next_after" version = "1.0.0" @@ -675,6 +743,16 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "gethostname" version = "0.3.0" @@ -742,6 +820,7 @@ version = "0.1.0" dependencies = [ "ahash", "alot", + "arboard", "derive_more", "gooey-macros", "intentional", @@ -880,6 +959,8 @@ dependencies = [ "color_quant", "num-rational", "num-traits", + "png", + "tiff", ] [[package]] @@ -965,6 +1046,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + [[package]] name = "js-sys" version = "0.3.65" @@ -1260,13 +1347,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] name = "naga" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61d829abac9f5230a85d8cc83ec0879b4c09790208ae25b5ea031ef84562e071" +checksum = "6cd05939c491da968a42986204b7431678be21fdcd4b10cc84997ba130ada5a4" dependencies = [ "bit-set", "bitflags 2.4.1", @@ -1321,6 +1409,18 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.25.1" @@ -1428,6 +1528,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.1" @@ -1459,6 +1570,15 @@ dependencies = [ "cc", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -1610,6 +1730,19 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "pollster" version = "0.3.0" @@ -1935,6 +2068,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "similar" version = "2.3.0" @@ -2018,6 +2157,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "strict-num" version = "0.1.1" @@ -2099,6 +2244,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tiny-skia" version = "0.11.2" @@ -2496,6 +2652,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "wgpu" version = "0.18.0" @@ -2523,9 +2685,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "837e02ddcdc6d4a9b56ba4598f7fd4202a7699ab03f6ef4dcdebfad2c966aea6" +checksum = "ef91c1d62d1e9e81c79e600131a258edf75c9531cbdbde09c44a011a47312726" dependencies = [ "arrayvec", "bit-vec", @@ -2838,7 +3000,7 @@ dependencies = [ "web-time", "windows-sys 0.48.0", "x11-dl", - "x11rb", + "x11rb 0.12.0", "xkbcommon-dl", ] @@ -2862,6 +3024,19 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname 0.2.3", + "nix 0.24.3", + "winapi", + "winapi-wsapoll", + "x11rb-protocol 0.10.0", +] + [[package]] name = "x11rb" version = "0.12.0" @@ -2869,14 +3044,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ "as-raw-xcb-connection", - "gethostname", + "gethostname 0.3.0", "libc", "libloading 0.7.4", "nix 0.26.4", "once_cell", "winapi", "winapi-wsapoll", - "x11rb-protocol", + "x11rb-protocol 0.12.0", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6b642d1..3b3f92a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ palette = "0.7.3" ahash = "0.8.6" gooey-macros = { version = "0.1.0", path = "gooey-macros" } derive_more = { version = "1.0.0-beta.6", features = ["from"] } +arboard = "3.2.1" # [patch."https://github.com/khonsulabs/kludgine"] diff --git a/src/widgets/input.rs b/src/widgets/input.rs index c294600..da35c71 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -118,6 +118,101 @@ impl Input { editor.set_select_opt(Some(Cursor::new_with_affinity(0, 0, Affinity::Before))); } } + + fn handle_key( + &mut self, + input: KeyEvent, + context: &mut EventContext<'_, '_>, + ) -> (bool, EventHandling) { + let editor = self.editor_mut(context.kludgine, &context.widget); + + match (input.state, input.logical_key, input.text.as_deref()) { + (ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => { + editor.action( + context.kludgine.font_system(), + match key { + Key::Backspace => Action::Backspace, + Key::Delete => Action::Delete, + _ => unreachable!("previously matched"), + }, + ); + (true, HANDLED) + } + (ElementState::Pressed, key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => { + let modifiers = context.modifiers(); + match (editor.select_opt(), modifiers.state().shift_key()) { + (None, true) => { + editor.set_select_opt(Some(editor.cursor())); + } + (Some(_), false) => { + editor.set_select_opt(None); + } + _ => {} + }; + + editor.action( + context.kludgine.font_system(), + match key { + Key::ArrowLeft if modifiers.word_select() => Action::PreviousWord, + Key::ArrowLeft => Action::Left, + Key::ArrowDown => Action::Down, + Key::ArrowUp => Action::Up, + Key::ArrowRight if modifiers.word_select() => Action::NextWord, + Key::ArrowRight => Action::Right, + _ => unreachable!("previously matched"), + }, + ); + (false, HANDLED) + } + (state, _, Some("a")) if context.modifiers().primary() => { + if state.is_pressed() { + self.select_all(); + } + (false, HANDLED) + } + (state, _, Some("c")) if context.modifiers().primary() => { + + if state.is_pressed() { + if let Some(mut clipboard) = context.clipboard_guard() { + if let Some(selection) = editor.copy_selection() { + match clipboard.set_text(selection) { + Ok(()) => {}, + Err(err) => tracing::error!("error copying to clipboard: {err}"), + } + } + } + } + (false, HANDLED) + } + (state, _, Some("v")) if context.modifiers().primary() => { + let pasted = state.is_pressed() && + match context.clipboard_guard().map(|mut clipboard| clipboard.get_text()) { + Some(Ok(text)) => { + editor.insert_string(&text, None); + true + }, + None | Some(Err(arboard::Error::ConversionFailure)) => false, + Some(Err(err)) => {tracing::error!("error retrieving clipboard contents: {err}"); false}, + } + + ; + (pasted, HANDLED) + } + (state, _, Some(text)) + if !context.modifiers().primary() + && text != "\t" // tab + && text != "\r" // enter/return + && text != "\u{1b}" // escape + => + { + if state.is_pressed() { + editor.insert_string(text, None); + } + (state.is_pressed(), HANDLED) + } + (_, _, _) => (false, IGNORED), + } + } } impl Default for Input { @@ -389,70 +484,11 @@ impl Widget for Input { on_key.invoke(input.clone())?; } - let editor = self.editor_mut(context.kludgine, &context.widget); - // println!( // "Keyboard input: {:?}. {:?}, {:?}", // input.logical_key, input.text, input.physical_key // ); - let (text_changed, handled) = match (input.state, input.logical_key, input.text.as_deref()) { - (ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => { - editor.action( - context.kludgine.font_system(), - match key { - Key::Backspace => Action::Backspace, - Key::Delete => Action::Delete, - _ => unreachable!("previously matched"), - }, - ); - (true, HANDLED) - } - (ElementState::Pressed, key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => { - let modifiers = context.modifiers(); - match (editor.select_opt(), modifiers.state().shift_key()) { - (None, true) => { - editor.set_select_opt(Some(editor.cursor())); - } - (Some(_), false) => { - editor.set_select_opt(None); - } - _ => {} - }; - - editor.action( - context.kludgine.font_system(), - match key { - Key::ArrowLeft if modifiers.word_select() => Action::PreviousWord, - Key::ArrowLeft => Action::Left, - Key::ArrowDown => Action::Down, - Key::ArrowUp => Action::Up, - Key::ArrowRight if modifiers.word_select() => Action::NextWord, - Key::ArrowRight => Action::Right, - _ => unreachable!("previously matched"), - }, - ); - (false, HANDLED) - } - (state, _, Some("a")) if context.modifiers().primary() => { - if state.is_pressed() { - self.select_all(); - } - (false, HANDLED) - } - (state, _, Some(text)) - if !context.modifiers().primary() - && text != "\t" // tab - && text != "\r" // enter/return - && text != "\u{1b}" // escape - => - { - if state.is_pressed() { - editor.insert_string(text, None); - } - (state.is_pressed(), HANDLED) - } - (_, _, _) => (false, IGNORED), - }; + let (text_changed, handled) = self.handle_key(input, context); if handled.is_break() { context.set_needs_redraw(); diff --git a/src/window.rs b/src/window.rs index e052d68..fdbc0f2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,10 +6,11 @@ use std::ops::{Deref, DerefMut, Not}; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::path::Path; use std::string::ToString; -use std::sync::OnceLock; +use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; use ahash::AHashMap; use alot::LotId; +use arboard::Clipboard; use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize}; use kludgine::app::winit::event::{ DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, @@ -32,7 +33,7 @@ use crate::context::{ use crate::graphics::Graphics; use crate::styles::ThemePair; use crate::tree::Tree; -use crate::utils::ModifiersExt; +use crate::utils::{IgnorePoison, ModifiersExt}; use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; use crate::widget::{ EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, @@ -44,6 +45,7 @@ use crate::{initialize_tracing, ConstraintLimit, Run}; /// A currently running Gooey window. pub struct RunningWindow<'window> { window: kludgine::app::Window<'window, WindowCommand>, + clipboard: Option>>, focused: Dynamic, occluded: Dynamic, } @@ -51,11 +53,13 @@ pub struct RunningWindow<'window> { impl<'window> RunningWindow<'window> { pub(crate) fn new( window: kludgine::app::Window<'window, WindowCommand>, + clipboard: &Option>>, focused: &Dynamic, occluded: &Dynamic, ) -> Self { Self { window, + clipboard: clipboard.clone(), focused: focused.clone(), occluded: occluded.clone(), } @@ -74,6 +78,15 @@ impl<'window> RunningWindow<'window> { pub fn occluded(&self) -> &Dynamic { &self.occluded } + + /// Returns a locked mutex guard to the OS's clipboard, if one was able to be + /// initialized when the window opened. + #[must_use] + pub fn clipboard_guard(&mut self) -> Option> { + self.clipboard + .as_ref() + .map(|mutex| mutex.lock().ignore_poison()) + } } impl<'window> Deref for RunningWindow<'window> { @@ -291,16 +304,21 @@ struct GooeyWindow { current_theme: ThemePair, theme_mode: Value, transparent: bool, + clipboard: Option>>, } impl GooeyWindow where T: WindowBehavior, { - fn request_close(&mut self, window: &mut RunningWindow<'_>) -> bool { - self.should_close |= self.behavior.close_requested(window); + fn request_close( + should_close: &mut bool, + behavior: &mut T, + window: &mut RunningWindow<'_>, + ) -> bool { + *should_close |= behavior.close_requested(window); - self.should_close + *should_close } fn keyboard_activate_widget( @@ -453,6 +471,10 @@ where .take() .expect("theme always present"); + let clipboard = Clipboard::new() + .ok() + .map(|clipboard| Arc::new(Mutex::new(clipboard))); + let theme_mode = match context.settings.borrow_mut().theme_mode.take() { Some(Value::Dynamic(dynamic)) => { dynamic.update(window.theme().into()); @@ -463,7 +485,7 @@ where }; let transparent = context.settings.borrow().transparent; let mut behavior = T::initialize( - &mut RunningWindow::new(window, &focused, &occluded), + &mut RunningWindow::new(window, &clipboard, &focused, &occluded), context.user, ); let root = Tree::default().push_boxed(behavior.make_root(), None); @@ -494,6 +516,7 @@ where theme, theme_mode, transparent, + clipboard, } } @@ -521,7 +544,7 @@ where let is_expanded = self.constrain_window_resizing(resizable, &window, graphics); let graphics = self.contents.new_frame(graphics); - let mut window = RunningWindow::new(window, &self.focused, &self.occluded); + let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut context = GraphicsContext { widget: WidgetContext::new( self.root.clone(), @@ -633,11 +656,11 @@ where window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) -> bool { - self.request_close(&mut RunningWindow::new( - window, - &self.focused, - &self.occluded, - )) + Self::request_close( + &mut self.should_close, + &mut self.behavior, + &mut RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded), + ) } // fn power_preference() -> wgpu::PowerPreference { @@ -700,7 +723,7 @@ where let Some(target) = self.root.tree.widget_from_node(target) else { return; }; - let mut window = RunningWindow::new(window, &self.focused, &self.occluded); + let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut target = EventContext::new( WidgetContext::new( target, @@ -722,7 +745,13 @@ where if !handled { match input.logical_key { Key::Character(ch) if ch == "w" && window.modifiers().primary() => { - if input.state.is_pressed() && self.request_close(&mut window) { + if input.state.is_pressed() + && Self::request_close( + &mut self.should_close, + &mut self.behavior, + &mut window, + ) + { window.set_needs_redraw(); } } @@ -803,7 +832,7 @@ where .expect("missing widget") }); - let mut window = RunningWindow::new(window, &self.focused, &self.occluded); + let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut widget = EventContext::new( WidgetContext::new( widget, @@ -839,7 +868,7 @@ where .widget(self.root.id()) .expect("missing widget") }); - let mut window = RunningWindow::new(window, &self.focused, &self.occluded); + let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut target = EventContext::new( WidgetContext::new( widget, @@ -866,7 +895,7 @@ where let location = Point::::from(position); self.cursor.location = Some(location); - let mut window = RunningWindow::new(window, &self.focused, &self.occluded); + let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); EventContext::new( WidgetContext::new( @@ -913,7 +942,8 @@ where _device_id: DeviceId, ) { if self.cursor.widget.take().is_some() { - let mut window = RunningWindow::new(window, &self.focused, &self.occluded); + let mut window = + RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut context = EventContext::new( WidgetContext::new( self.root.clone(), @@ -937,7 +967,7 @@ where state: ElementState, button: MouseButton, ) { - let mut window = RunningWindow::new(window, &self.focused, &self.occluded); + let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); match state { ElementState::Pressed => { EventContext::new(