From 8b3b5dff37cd4bdc7f1dcd1bc15c26dfb26374dd Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Tue, 18 Jan 2022 23:28:24 +0300 Subject: [PATCH] Provide system clipboard handling API on `Window` Clipboard handling is essential for system window handling and also is a complicated task to be implemented and handled properly. So abstracting this code into a windowing library like `winit` is the most sensible thing to do. While it could be offloaded to crates like copypasta[1] the result is always not good enough[2][3] and it also results in overengineering[4] (look at its codebase size and compare to what we've got in winit for Wayland and how much better the handling is done in winit). The new API is on the `Window` and is the following: ```rust pub fn set_clipboard_content + 'static>( &self, content: C, mimes: HashSet); pub fn request_clipboard_content( &self, mimes: HashSet, metadata: Option>); ``` And also on `WindowExtUnix` where added similar methods to get primary selection content (The one on middle mouse button). Working with clipboard is async and loading it is in resulted window event in `event::WindowEvent::ClipboardContent`. The use of metadata is due to provide ability to identify requested load from system's clipboard. [1] - https://github.com/alacritty/copypasta. [2] - https://github.com/alacritty/alacritty/issues/3108 [3] - https://github.com/alacritty/alacritty/issues/3601 [4] - https://github.com/Smithay/smithay-clipboard --- CHANGELOG.md | 2 + FEATURES.md | 1 + src/event.rs | 26 ++ src/platform/unix.rs | 39 +++ src/platform_impl/linux/mod.rs | 41 ++- src/platform_impl/linux/wayland/clipboard.rs | 308 ++++++++++++++++++ src/platform_impl/linux/wayland/env.rs | 73 ++++- src/platform_impl/linux/wayland/mod.rs | 2 + .../linux/wayland/seat/keyboard/handlers.rs | 23 +- .../linux/wayland/seat/keyboard/mod.rs | 8 +- .../linux/wayland/seat/pointer/handlers.rs | 11 +- src/platform_impl/linux/wayland/window/mod.rs | 64 +++- .../linux/wayland/window/shim.rs | 69 +++- src/platform_impl/linux/x11/window.rs | 39 ++- src/window.rs | 29 ++ 15 files changed, 724 insertions(+), 11 deletions(-) create mode 100644 src/platform_impl/linux/wayland/clipboard.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ccaba2b7..b69bb03c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - On X11, add mappings for numpad comma, numpad enter, numlock and pause. - On macOS, fix Pinyin IME input by reverting a change that intended to improve IME. - On Windows, fix a crash with transparent windows on Windows 11. +- Add `set_clipboard_content` and `request_clipboard_content` API on `Window` +- Add `set_primary_clipboard_content` and `request_primary_clipboard_content` API on `WindowExtUnix` # 0.26.0 (2021-12-01) diff --git a/FEATURES.md b/FEATURES.md index 484dacbc19..42825e9c62 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -217,6 +217,7 @@ Changes in the API that have been agreed upon but aren't implemented across all |New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |❓ | |Event Loop 2.0 ([#459]) |✔️ |✔️ |❌ |✔️ |❌ |✔️ |❓ | |Keyboard Input ([#812]) |❌ |❌ |❌ |❌ |❌ |❌ |❓ | +|Clipboard handling |❌ |❌ |❌ |✔️ |❌ |❌ |❓ | ### Completed API Reworks |Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | diff --git a/src/event.rs b/src/event.rs index 6660e00688..e9033de108 100644 --- a/src/event.rs +++ b/src/event.rs @@ -270,6 +270,9 @@ pub enum WindowEvent<'a> { /// issue, and it should get fixed - but it's the current state of the API. ModifiersChanged(ModifiersState), + /// Loaded clipboard content. + ClipboardContent(ClipboardContent), + /// The cursor has moved on the window. CursorMoved { device_id: DeviceId, @@ -354,6 +357,27 @@ pub enum WindowEvent<'a> { ThemeChanged(Theme), } +/// Metadata which is passed along request to load clipboard. +pub type ClipboardMetadata = dyn std::any::Any + Send + Sync + 'static; + +#[derive(Debug, Clone)] +pub struct ClipboardContent { + /// Clipboard data. + pub data: Vec, + + /// Mime type which was picked to be loaded. + pub mime: String, + + /// Metadata passed to `request_clipboard_content`. + pub metadata: Option>, +} + +impl PartialEq for ClipboardContent { + fn eq(&self, other: &Self) -> bool { + self.data == other.data + } +} + impl Clone for WindowEvent<'static> { fn clone(&self) -> Self { use self::WindowEvent::*; @@ -366,6 +390,7 @@ impl Clone for WindowEvent<'static> { HoveredFile(file) => HoveredFile(file.clone()), HoveredFileCancelled => HoveredFileCancelled, ReceivedCharacter(c) => ReceivedCharacter(*c), + ClipboardContent(content) => ClipboardContent(content.clone()), Focused(f) => Focused(*f), KeyboardInput { device_id, @@ -456,6 +481,7 @@ impl<'a> WindowEvent<'a> { DroppedFile(file) => Some(DroppedFile(file)), HoveredFile(file) => Some(HoveredFile(file)), HoveredFileCancelled => Some(HoveredFileCancelled), + ClipboardContent(content) => Some(ClipboardContent(content)), ReceivedCharacter(c) => Some(ReceivedCharacter(c)), Focused(focused) => Some(Focused(focused)), KeyboardInput { diff --git a/src/platform/unix.rs b/src/platform/unix.rs index ba600b134b..205a88a783 100644 --- a/src/platform/unix.rs +++ b/src/platform/unix.rs @@ -6,11 +6,13 @@ target_os = "openbsd" ))] +use std::collections::HashSet; use std::os::raw; #[cfg(feature = "x11")] use std::{ptr, sync::Arc}; use crate::{ + event::ClipboardMetadata, event_loop::{EventLoop, EventLoopWindowTarget}, monitor::MonitorHandle, window::{Window, WindowBuilder}, @@ -192,6 +194,18 @@ impl EventLoopExtUnix for EventLoop { /// Additional methods on `Window` that are specific to Unix. pub trait WindowExtUnix { + fn request_primary_clipboard_content( + &self, + mimes: HashSet, + metadata: Option>, + ); + + fn set_primary_clipboard_content + 'static>( + &self, + content: C, + mimes: HashSet, + ); + /// Returns the ID of the `Window` xlib object that is used by this window. /// /// Returns `None` if the window doesn't use xlib (if it uses wayland for example). @@ -248,6 +262,31 @@ pub trait WindowExtUnix { } impl WindowExtUnix for Window { + /// Request primary system clipboard content. + /// + /// This clipboard is usually present on middle mouse button. + /// + /// The result of loading content with provided `mimes` mime types is delivered via + /// [`crate::event::WindowEvent::ClipboardContent`] with the given `metadata`. + fn request_primary_clipboard_content( + &self, + mimes: HashSet, + metadata: Option>, + ) { + self.window + .request_primary_clipboard_content(mimes, metadata); + } + + /// Set primary system clipboard content to provided `content` and advertising it with given + /// `mimes` mime types. + fn set_primary_clipboard_content + 'static>( + &self, + content: C, + mimes: HashSet, + ) { + self.window.set_primary_clipboard_content(content, mimes); + } + #[inline] #[cfg(feature = "x11")] fn xlib_window(&self) -> Option { diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index b8e233653c..79e7c7ff5f 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -9,9 +9,10 @@ #[cfg(all(not(feature = "x11"), not(feature = "wayland")))] compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); +use std::collections::{HashSet, VecDeque}; #[cfg(feature = "wayland")] use std::error::Error; -use std::{collections::VecDeque, env, fmt}; +use std::{env, fmt}; #[cfg(feature = "x11")] use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Arc}; @@ -26,7 +27,7 @@ use self::x11::{ffi::XVisualInfo, util::WindowType as XWindowType, XConnection, use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, - event::Event, + event::{ClipboardMetadata, Event}, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, icon::Icon, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, @@ -451,6 +452,42 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.request_redraw()) } + #[inline] + pub fn request_clipboard_content( + &self, + mime: HashSet, + metadata: Option>, + ) { + x11_or_wayland!(match self; Window(w) => w.request_clipboard_content(mime, metadata)) + } + + #[inline] + pub fn set_clipboard_content + 'static>( + &self, + content: C, + mimes: HashSet, + ) { + x11_or_wayland!(match self; Window(w) => w.set_clipboard_content(content, mimes)) + } + + #[inline] + pub fn request_primary_clipboard_content( + &self, + mimes: HashSet, + metadata: Option>, + ) { + x11_or_wayland!(match self; Window(w) => w.request_primary_clipboard_content(mimes, metadata)) + } + + #[inline] + pub fn set_primary_clipboard_content + 'static>( + &self, + content: C, + mimes: HashSet, + ) { + x11_or_wayland!(match self; Window(w) => w.set_primary_clipboard_content(content, mimes)) + } + #[inline] pub fn current_monitor(&self) -> Option { match self { diff --git a/src/platform_impl/linux/wayland/clipboard.rs b/src/platform_impl/linux/wayland/clipboard.rs new file mode 100644 index 0000000000..50ae4481a8 --- /dev/null +++ b/src/platform_impl/linux/wayland/clipboard.rs @@ -0,0 +1,308 @@ +//! Wayland clipboard handling. + +use std::collections::HashSet; +use std::fs::File; +use std::io::{Error, ErrorKind, Read, Result, Write}; +use std::os::unix::io::{FromRawFd, IntoRawFd, RawFd}; +use std::rc::Rc; +use std::sync::Arc; + +use sctk::reexports::calloop::generic::Generic; +use sctk::reexports::calloop::{Interest, LoopHandle, Mode, PostAction}; + +use sctk::data_device::{DataSourceEvent, ReadPipe, WritePipe}; +use sctk::environment::Environment; +use sctk::primary_selection::PrimarySelectionSourceEvent; + +use crate::event::{ClipboardContent, ClipboardMetadata, WindowEvent}; +use crate::platform_impl::wayland::env::WinitEnv; +use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::window::shim::LatestSeat; + +use super::WindowId; + +#[derive(Clone, Copy)] +pub enum ClipboardType { + /// Primary clipboard which is provided by `gtk_primary_selection_device_manager` or + /// `zwp_primary_selection_device_manager` protocols. + Primary, + + /// Clipboard which is provided by `wl_data_device_manager`. + Clipboard, +} + +pub struct ClipboardManager { + /// Environment to access clipboard related globals. + env: Environment, + + /// Loop handle which is used to register pipes in main event loop. + loop_handle: LoopHandle<'static, WinitState>, + + /// Latest seat information. + seat: Option, +} + +impl ClipboardManager { + pub fn new(env: Environment, loop_handle: LoopHandle<'static, WinitState>) -> Self { + Self { + env, + loop_handle, + seat: None, + } + } + + /// Update information about the latest observed seat and its serial. + pub fn update_seat_info(&mut self, seat: Option) { + self.seat = seat; + } + + /// Set `ty` clipboard content to `content` and offering it with `mimes` mime types. + pub fn set_content( + &self, + ty: ClipboardType, + content: Rc>, + mimes: HashSet, + ) { + let seat = match self.seat.as_ref() { + Some(seat) => seat, + None => return, + }; + + let mimes = mimes.into_iter().collect(); + match ty { + ClipboardType::Clipboard => self.set_clipboard_content(seat, content, mimes), + ClipboardType::Primary => self.set_primary_content(seat, content, mimes), + } + } + + fn set_clipboard_content( + &self, + seat: &LatestSeat, + content: Rc>, + mimes: Vec, + ) { + let loop_handle = self.loop_handle.clone(); + let data_source = self.env.new_data_source(mimes, move |event, _| { + let pipe = match event { + DataSourceEvent::Send { pipe, .. } => pipe, + _ => return, + }; + + write_clipboard(pipe, content.clone(), &loop_handle); + }); + + let _ = self.env.with_data_device(&seat.seat, |device| { + device.set_selection(&Some(data_source), seat.serial); + }); + } + + fn set_primary_content( + &self, + seat: &LatestSeat, + content: Rc>, + mimes: Vec, + ) { + let loop_handle = self.loop_handle.clone(); + let primary_source = self + .env + .new_primary_selection_source(mimes, move |event, _| { + let pipe = match event { + PrimarySelectionSourceEvent::Send { pipe, .. } => pipe, + _ => return, + }; + + write_clipboard(pipe, content.clone(), &loop_handle); + }); + + let _ = self.env.with_primary_selection(&seat.seat, |device| { + device.set_selection(&Some(primary_source), seat.serial); + }); + } + + /// Request `ty` clipboard content picking from `mimes` forwarding it to `window_id` passing + /// `metadata`. + pub fn request_content( + &self, + window_id: WindowId, + ty: ClipboardType, + mimes: HashSet, + metadata: Option>, + ) { + let seat = match self.seat.as_ref() { + Some(seat) => seat, + None => return, + }; + + match ty { + ClipboardType::Primary => { + self.request_primary_content(seat, window_id, mimes, metadata) + } + ClipboardType::Clipboard => { + self.request_clipboard_content(seat, window_id, mimes, metadata) + } + } + } + + fn request_clipboard_content( + &self, + seat: &LatestSeat, + window_id: WindowId, + mimes: HashSet, + metadata: Option>, + ) { + let loop_handle = self.loop_handle.clone(); + let _ = self.env.with_data_device(&seat.seat, move |device| { + device.with_selection(move |offer| { + let offer = match offer { + Some(offer) => offer, + None => return, + }; + + let mut mime = String::new(); + offer.with_mime_types(|types| { + for ty in types { + if mimes.contains(ty) { + mime = ty.to_string(); + } + } + }); + + let reader = match offer.receive(mime.clone()) { + Ok(reader) => reader, + Err(_) => return, + }; + + read_clipboard(reader, mime, metadata, window_id, &loop_handle); + }) + }); + } + + fn request_primary_content( + &self, + seat: &LatestSeat, + window_id: WindowId, + mimes: HashSet, + metadata: Option>, + ) { + let loop_handle = self.loop_handle.clone(); + let _ = self.env.with_primary_selection(&seat.seat, move |device| { + device.with_selection(move |offer| { + let offer = match offer { + Some(offer) => offer, + None => return, + }; + + let mut mime = String::new(); + offer.with_mime_types(|types| { + for ty in types { + if mimes.contains(ty) { + mime = ty.to_string(); + } + } + }); + + let reader = match offer.receive(mime.clone()) { + Ok(reader) => reader, + Err(_) => return, + }; + + read_clipboard(reader, mime, metadata, window_id, &loop_handle); + }) + }); + } +} + +/// Handle writing to clipboard's pipe write end. +fn write_clipboard( + writer: WritePipe, + content: Rc + 'static>, + loop_handle: &LoopHandle<'static, WinitState>, +) { + let mut writer = unsafe { + match raw_fd_into_non_blocking_file(writer.into_raw_fd()) { + Ok(writer) => writer, + Err(_) => return, + } + }; + + let written = match writer.write((&*content).as_ref()) { + Ok(n) if n == (&*content).as_ref().len() => return, + Ok(n) => n, + Err(err) if err.kind() == ErrorKind::WouldBlock => 0, + Err(_) => return, + }; + + // We weren't able to write all content at once, so add pipe as a `calloop`'s event source + // and continue writing when current content will be read. + let mut written = Rc::new(written); + let left_to_write = content.clone(); + let writer = Generic::new(writer, Interest::WRITE, Mode::Level); + let _ = loop_handle.insert_source(writer, move |_, file, _| { + let left_to_write = (&*left_to_write).as_ref(); + let (n, action) = match file.write(&left_to_write[*written..]) { + Ok(n) if *written + n == left_to_write.len() => (n, PostAction::Remove), + Ok(n) => (n, PostAction::Continue), + Err(err) if err.kind() == ErrorKind::WouldBlock => (0, PostAction::Continue), + Err(_) => (0, PostAction::Remove), + }; + + *Rc::get_mut(&mut written).unwrap() += n; + Ok(action) + }); +} + +/// Handle writing to clipboard's pipes read end. +fn read_clipboard( + reader: ReadPipe, + mime: String, + metadata: Option>, + window_id: WindowId, + loop_handle: &LoopHandle<'static, WinitState>, +) { + let reader = unsafe { + match raw_fd_into_non_blocking_file(reader.into_raw_fd()) { + Ok(reader) => reader, + Err(_) => return, + } + }; + let mut content = Vec::::with_capacity(u16::MAX as usize); + let mut reader_buffer = [0; u16::MAX as usize]; + let reader = Generic::new(reader, Interest::READ, Mode::Level); + let _ = loop_handle.insert_source(reader, move |_, file, winit_state| { + let action = loop { + match file.read(&mut reader_buffer) { + Ok(0) => { + let event_sink = &mut winit_state.event_sink; + let data = std::mem::take(&mut content); + let content = ClipboardContent { + data, + mime: mime.clone(), + metadata: metadata.clone(), + }; + let window_event = WindowEvent::ClipboardContent(content); + event_sink.push_window_event(window_event, window_id); + break PostAction::Remove; + } + Ok(n) => content.extend_from_slice(&reader_buffer[..n]), + Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue, + Err(_) => break PostAction::Remove, + } + }; + + Ok(action) + }); +} + +/// Create a `File` from `RawFd` marking it with `O_NONBLOCK`. +unsafe fn raw_fd_into_non_blocking_file(raw_fd: RawFd) -> Result { + let flags = libc::fcntl(raw_fd, libc::F_GETFD); + if flags < 0 { + return Err(Error::from_raw_os_error(flags)); + } + let result = libc::fcntl(raw_fd, libc::F_SETFL, flags | libc::O_NONBLOCK); + if result < 0 { + return Err(Error::from_raw_os_error(result)); + } + + Ok(File::from_raw_fd(raw_fd)) +} diff --git a/src/platform_impl/linux/wayland/env.rs b/src/platform_impl/linux/wayland/env.rs index ae213cb64a..ded09c3ee1 100644 --- a/src/platform_impl/linux/wayland/env.rs +++ b/src/platform_impl/linux/wayland/env.rs @@ -14,18 +14,30 @@ use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_rela use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1; use sctk::reexports::protocols::unstable::text_input::v3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activation_v1::XdgActivationV1; +use sctk::reexports::protocols::unstable::primary_selection::v1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1; +use sctk::reexports::protocols::misc::gtk_primary_selection::client::gtk_primary_selection_device_manager::GtkPrimarySelectionDeviceManager; +use sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager; +use sctk::data_device::{DataDevice, DataDeviceHandler, DataDeviceHandling, DndEvent}; use sctk::environment::{Environment, SimpleGlobal}; use sctk::output::{OutputHandler, OutputHandling, OutputInfo, OutputStatusListener}; +use sctk::primary_selection::{ + PrimarySelectionDevice, PrimarySelectionDeviceManager, PrimarySelectionHandler, + PrimarySelectionHandling, +}; + use sctk::seat::{SeatData, SeatHandler, SeatHandling, SeatListener}; use sctk::shell::{Shell, ShellHandler, ShellHandling}; use sctk::shm::ShmHandler; +use sctk::MissingGlobal; /// Set of extra features that are supported by the compositor. #[derive(Debug, Clone, Copy)] pub struct WindowingFeatures { cursor_grab: bool, xdg_activation: bool, + clipboard: bool, + primary_clipboard: bool, } impl WindowingFeatures { @@ -33,9 +45,14 @@ impl WindowingFeatures { pub fn new(env: &Environment) -> Self { let cursor_grab = env.get_global::().is_some(); let xdg_activation = env.get_global::().is_some(); + let clipboard = env.get_global::().is_some(); + let primary_clipboard = env.get_primary_selection_manager().is_some(); + Self { cursor_grab, xdg_activation, + clipboard, + primary_clipboard, } } @@ -46,6 +63,14 @@ impl WindowingFeatures { pub fn xdg_activation(&self) -> bool { self.xdg_activation } + + pub fn clipboard(&self) -> bool { + self.clipboard + } + + pub fn primary_clipboard(&self) -> bool { + self.primary_clipboard + } } sctk::environment!(WinitEnv, @@ -56,6 +81,9 @@ sctk::environment!(WinitEnv, WlShell => shell, XdgWmBase => shell, ZxdgShellV6 => shell, + WlDataDeviceManager => data_device, + ZwpPrimarySelectionDeviceManagerV1 => primary_selection, + GtkPrimarySelectionDeviceManager => primary_selection, ZxdgDecorationManagerV1 => decoration_manager, ZwpRelativePointerManagerV1 => relative_pointer_manager, ZwpPointerConstraintsV1 => pointer_constraints, @@ -82,6 +110,10 @@ pub struct WinitEnv { shell: ShellHandler, + data_device: DataDeviceHandler, + + primary_selection: PrimarySelectionHandler, + relative_pointer_manager: SimpleGlobal, pointer_constraints: SimpleGlobal, @@ -99,7 +131,7 @@ impl WinitEnv { let outputs = OutputHandler::new(); // Keyboard/Pointer/Touch input. - let seats = SeatHandler::new(); + let mut seats = SeatHandler::new(); // Essential globals. let shm = ShmHandler::new(); @@ -110,6 +142,12 @@ impl WinitEnv { // backends. let shell = ShellHandler::new(); + // Clipboard and drag'n'drop. + let data_device = DataDeviceHandler::init(&mut seats); + + // Primary clipboard. + let primary_selection = PrimarySelectionHandler::init(&mut seats); + // Server side decorations. let decoration_manager = SimpleGlobal::new(); @@ -132,6 +170,8 @@ impl WinitEnv { compositor, subcompositor, shell, + data_device, + primary_selection, decoration_manager, relative_pointer_manager, pointer_constraints, @@ -164,3 +204,34 @@ impl OutputHandling for WinitEnv { self.outputs.listen(f) } } + +impl DataDeviceHandling for WinitEnv { + fn set_callback, DispatchData<'_>) + 'static>( + &mut self, + callback: F, + ) -> Result<(), MissingGlobal> { + self.data_device.set_callback(callback) + } + + fn with_device( + &self, + seat: &WlSeat, + f: F, + ) -> Result<(), MissingGlobal> { + self.data_device.with_device(seat, f) + } +} + +impl PrimarySelectionHandling for WinitEnv { + fn with_primary_selection( + &self, + seat: &WlSeat, + f: F, + ) -> Result<(), MissingGlobal> { + self.primary_selection.with_primary_selection(seat, f) + } + + fn get_primary_selection_manager(&self) -> Option { + self.primary_selection.get_primary_selection_manager() + } +} diff --git a/src/platform_impl/linux/wayland/mod.rs b/src/platform_impl/linux/wayland/mod.rs index 4ed564aec0..73f90febab 100644 --- a/src/platform_impl/linux/wayland/mod.rs +++ b/src/platform_impl/linux/wayland/mod.rs @@ -8,10 +8,12 @@ use sctk::reexports::client::protocol::wl_surface::WlSurface; +pub use clipboard::{ClipboardManager, ClipboardType}; pub use event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; pub use output::{MonitorHandle, VideoMode}; pub use window::Window; +mod clipboard; mod env; mod event_loop; mod output; diff --git a/src/platform_impl/linux/wayland/seat/keyboard/handlers.rs b/src/platform_impl/linux/wayland/seat/keyboard/handlers.rs index 7c320973b8..ca2f5bf9be 100644 --- a/src/platform_impl/linux/wayland/seat/keyboard/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/keyboard/handlers.rs @@ -6,6 +6,7 @@ use sctk::seat::keyboard::Event as KeyboardEvent; use crate::event::{ElementState, KeyboardInput, ModifiersState, WindowEvent}; use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::window::shim::LatestSeat; use crate::platform_impl::wayland::{self, DeviceId}; use super::keymap; @@ -19,9 +20,17 @@ pub(super) fn handle_keyboard( ) { let event_sink = &mut winit_state.event_sink; match event { - KeyboardEvent::Enter { surface, .. } => { + KeyboardEvent::Enter { + surface, serial, .. + } => { let window_id = wayland::make_wid(&surface); + let window_handle = match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle, + None => return, + }; + window_handle.update_seat_info(Some(LatestSeat::new(inner.seat.clone(), serial))); + // Window gained focus. event_sink.push_window_event(WindowEvent::Focused(true), window_id); @@ -36,6 +45,11 @@ pub(super) fn handle_keyboard( KeyboardEvent::Leave { surface, .. } => { let window_id = wayland::make_wid(&surface); + match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle.update_seat_info(None), + None => return, + }; + // Notify that no modifiers are being pressed. if !inner.modifiers_state.borrow().is_empty() { event_sink.push_window_event( @@ -55,6 +69,7 @@ pub(super) fn handle_keyboard( keysym, state, utf8, + serial, .. } => { let window_id = match inner.target_window_id { @@ -62,6 +77,12 @@ pub(super) fn handle_keyboard( None => return, }; + let window_handle = match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle, + None => return, + }; + window_handle.update_seat_info(Some(LatestSeat::new(inner.seat.clone(), serial))); + let state = match state { KeyState::Pressed => ElementState::Pressed, KeyState::Released => ElementState::Released, diff --git a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs index c6e0ad456e..3911e4064b 100644 --- a/src/platform_impl/linux/wayland/seat/keyboard/mod.rs +++ b/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -34,7 +34,7 @@ impl Keyboard { loop_handle: LoopHandle<'static, WinitState>, modifiers_state: Rc>, ) -> Option { - let mut inner = KeyboardInner::new(modifiers_state); + let mut inner = KeyboardInner::new(seat.detach(), modifiers_state); let keyboard_data = keyboard::map_keyboard_repeat( loop_handle.clone(), seat, @@ -72,6 +72,9 @@ struct KeyboardInner { /// Currently focused surface. target_window_id: Option, + /// Seat assocciated with this keyboard. + seat: WlSeat, + /// A pending state of modifiers. /// /// This state is getting set if we've got a modifiers update @@ -84,11 +87,12 @@ struct KeyboardInner { } impl KeyboardInner { - fn new(modifiers_state: Rc>) -> Self { + fn new(seat: WlSeat, modifiers_state: Rc>) -> Self { Self { target_window_id: None, pending_modifers_state: None, modifiers_state, + seat, } } } diff --git a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs index 8c2474a989..cbc79b8265 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs @@ -14,6 +14,7 @@ use crate::event::{ DeviceEvent, ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent, }; use crate::platform_impl::wayland::event_loop::WinitState; +use crate::platform_impl::wayland::window::shim::LatestSeat; use crate::platform_impl::wayland::{self, DeviceId}; use super::{PointerData, WinitPointer}; @@ -55,6 +56,7 @@ pub(super) fn handle_pointer( let scale_factor = sctk::get_surface_scale_factor(&surface) as f64; pointer_data.surface = Some(surface); + window_handle.update_seat_info(Some(LatestSeat::new(seat.clone(), serial))); // Notify window that pointer entered the surface. let winit_pointer = WinitPointer { pointer, @@ -98,6 +100,8 @@ pub(super) fn handle_pointer( None => return, }; + window_handle.update_seat_info(Some(LatestSeat::new(seat.clone(), serial))); + // Notify a window that pointer is no longer observing it. let winit_pointer = WinitPointer { pointer, @@ -128,7 +132,6 @@ pub(super) fn handle_pointer( }; let window_id = wayland::make_wid(surface); - let scale_factor = sctk::get_surface_scale_factor(surface) as f64; let position = LogicalPosition::new(surface_x, surface_y).to_physical(scale_factor); @@ -155,6 +158,12 @@ pub(super) fn handle_pointer( None => return, }; + let window_handle = match winit_state.window_map.get_mut(&window_id) { + Some(window_handle) => window_handle, + None => return, + }; + window_handle.update_seat_info(Some(LatestSeat::new(seat, serial))); + let state = match state { wl_pointer::ButtonState::Pressed => ElementState::Pressed, wl_pointer::ButtonState::Released => ElementState::Released, diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 8af9ca20b1..2369700560 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -1,4 +1,5 @@ -use std::collections::VecDeque; +use std::collections::{HashSet, VecDeque}; +use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; @@ -12,6 +13,7 @@ use sctk::window::{Decorations, FallbackFrame}; use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::ClipboardMetadata; use crate::monitor::MonitorHandle as RootMonitorHandle; use crate::platform_impl::{ MonitorHandle as PlatformMonitorHandle, OsError, @@ -19,6 +21,7 @@ use crate::platform_impl::{ }; use crate::window::{CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}; +use super::clipboard::ClipboardType; use super::env::WindowingFeatures; use super::event_loop::WinitState; use super::output::{MonitorHandle, OutputManagerHandle}; @@ -206,6 +209,7 @@ impl Window { let window_handle = WindowHandle::new( &event_loop_window_target.env, window, + event_loop_window_target.event_loop_handle.clone(), size.clone(), window_requests.clone(), ); @@ -460,6 +464,64 @@ impl Window { self.send_request(WindowRequest::IMEPosition(position)); } + #[inline] + pub fn set_clipboard_content + 'static>( + &self, + content: C, + mimes: HashSet, + ) { + if !self.windowing_features.clipboard() { + return; + } + + let content = Rc::new(content); + let ty = ClipboardType::Clipboard; + self.send_request(WindowRequest::SetClipboardContent(ty, content, mimes)); + } + + #[inline] + pub fn set_primary_clipboard_content + 'static>( + &self, + content: C, + mimes: HashSet, + ) { + if !self.windowing_features.primary_clipboard() { + return; + } + + let ty = ClipboardType::Primary; + let content = Rc::new(content); + self.send_request(WindowRequest::SetClipboardContent(ty, content, mimes)); + } + + #[inline] + pub fn request_clipboard_content( + &self, + mimes: HashSet, + metadata: Option>, + ) { + if !self.windowing_features.clipboard() { + return; + } + + let ty = ClipboardType::Clipboard; + self.send_request(WindowRequest::RequestClipboardContent(ty, mimes, metadata)); + } + + #[inline] + pub fn request_primary_clipboard_content( + &self, + mimes: HashSet, + metadata: Option>, + ) { + if !self.windowing_features.primary_clipboard() { + return; + } + + let ty = ClipboardType::Primary; + self.send_request(WindowRequest::RequestClipboardContent(ty, mimes, metadata)); + } + #[inline] pub fn display(&self) -> &Display { &self.display diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index 7c76a2fa63..ed8dff73c0 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -1,7 +1,13 @@ use std::cell::Cell; +use std::collections::HashSet; +use std::rc::Rc; use std::sync::{Arc, Mutex}; +use sctk::reexports::calloop; + use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::protocol::wl_seat::WlSeat; + use sctk::reexports::client::Attached; use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activation_token_v1; use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activation_v1::XdgActivationV1; @@ -11,8 +17,9 @@ use sctk::window::{Decorations, FallbackFrame, Window}; use crate::dpi::{LogicalPosition, LogicalSize}; -use crate::event::WindowEvent; +use crate::event::{ClipboardMetadata, WindowEvent}; use crate::platform_impl::wayland; +use crate::platform_impl::wayland::clipboard::{ClipboardManager, ClipboardType}; use crate::platform_impl::wayland::env::WinitEnv; use crate::platform_impl::wayland::event_loop::WinitState; use crate::platform_impl::wayland::seat::pointer::WinitPointer; @@ -21,7 +28,7 @@ use crate::platform_impl::wayland::WindowId; use crate::window::{CursorIcon, UserAttentionType}; /// A request to SCTK window from Winit window. -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum WindowRequest { /// Set fullscreen. /// @@ -70,6 +77,16 @@ pub enum WindowRequest { /// Set IME window position. IMEPosition(LogicalPosition), + /// Set clipboard content. + SetClipboardContent(ClipboardType, std::rc::Rc>, HashSet), + + /// Requeset clipboard content + RequestClipboardContent( + ClipboardType, + HashSet, + Option>, + ), + /// Request Attention. /// /// `None` unsets the attention request. @@ -167,16 +184,33 @@ pub struct WindowHandle { /// Indicator whether user attention is requested. attention_requested: Cell, + + /// Clipboard manager. + clipboard_manager: ClipboardManager, +} + +#[derive(Debug)] +pub struct LatestSeat { + pub seat: WlSeat, + pub serial: u32, +} + +impl LatestSeat { + pub fn new(seat: WlSeat, serial: u32) -> Self { + Self { seat, serial } + } } impl WindowHandle { pub fn new( env: &Environment, window: Window, + loop_handle: calloop::LoopHandle<'static, WinitState>, size: Arc>>, pending_window_requests: Arc>>, ) -> Self { let xdg_activation = env.get_global::(); + let clipboard_manager = ClipboardManager::new(env.clone(), loop_handle); Self { window, @@ -189,6 +223,7 @@ impl WindowHandle { text_inputs: Vec::new(), xdg_activation, attention_requested: Cell::new(false), + clipboard_manager, } } @@ -333,6 +368,30 @@ impl WindowHandle { pointer.drag_window(&self.window); } } + + pub fn update_seat_info(&mut self, seat: Option) { + self.clipboard_manager.update_seat_info(seat); + } + + pub fn set_clipboard_content( + &self, + ty: ClipboardType, + content: Rc + 'static>, + mimes: HashSet, + ) { + self.clipboard_manager.set_content(ty, content, mimes); + } + + pub fn request_clipboard_content( + &self, + ty: ClipboardType, + mimes: HashSet, + metadata: Option>, + ) { + let window_id = wayland::make_wid(self.window.surface()); + self.clipboard_manager + .request_content(window_id, ty, mimes, metadata); + } } #[inline] @@ -425,6 +484,12 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { let window_update = window_updates.get_mut(window_id).unwrap(); window_update.refresh_frame = true; } + WindowRequest::SetClipboardContent(ty, content, mimes) => { + window_handle.set_clipboard_content(ty, content, mimes); + } + WindowRequest::RequestClipboardContent(ty, mimes, metadata) => { + window_handle.request_clipboard_content(ty, mimes, metadata); + } WindowRequest::Attention(request_type) => { window_handle.set_user_attention(request_type); } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 45759b05b2..884f5b2412 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1,6 +1,9 @@ use raw_window_handle::XlibHandle; + use std::{ - cmp, env, + cmp, + collections::HashSet, + env, ffi::CString, mem::{self, replace, MaybeUninit}, os::raw::*, @@ -8,6 +11,7 @@ use std::{ ptr, slice, sync::Arc, }; + use x11_dl::xlib::TrueColor; use libc; @@ -16,6 +20,7 @@ use parking_lot::Mutex; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, + event::ClipboardMetadata, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, platform_impl::{ x11::{ime::ImeContextCreationError, MonitorHandle as X11MonitorHandle}, @@ -1447,6 +1452,38 @@ impl UnownedWindow { WindowId(self.xwindow) } + #[inline] + pub fn request_clipboard_content( + &self, + _mimes: HashSet, + _metadata: Option>, + ) { + unimplemented!(); + } + + #[inline] + pub fn set_clipboard_content>(&self, _content: C, _mimes: HashSet) { + unimplemented!(); + } + + #[inline] + pub fn request_primary_clipboard_content( + &self, + _mimes: HashSet, + _metadata: Option>, + ) { + unimplemented!(); + } + + #[inline] + pub fn set_primary_clipboard_content>( + &self, + _content: C, + _mimes: HashSet, + ) { + unimplemented!(); + } + #[inline] pub fn request_redraw(&self) { self.redraw_sender diff --git a/src/window.rs b/src/window.rs index 9410421d57..c4cff68a8f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,9 +1,11 @@ //! The `Window` struct and associated types. +use std::collections::HashSet; use std::fmt; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError}, + event::ClipboardMetadata, event_loop::EventLoopWindowTarget, monitor::{MonitorHandle, VideoMode}, platform_impl, @@ -962,6 +964,33 @@ impl Window { } } +/// Clipboard functions. +impl Window { + /// Request system clipboard content. + /// + /// The result of loading content with provided `mimes` mime types is delivered via + /// [`crate::event::WindowEvent::ClipboardContent`] with the given `metadata`. + #[inline] + pub fn request_clipboard_content( + &self, + mimes: HashSet, + metadata: Option>, + ) { + self.window.request_clipboard_content(mimes, metadata); + } + + /// Set system clipboard content to provided `content` and advertising it with given `mimes` + /// mime types. + #[inline] + pub fn set_clipboard_content + 'static>( + &self, + content: C, + mimes: HashSet, + ) { + self.window.set_clipboard_content(content, mimes); + } +} + unsafe impl raw_window_handle::HasRawWindowHandle for Window { /// Returns a `raw_window_handle::RawWindowHandle` for the Window ///