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 ///