From c8fe672b7393c06ceabae41ea32f9b47284003c5 Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Sat, 13 Aug 2022 01:51:23 +0200 Subject: [PATCH 01/87] Wayland: support basic Drag&Drop --- Cargo.toml | 292 +++++++++ src/platform_impl/linux/wayland/env.rs | 174 ++++++ .../linux/wayland/event_loop/mod.rs | 583 ++++++++++++++++++ .../linux/wayland/event_loop/state.rs | 36 ++ .../linux/wayland/seat/dnd/handlers.rs | 96 +++ .../linux/wayland/seat/dnd/mod.rs | 32 + src/platform_impl/linux/wayland/seat/mod.rs | 230 +++++++ 7 files changed, 1443 insertions(+) create mode 100644 src/platform_impl/linux/wayland/env.rs create mode 100644 src/platform_impl/linux/wayland/event_loop/mod.rs create mode 100644 src/platform_impl/linux/wayland/event_loop/state.rs create mode 100644 src/platform_impl/linux/wayland/seat/dnd/handlers.rs create mode 100644 src/platform_impl/linux/wayland/seat/dnd/mod.rs create mode 100644 src/platform_impl/linux/wayland/seat/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 3321df958d..417f7cb230 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,295 @@ +[package] +name = "winit" +version = "0.27.2" +authors = ["The winit contributors", "Pierre Krieger "] +description = "Cross-platform window creation library." +edition = "2021" +keywords = ["windowing"] +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/rust-windowing/winit" +documentation = "https://docs.rs/winit" +categories = ["gui"] +rust-version = "1.57.0" + +[package.metadata.docs.rs] +features = ["serde"] +default-target = "x86_64-unknown-linux-gnu" +# These are all tested in CI +targets = [ + # Windows + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + # macOS + "x86_64-apple-darwin", + # Unix (X11 & Wayland) + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + # iOS + "x86_64-apple-ios", + # Android + "aarch64-linux-android", + # WebAssembly + "wasm32-unknown-unknown", +] + +[features] +default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +x11 = ["x11-dl", "mio", "percent-encoding", "parking_lot"] +wayland = ["wayland-client", "wayland-protocols", "sctk"] +wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"] +wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/title"] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] + +[dependencies] +instant = { version = "0.1", features = ["wasm-bindgen"] } +once_cell = "1.12" +log = "0.4" +serde = { version = "1", optional = true, features = ["serde_derive"] } +raw_window_handle = { package = "raw-window-handle", version = "0.5" } +raw_window_handle_04 = { package = "raw-window-handle", version = "0.4" } +bitflags = "1" +mint = { version = "0.5.6", optional = true } + +[dev-dependencies] +image = { version = "0.24.0", default-features = false, features = ["png"] } +simple_logger = "2.1.0" + +[target.'cfg(target_os = "android")'.dependencies] +# Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 +ndk = "0.7.0" +ndk-glue = "0.7.0" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +objc = "0.2.7" + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.24" +core-foundation = "0.9" +core-graphics = "0.22" +dispatch = "0.2.0" + +[target.'cfg(target_os = "windows")'.dependencies] +parking_lot = "0.12" + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.36" +features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_Media", + "Win32_System_Com_StructuredStorage", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_UI_Accessibility", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Input_Touch", + "Win32_UI_Shell", + "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging", +] + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] +wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true } +wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true } +sctk = { package = "smithay-client-toolkit", version = "0.16.0", default_features = false, features = ["calloop"], optional = true } +sctk-adwaita = { version = "0.4.1", optional = true } +mio = { version = "0.8", features = ["os-ext"], optional = true } +x11-dl = { version = "2.18.5", optional = true } +percent-encoding = { version = "2.0", optional = true } +parking_lot = { version = "0.12.0", optional = true } +libc = "0.2.64" + +[target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] +package = "web-sys" +version = "0.3.22" +features = [ + 'console', + "AddEventListenerOptions", + 'CssStyleDeclaration', + 'BeforeUnloadEvent', + 'Document', + 'DomRect', + 'Element', + 'Event', + 'EventTarget', + 'FocusEvent', + 'HtmlCanvasElement', + 'HtmlElement', + 'KeyboardEvent', + 'MediaQueryList', + 'MediaQueryListEvent', + 'MouseEvent', + 'Node', + 'PointerEvent', + 'Window', + 'WheelEvent' +] + +[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] +version = "0.2.45" + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +console_log = "0.2" + +[package] +name = "winit" +version = "0.27.2" +authors = ["The winit contributors", "Pierre Krieger "] +description = "Cross-platform window creation library." +edition = "2021" +keywords = ["windowing"] +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/rust-windowing/winit" +documentation = "https://docs.rs/winit" +categories = ["gui"] +rust-version = "1.57.0" + +[package.metadata.docs.rs] +features = ["serde"] +default-target = "x86_64-unknown-linux-gnu" +# These are all tested in CI +targets = [ + # Windows + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + # macOS + "x86_64-apple-darwin", + # Unix (X11 & Wayland) + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + # iOS + "x86_64-apple-ios", + # Android + "aarch64-linux-android", + # WebAssembly + "wasm32-unknown-unknown", +] + +[features] +default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +x11 = ["x11-dl", "mio", "percent-encoding", "parking_lot"] +wayland = ["wayland-client", "wayland-protocols", "sctk", "percent-encoding"] +wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"] +wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/title"] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] + +[dependencies] +instant = { version = "0.1", features = ["wasm-bindgen"] } +once_cell = "1.12" +log = "0.4" +serde = { version = "1", optional = true, features = ["serde_derive"] } +raw_window_handle = { package = "raw-window-handle", version = "0.5" } +raw_window_handle_04 = { package = "raw-window-handle", version = "0.4" } +bitflags = "1" +mint = { version = "0.5.6", optional = true } + +[dev-dependencies] +image = { version = "0.24.0", default-features = false, features = ["png"] } +simple_logger = "2.1.0" + +[target.'cfg(target_os = "android")'.dependencies] +# Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 +ndk = "0.7.0" +ndk-glue = "0.7.0" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +objc = "0.2.7" + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.24" +core-foundation = "0.9" +core-graphics = "0.22" +dispatch = "0.2.0" + +[target.'cfg(target_os = "windows")'.dependencies] +parking_lot = "0.12" + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.36" +features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_Media", + "Win32_System_Com_StructuredStorage", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_UI_Accessibility", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Input_Touch", + "Win32_UI_Shell", + "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging", +] + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] +wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true } +wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true } +sctk = { package = "smithay-client-toolkit", version = "0.16.0", default_features = false, features = ["calloop"], optional = true } +sctk-adwaita = { version = "0.4.1", optional = true } +mio = { version = "0.8", features = ["os-ext"], optional = true } +x11-dl = { version = "2.18.5", optional = true } +percent-encoding = { version = "2.0", optional = true } +parking_lot = { version = "0.12.0", optional = true } +libc = "0.2.64" + +[target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] +package = "web-sys" +version = "0.3.22" +features = [ + 'console', + "AddEventListenerOptions", + 'CssStyleDeclaration', + 'BeforeUnloadEvent', + 'Document', + 'DomRect', + 'Element', + 'Event', + 'EventTarget', + 'FocusEvent', + 'HtmlCanvasElement', + 'HtmlElement', + 'KeyboardEvent', + 'MediaQueryList', + 'MediaQueryListEvent', + 'MouseEvent', + 'Node', + 'PointerEvent', + 'Window', + 'WheelEvent' +] + +[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] +version = "0.2.45" + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +console_log = "0.2" + [workspace] default-members = ["winit"] members = ["dpi", "winit*"] diff --git a/src/platform_impl/linux/wayland/env.rs b/src/platform_impl/linux/wayland/env.rs new file mode 100644 index 0000000000..924d52c4fd --- /dev/null +++ b/src/platform_impl/linux/wayland/env.rs @@ -0,0 +1,174 @@ +//! SCTK environment setup. + +use sctk::reexports::client::protocol::wl_compositor::WlCompositor; +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::protocols::unstable::xdg_shell::v6::client::zxdg_shell_v6::ZxdgShellV6; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::protocols::unstable::xdg_decoration::v1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; +use sctk::reexports::client::protocol::wl_shell::WlShell; +use sctk::reexports::client::protocol::wl_subcompositor::WlSubcompositor; +use sctk::reexports::client::{Attached, DispatchData}; +use sctk::reexports::client::protocol::wl_shm::WlShm; +use sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager; +use sctk::reexports::protocols::xdg_shell::client::xdg_wm_base::XdgWmBase; +use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1; +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::environment::{Environment, SimpleGlobal}; +use sctk::output::{OutputHandler, OutputHandling, OutputInfo, OutputStatusListener}; +use sctk::seat::{SeatData, SeatHandler, SeatHandling, SeatListener}; +use sctk::shell::{Shell, ShellHandler, ShellHandling}; +use sctk::shm::ShmHandler; + +/// Set of extra features that are supported by the compositor. +#[derive(Debug, Clone, Copy)] +pub struct WindowingFeatures { + pointer_constraints: bool, + xdg_activation: bool, +} + +impl WindowingFeatures { + /// Create `WindowingFeatures` based on the presented interfaces. + pub fn new(env: &Environment) -> Self { + let pointer_constraints = env.get_global::().is_some(); + let xdg_activation = env.get_global::().is_some(); + Self { + pointer_constraints, + xdg_activation, + } + } + + pub fn pointer_constraints(&self) -> bool { + self.pointer_constraints + } + + pub fn xdg_activation(&self) -> bool { + self.xdg_activation + } +} + +sctk::environment!(WinitEnv, + singles = [ + WlShm => shm, + WlCompositor => compositor, + WlSubcompositor => subcompositor, + WlShell => shell, + XdgWmBase => shell, + ZxdgShellV6 => shell, + ZxdgDecorationManagerV1 => decoration_manager, + ZwpRelativePointerManagerV1 => relative_pointer_manager, + ZwpPointerConstraintsV1 => pointer_constraints, + ZwpTextInputManagerV3 => text_input_manager, + XdgActivationV1 => xdg_activation, + WlDataDeviceManager => data_device_manager, + ], + multis = [ + WlSeat => seats, + WlOutput => outputs, + ] +); + +/// The environment that we utilize. +pub struct WinitEnv { + seats: SeatHandler, + + outputs: OutputHandler, + + shm: ShmHandler, + + compositor: SimpleGlobal, + + subcompositor: SimpleGlobal, + + shell: ShellHandler, + + relative_pointer_manager: SimpleGlobal, + + pointer_constraints: SimpleGlobal, + + text_input_manager: SimpleGlobal, + + decoration_manager: SimpleGlobal, + + xdg_activation: SimpleGlobal, + + data_device_manager: SimpleGlobal, +} + +impl WinitEnv { + pub fn new() -> Self { + // Output tracking for available_monitors, etc. + let outputs = OutputHandler::new(); + + // Keyboard/Pointer/Touch input. + let seats = SeatHandler::new(); + + // Essential globals. + let shm = ShmHandler::new(); + let compositor = SimpleGlobal::new(); + let subcompositor = SimpleGlobal::new(); + + // Gracefully handle shell picking, since SCTK automatically supports multiple + // backends. + let shell = ShellHandler::new(); + + // Server side decorations. + let decoration_manager = SimpleGlobal::new(); + + // Device events for pointer. + let relative_pointer_manager = SimpleGlobal::new(); + + // Pointer grab functionality. + let pointer_constraints = SimpleGlobal::new(); + + // IME handling. + let text_input_manager = SimpleGlobal::new(); + + // Surface activation. + let xdg_activation = SimpleGlobal::new(); + + // Data device manager. + let data_device_manager = SimpleGlobal::new(); + + Self { + seats, + outputs, + shm, + compositor, + subcompositor, + shell, + decoration_manager, + relative_pointer_manager, + pointer_constraints, + text_input_manager, + xdg_activation, + data_device_manager, + } + } +} + +impl ShellHandling for WinitEnv { + fn get_shell(&self) -> Option { + self.shell.get_shell() + } +} + +impl SeatHandling for WinitEnv { + fn listen, &SeatData, DispatchData<'_>) + 'static>( + &mut self, + f: F, + ) -> SeatListener { + self.seats.listen(f) + } +} + +impl OutputHandling for WinitEnv { + fn listen) + 'static>( + &mut self, + f: F, + ) -> OutputStatusListener { + self.outputs.listen(f) + } +} diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs new file mode 100644 index 0000000000..114ec45d0c --- /dev/null +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -0,0 +1,583 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::error::Error; +use std::io::Result as IOResult; +use std::mem; +use std::process; +use std::rc::Rc; +use std::time::{Duration, Instant}; + +use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; + +use sctk::reexports::client::protocol::wl_compositor::WlCompositor; +use sctk::reexports::client::protocol::wl_shm::WlShm; +use sctk::reexports::client::Display; + +use sctk::reexports::calloop; + +use sctk::environment::Environment; +use sctk::seat::pointer::{ThemeManager, ThemeSpec}; +use sctk::WaylandSource; + +use crate::event::{Event, StartCause, WindowEvent}; +use crate::event_loop::{ControlFlow, EventLoopWindowTarget as RootEventLoopWindowTarget}; +use crate::platform_impl::platform::sticky_exit_callback; +use crate::platform_impl::EventLoopWindowTarget as PlatformEventLoopWindowTarget; + +use super::env::{WindowingFeatures, WinitEnv}; +use super::output::OutputManager; +use super::seat::SeatManager; +use super::window::shim::{self, WindowCompositorUpdate, WindowUserRequest}; +use super::{DeviceId, WindowId}; + +mod proxy; +mod sink; +mod state; + +pub use proxy::EventLoopProxy; +pub use sink::EventSink; +pub use state::WinitState; + +type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; + +pub struct EventLoopWindowTarget { + /// Wayland display. + pub display: Display, + + /// Environment to handle object creation, etc. + pub env: Environment, + + /// Event loop handle. + pub event_loop_handle: calloop::LoopHandle<'static, WinitState>, + + /// Output manager. + pub output_manager: OutputManager, + + /// State that we share across callbacks. + pub state: RefCell, + + /// Dispatcher of Wayland events. + pub wayland_dispatcher: WinitDispatcher, + + /// A proxy to wake up event loop. + pub event_loop_awakener: calloop::ping::Ping, + + /// The available windowing features. + pub windowing_features: WindowingFeatures, + + /// Theme manager to manage cursors. + /// + /// It's being shared between all windows to avoid loading + /// multiple similar themes. + pub theme_manager: ThemeManager, + + _marker: std::marker::PhantomData, +} + +impl EventLoopWindowTarget { + pub fn raw_display_handle(&self) -> RawDisplayHandle { + let mut display_handle = WaylandDisplayHandle::empty(); + display_handle.display = self.display.get_display_ptr() as *mut _; + RawDisplayHandle::Wayland(display_handle) + } +} + +pub struct EventLoop { + /// Dispatcher of Wayland events. + pub wayland_dispatcher: WinitDispatcher, + + /// Event loop. + event_loop: calloop::EventLoop<'static, WinitState>, + + /// Wayland display. + display: Display, + + /// Pending user events. + pending_user_events: Rc>>, + + /// Sender of user events. + user_events_sender: calloop::channel::Sender, + + /// Window target. + window_target: RootEventLoopWindowTarget, + + /// Output manager. + _seat_manager: SeatManager, +} + +impl EventLoop { + pub fn new() -> Result, Box> { + // Connect to wayland server and setup event queue. + let display = Display::connect_to_env()?; + let mut event_queue = display.create_event_queue(); + let display_proxy = display.attach(event_queue.token()); + + // Setup environment. + let env = Environment::new(&display_proxy, &mut event_queue, WinitEnv::new())?; + + // Create event loop. + let event_loop = calloop::EventLoop::<'static, WinitState>::try_new()?; + // Build windowing features. + let windowing_features = WindowingFeatures::new(&env); + + // Create a theme manager. + let compositor = env.require_global::(); + let shm = env.require_global::(); + let theme_manager = ThemeManager::init(ThemeSpec::System, compositor, shm); + + // Setup theme seat and output managers. + let seat_manager = SeatManager::new(&env, event_loop.handle(), theme_manager.clone()); + let output_manager = OutputManager::new(&env); + + // A source of events that we plug into our event loop. + let wayland_source = WaylandSource::new(event_queue); + let wayland_dispatcher = + calloop::Dispatcher::new(wayland_source, |_, queue, winit_state| { + queue.dispatch_pending(winit_state, |event, object, _| { + panic!( + "[calloop] Encountered an orphan event: {}@{} : {}", + event.interface, + object.as_ref().id(), + event.name + ); + }) + }); + + let _wayland_source_dispatcher = event_loop + .handle() + .register_dispatcher(wayland_dispatcher.clone())?; + + // A source of user events. + let pending_user_events = Rc::new(RefCell::new(Vec::new())); + let pending_user_events_clone = pending_user_events.clone(); + let (user_events_sender, user_events_channel) = calloop::channel::channel(); + + // User events channel. + event_loop + .handle() + .insert_source(user_events_channel, move |event, _, _| { + if let calloop::channel::Event::Msg(msg) = event { + pending_user_events_clone.borrow_mut().push(msg); + } + })?; + + // An event's loop awakener to wake up for window events from winit's windows. + let (event_loop_awakener, event_loop_awakener_source) = calloop::ping::make_ping()?; + + // Handler of window requests. + event_loop + .handle() + .insert_source(event_loop_awakener_source, move |_, _, state| { + // Drain events here as well to account for application doing batch event processing + // on RedrawEventsCleared. + shim::handle_window_requests(state); + })?; + + let event_loop_handle = event_loop.handle(); + let window_map = HashMap::new(); + let event_sink = EventSink::new(); + let window_user_requests = HashMap::new(); + let window_compositor_updates = HashMap::new(); + + // Create event loop window target. + let event_loop_window_target = EventLoopWindowTarget { + display: display.clone(), + env, + state: RefCell::new(WinitState { + window_map, + event_sink, + window_user_requests, + window_compositor_updates, + display: display.clone(), + }), + event_loop_handle, + output_manager, + event_loop_awakener, + wayland_dispatcher: wayland_dispatcher.clone(), + windowing_features, + theme_manager, + _marker: std::marker::PhantomData, + }; + + // Create event loop itself. + let event_loop = Self { + event_loop, + display, + pending_user_events, + wayland_dispatcher, + _seat_manager: seat_manager, + user_events_sender, + window_target: RootEventLoopWindowTarget { + p: PlatformEventLoopWindowTarget::Wayland(event_loop_window_target), + _marker: std::marker::PhantomData, + }, + }; + + Ok(event_loop) + } + + pub fn run(mut self, callback: F) -> ! + where + F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow) + 'static, + { + let exit_code = self.run_return(callback); + process::exit(exit_code); + } + + pub fn run_return(&mut self, mut callback: F) -> i32 + where + F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + { + let mut control_flow = ControlFlow::Poll; + let pending_user_events = self.pending_user_events.clone(); + + callback( + Event::NewEvents(StartCause::Init), + &self.window_target, + &mut control_flow, + ); + + // NB: For consistency all platforms must emit a 'resumed' event even though Wayland + // applications don't themselves have a formal suspend/resume lifecycle. + callback(Event::Resumed, &self.window_target, &mut control_flow); + + let mut window_compositor_updates: Vec<(WindowId, WindowCompositorUpdate)> = Vec::new(); + let mut window_user_requests: Vec<(WindowId, WindowUserRequest)> = Vec::new(); + let mut event_sink_back_buffer = Vec::new(); + + // NOTE We break on errors from dispatches, since if we've got protocol error + // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is not + // really an option. Instead we inform that the event loop got destroyed. We may + // communicate an error that something was terminated, but winit doesn't provide us + // with an API to do that via some event. + // Still, we set the exit code to the error's OS error code, or to 1 if not possible. + let exit_code = loop { + // Send pending events to the server. + let _ = self.display.flush(); + + // During the run of the user callback, some other code monitoring and reading the + // Wayland socket may have been run (mesa for example does this with vsync), if that + // is the case, some events may have been enqueued in our event queue. + // + // If some messages are there, the event loop needs to behave as if it was instantly + // woken up by messages arriving from the Wayland socket, to avoid delaying the + // dispatch of these events until we're woken up again. + let instant_wakeup = { + let mut wayland_source = self.wayland_dispatcher.as_source_mut(); + let queue = wayland_source.queue(); + let state = match &mut self.window_target.p { + PlatformEventLoopWindowTarget::Wayland(window_target) => { + window_target.state.get_mut() + } + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + match queue.dispatch_pending(state, |_, _, _| unimplemented!()) { + Ok(dispatched) => dispatched > 0, + Err(error) => break error.raw_os_error().unwrap_or(1), + } + }; + + match control_flow { + ControlFlow::ExitWithCode(code) => break code, + ControlFlow::Poll => { + // Non-blocking dispatch. + let timeout = Duration::from_millis(0); + if let Err(error) = self.loop_dispatch(Some(timeout)) { + break error.raw_os_error().unwrap_or(1); + } + + callback( + Event::NewEvents(StartCause::Poll), + &self.window_target, + &mut control_flow, + ); + } + ControlFlow::Wait => { + let timeout = if instant_wakeup { + Some(Duration::from_millis(0)) + } else { + None + }; + + if let Err(error) = self.loop_dispatch(timeout) { + break error.raw_os_error().unwrap_or(1); + } + + callback( + Event::NewEvents(StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + }), + &self.window_target, + &mut control_flow, + ); + } + ControlFlow::WaitUntil(deadline) => { + let start = Instant::now(); + + // Compute the amount of time we'll block for. + let duration = if deadline > start && !instant_wakeup { + deadline - start + } else { + Duration::from_millis(0) + }; + + if let Err(error) = self.loop_dispatch(Some(duration)) { + break error.raw_os_error().unwrap_or(1); + } + + let now = Instant::now(); + + if now < deadline { + callback( + Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: Some(deadline), + }), + &self.window_target, + &mut control_flow, + ) + } else { + callback( + Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume: deadline, + }), + &self.window_target, + &mut control_flow, + ) + } + } + } + + // Handle pending user events. We don't need back buffer, since we can't dispatch + // user events indirectly via callback to the user. + for user_event in pending_user_events.borrow_mut().drain(..) { + sticky_exit_callback( + Event::UserEvent(user_event), + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + + // Process 'new' pending updates from compositor. + self.with_state(|state| { + window_compositor_updates.clear(); + window_compositor_updates.extend( + state + .window_compositor_updates + .iter_mut() + .map(|(wid, window_update)| (*wid, mem::take(window_update))), + ); + }); + + for (window_id, window_compositor_update) in window_compositor_updates.iter_mut() { + if let Some(scale_factor) = window_compositor_update.scale_factor.map(|f| f as f64) + { + let mut physical_size = self.with_state(|state| { + let window_handle = state.window_map.get(window_id).unwrap(); + let mut size = window_handle.size.lock().unwrap(); + + // Update the new logical size if it was changed. + let window_size = window_compositor_update.size.unwrap_or(*size); + *size = window_size; + + window_size.to_physical(scale_factor) + }); + + sticky_exit_callback( + Event::WindowEvent { + window_id: crate::window::WindowId(*window_id), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: &mut physical_size, + }, + }, + &self.window_target, + &mut control_flow, + &mut callback, + ); + + // We don't update size on a window handle since we'll do that later + // when handling size update. + let new_logical_size = physical_size.to_logical(scale_factor); + window_compositor_update.size = Some(new_logical_size); + } + + if let Some(size) = window_compositor_update.size.take() { + let physical_size = self.with_state(|state| { + let window_handle = state.window_map.get_mut(window_id).unwrap(); + let mut window_size = window_handle.size.lock().unwrap(); + + // Always issue resize event on scale factor change. + let physical_size = if window_compositor_update.scale_factor.is_none() + && *window_size == size + { + // The size hasn't changed, don't inform downstream about that. + None + } else { + *window_size = size; + let scale_factor = + sctk::get_surface_scale_factor(window_handle.window.surface()); + let physical_size = size.to_physical(scale_factor as f64); + Some(physical_size) + }; + + // We still perform all of those resize related logic even if the size + // hasn't changed, since GNOME relies on `set_geometry` calls after + // configures. + window_handle.window.resize(size.width, size.height); + window_handle.window.refresh(); + + // Mark that refresh isn't required, since we've done it right now. + state + .window_user_requests + .get_mut(window_id) + .unwrap() + .refresh_frame = false; + + physical_size + }); + + if let Some(physical_size) = physical_size { + sticky_exit_callback( + Event::WindowEvent { + window_id: crate::window::WindowId(*window_id), + event: WindowEvent::Resized(physical_size), + }, + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + } + + // If the close is requested, send it here. + if window_compositor_update.close_window { + sticky_exit_callback( + Event::WindowEvent { + window_id: crate::window::WindowId(*window_id), + event: WindowEvent::CloseRequested, + }, + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + } + + // The purpose of the back buffer and that swap is to not hold borrow_mut when + // we're doing callback to the user, since we can double borrow if the user decides + // to create a window in one of those callbacks. + self.with_state(|state| { + std::mem::swap( + &mut event_sink_back_buffer, + &mut state.event_sink.window_events, + ) + }); + + // Handle pending window events. + for event in event_sink_back_buffer.drain(..) { + let event = event.map_nonuser_event().unwrap(); + sticky_exit_callback(event, &self.window_target, &mut control_flow, &mut callback); + } + + // Send events cleared. + sticky_exit_callback( + Event::MainEventsCleared, + &self.window_target, + &mut control_flow, + &mut callback, + ); + + // Apply user requests, so every event required resize and latter surface commit will + // be applied right before drawing. This will also ensure that every `RedrawRequested` + // event will be delivered in time. + self.with_state(|state| { + shim::handle_window_requests(state); + }); + + // Process 'new' pending updates from compositor. + self.with_state(|state| { + window_user_requests.clear(); + window_user_requests.extend( + state + .window_user_requests + .iter_mut() + .map(|(wid, window_request)| (*wid, mem::take(window_request))), + ); + }); + + // Handle RedrawRequested events. + for (window_id, mut window_request) in window_user_requests.iter() { + // Handle refresh of the frame. + if window_request.refresh_frame { + self.with_state(|state| { + let window_handle = state.window_map.get_mut(window_id).unwrap(); + window_handle.window.refresh(); + }); + + // In general refreshing the frame requires surface commit, those force user + // to redraw. + window_request.redraw_requested = true; + } + + // Handle redraw request. + if window_request.redraw_requested { + sticky_exit_callback( + Event::RedrawRequested(crate::window::WindowId(*window_id)), + &self.window_target, + &mut control_flow, + &mut callback, + ); + } + } + + // Send RedrawEventCleared. + sticky_exit_callback( + Event::RedrawEventsCleared, + &self.window_target, + &mut control_flow, + &mut callback, + ); + }; + + callback(Event::LoopDestroyed, &self.window_target, &mut control_flow); + exit_code + } + + #[inline] + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.user_events_sender.clone()) + } + + #[inline] + pub fn window_target(&self) -> &RootEventLoopWindowTarget { + &self.window_target + } + + fn with_state U>(&mut self, f: F) -> U { + let state = match &mut self.window_target.p { + PlatformEventLoopWindowTarget::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + f(state) + } + + fn loop_dispatch>>(&mut self, timeout: D) -> IOResult<()> { + let state = match &mut self.window_target.p { + PlatformEventLoopWindowTarget::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + self.event_loop + .dispatch(timeout, state) + .map_err(|error| error.into()) + } +} diff --git a/src/platform_impl/linux/wayland/event_loop/state.rs b/src/platform_impl/linux/wayland/event_loop/state.rs new file mode 100644 index 0000000000..dae74c8de8 --- /dev/null +++ b/src/platform_impl/linux/wayland/event_loop/state.rs @@ -0,0 +1,36 @@ +//! A state that we pass around in a dispatch. + +use std::collections::HashMap; + +use wayland_client::Display; + +use super::EventSink; +use crate::platform_impl::wayland::window::shim::{ + WindowCompositorUpdate, WindowHandle, WindowUserRequest, +}; +use crate::platform_impl::wayland::WindowId; + +/// Wrapper to carry winit's state. +pub struct WinitState { + /// A sink for window and device events that is being filled during dispatching + /// event loop and forwarded downstream afterwards. + pub event_sink: EventSink, + + /// Window updates comming from the user requests. Those are separatelly dispatched right after + /// `MainEventsCleared`. + pub window_user_requests: HashMap, + + /// Window updates, which are coming from SCTK or the compositor, which require + /// calling back to the winit's downstream. They are handled right in the event loop, + /// unlike the ones coming from buffers on the `WindowHandle`'s. + pub window_compositor_updates: HashMap, + + /// Window map containing all SCTK windows. Since those windows aren't allowed + /// to be sent to other threads, they live on the event loop's thread + /// and requests from winit's windows are being forwarded to them either via + /// `WindowUpdate` or buffer on the associated with it `WindowHandle`. + pub window_map: HashMap, + + /// Handle to the display, to flush events when needed. + pub display: Display, +} diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs new file mode 100644 index 0000000000..113e3f2185 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -0,0 +1,96 @@ +use std::{ + io::{self, BufRead, BufReader}, + path::PathBuf, +}; + +use percent_encoding::percent_decode_str; +use sctk::data_device::{DataOffer, DndEvent}; +use wayland_client::Display; + +use crate::{event::WindowEvent, platform_impl::wayland::event_loop::WinitState}; + +use super::DndInner; + +const MIME_TYPE: &str = "text/uri-list"; + +pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: &mut WinitState) { + match event { + DndEvent::Enter { + offer: Some(offer), + surface, + .. + } => { + let window_id = match winit_state + .window_map + .iter() + .find(|(_, window)| window.window.surface() == &surface) + { + Some((id, _)) => *id, + None => return, + }; + + if let Ok(paths) = parse_offer(&winit_state.display, offer) { + if !paths.is_empty() { + offer.accept(Some(MIME_TYPE.into())); + for path in paths { + winit_state + .event_sink + .push_window_event(WindowEvent::HoveredFile(path), window_id); + } + inner.window_id = Some(window_id); + } + } + } + DndEvent::Drop { offer: Some(offer) } => { + if let Some(window_id) = inner.window_id { + inner.window_id = None; + + if let Ok(paths) = parse_offer(&winit_state.display, offer) { + for path in paths { + winit_state + .event_sink + .push_window_event(WindowEvent::DroppedFile(path), window_id); + } + } + } + } + DndEvent::Leave => { + if let Some(window_id) = inner.window_id { + inner.window_id = None; + + winit_state + .event_sink + .push_window_event(WindowEvent::HoveredFileCancelled, window_id); + } + } + _ => {} + } +} + +fn parse_offer(display: &Display, offer: &DataOffer) -> io::Result> { + let can_accept = offer.with_mime_types(|types| types.iter().any(|s| s == MIME_TYPE)); + if can_accept { + // Format: https://www.iana.org/assignments/media-types/text/uri-list + let mut paths = Vec::new(); + let pipe = offer.receive(MIME_TYPE.into())?; + let _ = display.flush(); + for line in BufReader::new(pipe).lines() { + let line = line?; + if line.starts_with('#') { + continue; + } + + let decoded = match percent_decode_str(&line).decode_utf8() { + Ok(decoded) => decoded, + Err(_) => continue, + }; + let start = "file://"; + if decoded.starts_with(start) { + paths.push(PathBuf::from(&decoded[start.len()..])); + } + } + Ok(paths) + } else { + Ok(Vec::new()) + } +} diff --git a/src/platform_impl/linux/wayland/seat/dnd/mod.rs b/src/platform_impl/linux/wayland/seat/dnd/mod.rs new file mode 100644 index 0000000000..9f808a1526 --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/dnd/mod.rs @@ -0,0 +1,32 @@ +mod handlers; + +use sctk::data_device::DataDevice; +use wayland_client::{ + protocol::{wl_data_device_manager::WlDataDeviceManager, wl_seat::WlSeat}, + Attached, +}; + +use crate::platform_impl::{wayland::event_loop::WinitState, WindowId}; + +pub(crate) struct Dnd { + _data_device: DataDevice, +} + +impl Dnd { + pub fn new(seat: &Attached, manager: &WlDataDeviceManager) -> Self { + let mut inner = DndInner { window_id: None }; + let data_device = + DataDevice::init_for_seat(manager, seat, move |event, mut dispatch_data| { + let winit_state = dispatch_data.get::().unwrap(); + handlers::handle_dnd(event, &mut inner, winit_state); + }); + Self { + _data_device: data_device, + } + } +} + +struct DndInner { + /// Window ID of the currently hovered window. + window_id: Option, +} diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs new file mode 100644 index 0000000000..d5c862425b --- /dev/null +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -0,0 +1,230 @@ +//! Seat handling and managing. + +use std::cell::RefCell; +use std::rc::Rc; + +use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1; +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::client::protocol::wl_data_device_manager::WlDataDeviceManager; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::Attached; + +use sctk::environment::Environment; +use sctk::reexports::calloop::LoopHandle; +use sctk::seat::pointer::ThemeManager; +use sctk::seat::{SeatData, SeatListener}; + +use super::env::WinitEnv; +use super::event_loop::WinitState; +use crate::event::ModifiersState; + +mod dnd; +mod keyboard; +pub mod pointer; +pub mod text_input; +mod touch; + +use dnd::Dnd; +use keyboard::Keyboard; +use pointer::Pointers; +use text_input::TextInput; +use touch::Touch; + +pub struct SeatManager { + /// Listener for seats. + _seat_listener: SeatListener, +} + +impl SeatManager { + pub fn new( + env: &Environment, + loop_handle: LoopHandle<'static, WinitState>, + theme_manager: ThemeManager, + ) -> Self { + let relative_pointer_manager = env.get_global::(); + let pointer_constraints = env.get_global::(); + let text_input_manager = env.get_global::(); + let data_device_manager = env.get_global::(); + + let mut inner = SeatManagerInner::new( + theme_manager, + relative_pointer_manager, + pointer_constraints, + text_input_manager, + data_device_manager, + loop_handle, + ); + + // Handle existing seats. + for seat in env.get_all_seats() { + let seat_data = match sctk::seat::clone_seat_data(&seat) { + Some(seat_data) => seat_data, + None => continue, + }; + + inner.process_seat_update(&seat, &seat_data); + } + + let seat_listener = env.listen_for_seats(move |seat, seat_data, _| { + inner.process_seat_update(&seat, seat_data); + }); + + Self { + _seat_listener: seat_listener, + } + } +} + +/// Inner state of the seat manager. +struct SeatManagerInner { + /// Currently observed seats. + seats: Vec, + + /// Loop handle. + loop_handle: LoopHandle<'static, WinitState>, + + /// Relative pointer manager. + relative_pointer_manager: Option>, + + /// Pointer constraints. + pointer_constraints: Option>, + + /// Text input manager. + text_input_manager: Option>, + + /// Data device manager (for Drag and Drop). + data_device_manager: Option>, + + /// A theme manager. + theme_manager: ThemeManager, +} + +impl SeatManagerInner { + fn new( + theme_manager: ThemeManager, + relative_pointer_manager: Option>, + pointer_constraints: Option>, + text_input_manager: Option>, + data_device_manager: Option>, + loop_handle: LoopHandle<'static, WinitState>, + ) -> Self { + Self { + seats: Vec::new(), + loop_handle, + relative_pointer_manager, + pointer_constraints, + text_input_manager, + data_device_manager, + theme_manager, + } + } + + /// Handle seats update from the `SeatListener`. + pub fn process_seat_update(&mut self, seat: &Attached, seat_data: &SeatData) { + let detached_seat = seat.detach(); + + let position = self.seats.iter().position(|si| si.seat == detached_seat); + let index = position.unwrap_or_else(|| { + self.seats.push(SeatInfo::new(detached_seat)); + self.seats.len() - 1 + }); + + let seat_info = &mut self.seats[index]; + + // Pointer handling. + if seat_data.has_pointer && !seat_data.defunct { + if seat_info.pointer.is_none() { + seat_info.pointer = Some(Pointers::new( + seat, + &self.theme_manager, + &self.relative_pointer_manager, + &self.pointer_constraints, + seat_info.modifiers_state.clone(), + )); + } + } else { + seat_info.pointer = None; + } + + // Handle keyboard. + if seat_data.has_keyboard && !seat_data.defunct { + if seat_info.keyboard.is_none() { + seat_info.keyboard = Keyboard::new( + seat, + self.loop_handle.clone(), + seat_info.modifiers_state.clone(), + ); + } + } else { + seat_info.keyboard = None; + } + + // Handle touch. + if seat_data.has_touch && !seat_data.defunct { + if seat_info.touch.is_none() { + seat_info.touch = Some(Touch::new(seat)); + } + } else { + seat_info.touch = None; + } + + // Handle text input. + if let Some(text_input_manager) = self.text_input_manager.as_ref() { + if seat_data.defunct { + seat_info.text_input = None; + } else if seat_info.text_input.is_none() { + seat_info.text_input = Some(TextInput::new(seat, text_input_manager)); + } + } + + if let Some(data_device_manager) = self.data_device_manager.as_ref() { + if seat_data.defunct { + seat_info.dnd = None; + } else { + seat_info.dnd = Some(Dnd::new(seat, &data_device_manager)); + } + } + } +} + +/// Resources associtated with a given seat. +struct SeatInfo { + /// Seat to which this `SeatInfo` belongs. + seat: WlSeat, + + /// A keyboard handle with its repeat rate handling. + keyboard: Option, + + /// All pointers we're using on a seat. + pointer: Option, + + /// Touch handling. + touch: Option, + + /// Text input handling aka IME. + text_input: Option, + + /// Drag and Drop handler. + dnd: Option, + + /// The current state of modifiers observed in keyboard handler. + /// + /// We keep modifiers state on a seat, since it's being used by pointer events as well. + modifiers_state: Rc>, +} + +impl SeatInfo { + pub fn new(seat: WlSeat) -> Self { + Self { + seat, + keyboard: None, + pointer: None, + touch: None, + text_input: None, + dnd: None, + modifiers_state: Rc::new(RefCell::new(ModifiersState::default())), + } + } +} From 809ae2c0d88e0346309d0ce54373bc9d8a013930 Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Sat, 13 Aug 2022 02:04:00 +0200 Subject: [PATCH 02/87] Address clippy lints --- src/platform_impl/linux/wayland/seat/dnd/handlers.rs | 5 ++--- src/platform_impl/linux/wayland/seat/mod.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs index 113e3f2185..b68e24b767 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -84,9 +84,8 @@ fn parse_offer(display: &Display, offer: &DataOffer) -> io::Result> Ok(decoded) => decoded, Err(_) => continue, }; - let start = "file://"; - if decoded.starts_with(start) { - paths.push(PathBuf::from(&decoded[start.len()..])); + if let Some(path) = decoded.strip_prefix("file://") { + paths.push(PathBuf::from(path)); } } Ok(paths) diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs index d5c862425b..5ff1b84f8b 100644 --- a/src/platform_impl/linux/wayland/seat/mod.rs +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -183,7 +183,7 @@ impl SeatManagerInner { if seat_data.defunct { seat_info.dnd = None; } else { - seat_info.dnd = Some(Dnd::new(seat, &data_device_manager)); + seat_info.dnd = Some(Dnd::new(seat, data_device_manager)); } } } From 31a4de14405d6ccb3c21015581a5cc0967145597 Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Sat, 13 Aug 2022 17:29:51 +0200 Subject: [PATCH 03/87] Use `make_wid` to simplify code --- .../linux/wayland/seat/dnd/handlers.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs index b68e24b767..cab880bab4 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -7,7 +7,10 @@ use percent_encoding::percent_decode_str; use sctk::data_device::{DataOffer, DndEvent}; use wayland_client::Display; -use crate::{event::WindowEvent, platform_impl::wayland::event_loop::WinitState}; +use crate::{ + event::WindowEvent, + platform_impl::wayland::{event_loop::WinitState, make_wid}, +}; use super::DndInner; @@ -20,14 +23,7 @@ pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: surface, .. } => { - let window_id = match winit_state - .window_map - .iter() - .find(|(_, window)| window.window.surface() == &surface) - { - Some((id, _)) => *id, - None => return, - }; + let window_id = make_wid(&surface); if let Ok(paths) = parse_offer(&winit_state.display, offer) { if !paths.is_empty() { From 9d88e5eb94493b7f8bc229f8a52bf66b6ac048c7 Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Sat, 13 Aug 2022 17:33:26 +0200 Subject: [PATCH 04/87] Also emit all appropriate cursor events --- .../linux/wayland/seat/dnd/handlers.rs | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs index cab880bab4..8cdfa339f8 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -8,8 +8,9 @@ use sctk::data_device::{DataOffer, DndEvent}; use wayland_client::Display; use crate::{ + dpi::PhysicalPosition, event::WindowEvent, - platform_impl::wayland::{event_loop::WinitState, make_wid}, + platform_impl::wayland::{event_loop::WinitState, make_wid, DeviceId}, }; use super::DndInner; @@ -21,6 +22,8 @@ pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: DndEvent::Enter { offer: Some(offer), surface, + x, + y, .. } => { let window_id = make_wid(&surface); @@ -28,6 +31,26 @@ pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: if let Ok(paths) = parse_offer(&winit_state.display, offer) { if !paths.is_empty() { offer.accept(Some(MIME_TYPE.into())); + + winit_state.event_sink.push_window_event( + WindowEvent::CursorEntered { + device_id: crate::event::DeviceId( + crate::platform_impl::DeviceId::Wayland(DeviceId), + ), + }, + window_id, + ); + winit_state.event_sink.push_window_event( + WindowEvent::CursorMoved { + device_id: crate::event::DeviceId( + crate::platform_impl::DeviceId::Wayland(DeviceId), + ), + position: PhysicalPosition::new(x, y), + modifiers: Default::default(), + }, + window_id, + ); + for path in paths { winit_state .event_sink @@ -57,6 +80,28 @@ pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: winit_state .event_sink .push_window_event(WindowEvent::HoveredFileCancelled, window_id); + winit_state.event_sink.push_window_event( + WindowEvent::CursorLeft { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + }, + window_id, + ); + } + } + DndEvent::Motion { x, y, .. } => { + if let Some(window_id) = inner.window_id { + winit_state.event_sink.push_window_event( + WindowEvent::CursorMoved { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + position: PhysicalPosition::new(x, y), + modifiers: Default::default(), + }, + window_id, + ); } } _ => {} From 2b0dd480e819b61506c70d2ab721f326eafc418c Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Fri, 19 Aug 2022 02:35:05 +0200 Subject: [PATCH 05/87] Read the pipe without blocking --- .../linux/wayland/event_loop/mod.rs | 1 - .../linux/wayland/event_loop/state.rs | 5 - .../linux/wayland/seat/dnd/handlers.rs | 113 +++++++++++++----- .../linux/wayland/seat/dnd/mod.rs | 14 ++- src/platform_impl/linux/wayland/seat/mod.rs | 6 +- 5 files changed, 100 insertions(+), 39 deletions(-) diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs index 114ec45d0c..60f951fc33 100644 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -188,7 +188,6 @@ impl EventLoop { event_sink, window_user_requests, window_compositor_updates, - display: display.clone(), }), event_loop_handle, output_manager, diff --git a/src/platform_impl/linux/wayland/event_loop/state.rs b/src/platform_impl/linux/wayland/event_loop/state.rs index dae74c8de8..0cf1c6680e 100644 --- a/src/platform_impl/linux/wayland/event_loop/state.rs +++ b/src/platform_impl/linux/wayland/event_loop/state.rs @@ -2,8 +2,6 @@ use std::collections::HashMap; -use wayland_client::Display; - use super::EventSink; use crate::platform_impl::wayland::window::shim::{ WindowCompositorUpdate, WindowHandle, WindowUserRequest, @@ -30,7 +28,4 @@ pub struct WinitState { /// and requests from winit's windows are being forwarded to them either via /// `WindowUpdate` or buffer on the associated with it `WindowHandle`. pub window_map: HashMap, - - /// Handle to the display, to flush events when needed. - pub display: Display, } diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs index 8cdfa339f8..fe83e651c5 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -1,11 +1,15 @@ use std::{ - io::{self, BufRead, BufReader}, + io::{self, Read}, + os::unix::prelude::{AsRawFd, RawFd}, path::PathBuf, + str, }; use percent_encoding::percent_decode_str; -use sctk::data_device::{DataOffer, DndEvent}; -use wayland_client::Display; +use sctk::{ + data_device::{DataOffer, DndEvent, ReadPipe}, + reexports::calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction}, +}; use crate::{ dpi::PhysicalPosition, @@ -27,11 +31,10 @@ pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: .. } => { let window_id = make_wid(&surface); - - if let Ok(paths) = parse_offer(&winit_state.display, offer) { + inner.window_id = Some(window_id); + offer.accept(Some(MIME_TYPE.into())); + let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| { if !paths.is_empty() { - offer.accept(Some(MIME_TYPE.into())); - winit_state.event_sink.push_window_event( WindowEvent::CursorEntered { device_id: crate::event::DeviceId( @@ -56,21 +59,20 @@ pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: .event_sink .push_window_event(WindowEvent::HoveredFile(path), window_id); } - inner.window_id = Some(window_id); } - } + }); } DndEvent::Drop { offer: Some(offer) } => { if let Some(window_id) = inner.window_id { inner.window_id = None; - if let Ok(paths) = parse_offer(&winit_state.display, offer) { + let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| { for path in paths { winit_state .event_sink .push_window_event(WindowEvent::DroppedFile(path), window_id); } - } + }); } } DndEvent::Leave => { @@ -108,29 +110,82 @@ pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: } } -fn parse_offer(display: &Display, offer: &DataOffer) -> io::Result> { +fn parse_offer( + loop_handle: &LoopHandle<'static, WinitState>, + offer: &DataOffer, + mut callback: impl FnMut(Vec, &mut WinitState) + 'static, +) -> io::Result<()> { let can_accept = offer.with_mime_types(|types| types.iter().any(|s| s == MIME_TYPE)); if can_accept { - // Format: https://www.iana.org/assignments/media-types/text/uri-list - let mut paths = Vec::new(); let pipe = offer.receive(MIME_TYPE.into())?; - let _ = display.flush(); - for line in BufReader::new(pipe).lines() { - let line = line?; - if line.starts_with('#') { - continue; + read_pipe_nonblocking(pipe, loop_handle, move |bytes, winit_state| { + // Format: https://www.iana.org/assignments/media-types/text/uri-list + let mut paths = Vec::new(); + for line in bytes.split(|b| *b == b'\n') { + let line = match str::from_utf8(line) { + Ok(line) => line, + Err(_) => continue, + }; + + if line.starts_with('#') { + continue; + } + + let decoded = match percent_decode_str(&line).decode_utf8() { + Ok(decoded) => decoded, + Err(_) => continue, + }; + if let Some(path) = decoded.strip_prefix("file://") { + paths.push(PathBuf::from(path)); + } } + callback(paths, winit_state); + })?; + } + Ok(()) +} - let decoded = match percent_decode_str(&line).decode_utf8() { - Ok(decoded) => decoded, - Err(_) => continue, - }; - if let Some(path) = decoded.strip_prefix("file://") { - paths.push(PathBuf::from(path)); +fn read_pipe_nonblocking( + pipe: ReadPipe, + loop_handle: &LoopHandle<'static, WinitState>, + mut callback: impl FnMut(Vec, &mut WinitState) + 'static, +) -> io::Result<()> { + unsafe { + make_fd_nonblocking(pipe.as_raw_fd())?; + } + + let mut content = Vec::::with_capacity(u16::MAX as usize); + let mut reader_buffer = [0; u16::MAX as usize]; + let reader = Generic::new(pipe, 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 data = std::mem::take(&mut content); + callback(data, winit_state); + break PostAction::Remove; + } + Ok(n) => content.extend_from_slice(&reader_buffer[..n]), + Err(err) if err.kind() == io::ErrorKind::WouldBlock => break PostAction::Continue, + Err(_) => break PostAction::Remove, } - } - Ok(paths) - } else { - Ok(Vec::new()) + }; + + Ok(action) + }); + Ok(()) +} + +unsafe fn make_fd_nonblocking(raw_fd: RawFd) -> io::Result<()> { + let flags = libc::fcntl(raw_fd, libc::F_GETFL); + if flags < 0 { + return Err(io::Error::from_raw_os_error(flags)); } + let result = libc::fcntl(raw_fd, libc::F_SETFL, flags | libc::O_NONBLOCK); + if result < 0 { + return Err(io::Error::from_raw_os_error(result)); + } + + Ok(()) } diff --git a/src/platform_impl/linux/wayland/seat/dnd/mod.rs b/src/platform_impl/linux/wayland/seat/dnd/mod.rs index 9f808a1526..8a66f9a995 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/mod.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/mod.rs @@ -1,6 +1,6 @@ mod handlers; -use sctk::data_device::DataDevice; +use sctk::{data_device::DataDevice, reexports::calloop::LoopHandle}; use wayland_client::{ protocol::{wl_data_device_manager::WlDataDeviceManager, wl_seat::WlSeat}, Attached, @@ -13,8 +13,15 @@ pub(crate) struct Dnd { } impl Dnd { - pub fn new(seat: &Attached, manager: &WlDataDeviceManager) -> Self { - let mut inner = DndInner { window_id: None }; + pub fn new( + seat: &Attached, + manager: &WlDataDeviceManager, + loop_handle: LoopHandle<'static, WinitState>, + ) -> Self { + let mut inner = DndInner { + loop_handle, + window_id: None, + }; let data_device = DataDevice::init_for_seat(manager, seat, move |event, mut dispatch_data| { let winit_state = dispatch_data.get::().unwrap(); @@ -27,6 +34,7 @@ impl Dnd { } struct DndInner { + loop_handle: LoopHandle<'static, WinitState>, /// Window ID of the currently hovered window. window_id: Option, } diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs index 5ff1b84f8b..ccbb829d29 100644 --- a/src/platform_impl/linux/wayland/seat/mod.rs +++ b/src/platform_impl/linux/wayland/seat/mod.rs @@ -183,7 +183,11 @@ impl SeatManagerInner { if seat_data.defunct { seat_info.dnd = None; } else { - seat_info.dnd = Some(Dnd::new(seat, data_device_manager)); + seat_info.dnd = Some(Dnd::new( + seat, + data_device_manager, + self.loop_handle.clone(), + )); } } } From 26876545bf1de426ac3e07c515c064ee915a4b3c Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Fri, 19 Aug 2022 02:36:38 +0200 Subject: [PATCH 06/87] Follow style --- .../linux/wayland/seat/dnd/handlers.rs | 26 +++++++------------ .../linux/wayland/seat/dnd/mod.rs | 10 +++---- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs index fe83e651c5..5667b5d9e3 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -1,21 +1,15 @@ -use std::{ - io::{self, Read}, - os::unix::prelude::{AsRawFd, RawFd}, - path::PathBuf, - str, -}; +use std::io::{self, Read}; +use std::os::unix::prelude::{AsRawFd, RawFd}; +use std::path::PathBuf; +use std::str; use percent_encoding::percent_decode_str; -use sctk::{ - data_device::{DataOffer, DndEvent, ReadPipe}, - reexports::calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction}, -}; - -use crate::{ - dpi::PhysicalPosition, - event::WindowEvent, - platform_impl::wayland::{event_loop::WinitState, make_wid, DeviceId}, -}; +use sctk::data_device::{DataOffer, DndEvent, ReadPipe}; +use sctk::reexports::calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction}; + +use crate::dpi::PhysicalPosition; +use crate::event::WindowEvent; +use crate::platform_impl::wayland::{event_loop::WinitState, make_wid, DeviceId}; use super::DndInner; diff --git a/src/platform_impl/linux/wayland/seat/dnd/mod.rs b/src/platform_impl/linux/wayland/seat/dnd/mod.rs index 8a66f9a995..db1a7d518d 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/mod.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/mod.rs @@ -1,13 +1,11 @@ -mod handlers; - use sctk::{data_device::DataDevice, reexports::calloop::LoopHandle}; -use wayland_client::{ - protocol::{wl_data_device_manager::WlDataDeviceManager, wl_seat::WlSeat}, - Attached, -}; +use wayland_client::protocol::{wl_data_device_manager::WlDataDeviceManager, wl_seat::WlSeat}; +use wayland_client::Attached; use crate::platform_impl::{wayland::event_loop::WinitState, WindowId}; +mod handlers; + pub(crate) struct Dnd { _data_device: DataDevice, } From d95dd413a0b76ab639276ed6026a3df301c0ea54 Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Fri, 19 Aug 2022 02:44:58 +0200 Subject: [PATCH 07/87] Address clippy lint --- src/platform_impl/linux/wayland/seat/dnd/handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs index 5667b5d9e3..6a7b4e9143 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -125,7 +125,7 @@ fn parse_offer( continue; } - let decoded = match percent_decode_str(&line).decode_utf8() { + let decoded = match percent_decode_str(line).decode_utf8() { Ok(decoded) => decoded, Err(_) => continue, }; From 60749aa9f64932d43d5d5316ab0900a47ea7366b Mon Sep 17 00:00:00 2001 From: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Wed, 24 Aug 2022 00:06:15 +0200 Subject: [PATCH 08/87] Fix parsing of CRLF-delimited lines --- src/platform_impl/linux/wayland/seat/dnd/handlers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs index 6a7b4e9143..2bfcad49aa 100644 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs @@ -1,4 +1,4 @@ -use std::io::{self, Read}; +use std::io::{self, BufRead, Read}; use std::os::unix::prelude::{AsRawFd, RawFd}; use std::path::PathBuf; use std::str; @@ -115,8 +115,8 @@ fn parse_offer( read_pipe_nonblocking(pipe, loop_handle, move |bytes, winit_state| { // Format: https://www.iana.org/assignments/media-types/text/uri-list let mut paths = Vec::new(); - for line in bytes.split(|b| *b == b'\n') { - let line = match str::from_utf8(line) { + for line in bytes.lines() { + let line = match line { Ok(line) => line, Err(_) => continue, }; @@ -125,7 +125,7 @@ fn parse_offer( continue; } - let decoded = match percent_decode_str(line).decode_utf8() { + let decoded = match percent_decode_str(&line).decode_utf8() { Ok(decoded) => decoded, Err(_) => continue, }; From 2c15c5e7ec9dc79796e4f400da1acff6520db05d Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 19 May 2026 13:52:21 +0200 Subject: [PATCH 09/87] Fix merge conflicts in Cargo.toml --- Cargo.toml | 292 ----------------------------------------------------- 1 file changed, 292 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 417f7cb230..3321df958d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,295 +1,3 @@ -[package] -name = "winit" -version = "0.27.2" -authors = ["The winit contributors", "Pierre Krieger "] -description = "Cross-platform window creation library." -edition = "2021" -keywords = ["windowing"] -license = "Apache-2.0" -readme = "README.md" -repository = "https://github.com/rust-windowing/winit" -documentation = "https://docs.rs/winit" -categories = ["gui"] -rust-version = "1.57.0" - -[package.metadata.docs.rs] -features = ["serde"] -default-target = "x86_64-unknown-linux-gnu" -# These are all tested in CI -targets = [ - # Windows - "i686-pc-windows-msvc", - "x86_64-pc-windows-msvc", - # macOS - "x86_64-apple-darwin", - # Unix (X11 & Wayland) - "i686-unknown-linux-gnu", - "x86_64-unknown-linux-gnu", - # iOS - "x86_64-apple-ios", - # Android - "aarch64-linux-android", - # WebAssembly - "wasm32-unknown-unknown", -] - -[features] -default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -x11 = ["x11-dl", "mio", "percent-encoding", "parking_lot"] -wayland = ["wayland-client", "wayland-protocols", "sctk"] -wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"] -wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/title"] -wayland-csd-adwaita-notitle = ["sctk-adwaita"] - -[dependencies] -instant = { version = "0.1", features = ["wasm-bindgen"] } -once_cell = "1.12" -log = "0.4" -serde = { version = "1", optional = true, features = ["serde_derive"] } -raw_window_handle = { package = "raw-window-handle", version = "0.5" } -raw_window_handle_04 = { package = "raw-window-handle", version = "0.4" } -bitflags = "1" -mint = { version = "0.5.6", optional = true } - -[dev-dependencies] -image = { version = "0.24.0", default-features = false, features = ["png"] } -simple_logger = "2.1.0" - -[target.'cfg(target_os = "android")'.dependencies] -# Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 -ndk = "0.7.0" -ndk-glue = "0.7.0" - -[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] -objc = "0.2.7" - -[target.'cfg(target_os = "macos")'.dependencies] -cocoa = "0.24" -core-foundation = "0.9" -core-graphics = "0.22" -dispatch = "0.2.0" - -[target.'cfg(target_os = "windows")'.dependencies] -parking_lot = "0.12" - -[target.'cfg(target_os = "windows")'.dependencies.windows-sys] -version = "0.36" -features = [ - "Win32_Devices_HumanInterfaceDevice", - "Win32_Foundation", - "Win32_Globalization", - "Win32_Graphics_Dwm", - "Win32_Graphics_Gdi", - "Win32_Media", - "Win32_System_Com_StructuredStorage", - "Win32_System_Com", - "Win32_System_LibraryLoader", - "Win32_System_Ole", - "Win32_System_SystemInformation", - "Win32_System_SystemServices", - "Win32_System_Threading", - "Win32_System_WindowsProgramming", - "Win32_UI_Accessibility", - "Win32_UI_Controls", - "Win32_UI_HiDpi", - "Win32_UI_Input_Ime", - "Win32_UI_Input_KeyboardAndMouse", - "Win32_UI_Input_Pointer", - "Win32_UI_Input_Touch", - "Win32_UI_Shell", - "Win32_UI_TextServices", - "Win32_UI_WindowsAndMessaging", -] - -[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] -wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true } -wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true } -sctk = { package = "smithay-client-toolkit", version = "0.16.0", default_features = false, features = ["calloop"], optional = true } -sctk-adwaita = { version = "0.4.1", optional = true } -mio = { version = "0.8", features = ["os-ext"], optional = true } -x11-dl = { version = "2.18.5", optional = true } -percent-encoding = { version = "2.0", optional = true } -parking_lot = { version = "0.12.0", optional = true } -libc = "0.2.64" - -[target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] -package = "web-sys" -version = "0.3.22" -features = [ - 'console', - "AddEventListenerOptions", - 'CssStyleDeclaration', - 'BeforeUnloadEvent', - 'Document', - 'DomRect', - 'Element', - 'Event', - 'EventTarget', - 'FocusEvent', - 'HtmlCanvasElement', - 'HtmlElement', - 'KeyboardEvent', - 'MediaQueryList', - 'MediaQueryListEvent', - 'MouseEvent', - 'Node', - 'PointerEvent', - 'Window', - 'WheelEvent' -] - -[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] -version = "0.2.45" - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -console_log = "0.2" - -[package] -name = "winit" -version = "0.27.2" -authors = ["The winit contributors", "Pierre Krieger "] -description = "Cross-platform window creation library." -edition = "2021" -keywords = ["windowing"] -license = "Apache-2.0" -readme = "README.md" -repository = "https://github.com/rust-windowing/winit" -documentation = "https://docs.rs/winit" -categories = ["gui"] -rust-version = "1.57.0" - -[package.metadata.docs.rs] -features = ["serde"] -default-target = "x86_64-unknown-linux-gnu" -# These are all tested in CI -targets = [ - # Windows - "i686-pc-windows-msvc", - "x86_64-pc-windows-msvc", - # macOS - "x86_64-apple-darwin", - # Unix (X11 & Wayland) - "i686-unknown-linux-gnu", - "x86_64-unknown-linux-gnu", - # iOS - "x86_64-apple-ios", - # Android - "aarch64-linux-android", - # WebAssembly - "wasm32-unknown-unknown", -] - -[features] -default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -x11 = ["x11-dl", "mio", "percent-encoding", "parking_lot"] -wayland = ["wayland-client", "wayland-protocols", "sctk", "percent-encoding"] -wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"] -wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/title"] -wayland-csd-adwaita-notitle = ["sctk-adwaita"] - -[dependencies] -instant = { version = "0.1", features = ["wasm-bindgen"] } -once_cell = "1.12" -log = "0.4" -serde = { version = "1", optional = true, features = ["serde_derive"] } -raw_window_handle = { package = "raw-window-handle", version = "0.5" } -raw_window_handle_04 = { package = "raw-window-handle", version = "0.4" } -bitflags = "1" -mint = { version = "0.5.6", optional = true } - -[dev-dependencies] -image = { version = "0.24.0", default-features = false, features = ["png"] } -simple_logger = "2.1.0" - -[target.'cfg(target_os = "android")'.dependencies] -# Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 -ndk = "0.7.0" -ndk-glue = "0.7.0" - -[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] -objc = "0.2.7" - -[target.'cfg(target_os = "macos")'.dependencies] -cocoa = "0.24" -core-foundation = "0.9" -core-graphics = "0.22" -dispatch = "0.2.0" - -[target.'cfg(target_os = "windows")'.dependencies] -parking_lot = "0.12" - -[target.'cfg(target_os = "windows")'.dependencies.windows-sys] -version = "0.36" -features = [ - "Win32_Devices_HumanInterfaceDevice", - "Win32_Foundation", - "Win32_Globalization", - "Win32_Graphics_Dwm", - "Win32_Graphics_Gdi", - "Win32_Media", - "Win32_System_Com_StructuredStorage", - "Win32_System_Com", - "Win32_System_LibraryLoader", - "Win32_System_Ole", - "Win32_System_SystemInformation", - "Win32_System_SystemServices", - "Win32_System_Threading", - "Win32_System_WindowsProgramming", - "Win32_UI_Accessibility", - "Win32_UI_Controls", - "Win32_UI_HiDpi", - "Win32_UI_Input_Ime", - "Win32_UI_Input_KeyboardAndMouse", - "Win32_UI_Input_Pointer", - "Win32_UI_Input_Touch", - "Win32_UI_Shell", - "Win32_UI_TextServices", - "Win32_UI_WindowsAndMessaging", -] - -[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] -wayland-client = { version = "0.29.4", default_features = false, features = ["use_system_lib"], optional = true } -wayland-protocols = { version = "0.29.4", features = [ "staging_protocols"], optional = true } -sctk = { package = "smithay-client-toolkit", version = "0.16.0", default_features = false, features = ["calloop"], optional = true } -sctk-adwaita = { version = "0.4.1", optional = true } -mio = { version = "0.8", features = ["os-ext"], optional = true } -x11-dl = { version = "2.18.5", optional = true } -percent-encoding = { version = "2.0", optional = true } -parking_lot = { version = "0.12.0", optional = true } -libc = "0.2.64" - -[target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] -package = "web-sys" -version = "0.3.22" -features = [ - 'console', - "AddEventListenerOptions", - 'CssStyleDeclaration', - 'BeforeUnloadEvent', - 'Document', - 'DomRect', - 'Element', - 'Event', - 'EventTarget', - 'FocusEvent', - 'HtmlCanvasElement', - 'HtmlElement', - 'KeyboardEvent', - 'MediaQueryList', - 'MediaQueryListEvent', - 'MouseEvent', - 'Node', - 'PointerEvent', - 'Window', - 'WheelEvent' -] - -[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] -version = "0.2.45" - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -console_log = "0.2" - [workspace] default-members = ["winit"] members = ["dpi", "winit*"] From ccd7116cce16a6a6a2c75812b1507d3f2066c82e Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 19 May 2026 18:22:55 +0200 Subject: [PATCH 10/87] Fix merge conflicts in `platform_impl`, start implementing data transfer API --- Cargo.toml | 1 + src/platform_impl/linux/wayland/env.rs | 174 ------ .../linux/wayland/event_loop/mod.rs | 582 ------------------ .../linux/wayland/event_loop/state.rs | 31 - .../linux/wayland/seat/dnd/handlers.rs | 185 ------ .../linux/wayland/seat/dnd/mod.rs | 38 -- src/platform_impl/linux/wayland/seat/mod.rs | 234 ------- winit-core/Cargo.toml | 2 + winit-core/src/data_transfer.rs | 210 +++++++ winit-core/src/event.rs | 73 ++- winit-core/src/lib.rs | 1 + winit-core/src/window.rs | 51 +- winit-wayland/Cargo.toml | 1 + winit-wayland/src/seat/dnd/mod.rs | 183 ++++++ winit-wayland/src/seat/mod.rs | 12 + winit-wayland/src/state.rs | 16 + winit-x11/src/event_processor.rs | 12 +- 17 files changed, 523 insertions(+), 1283 deletions(-) delete mode 100644 src/platform_impl/linux/wayland/env.rs delete mode 100644 src/platform_impl/linux/wayland/event_loop/mod.rs delete mode 100644 src/platform_impl/linux/wayland/event_loop/state.rs delete mode 100644 src/platform_impl/linux/wayland/seat/dnd/handlers.rs delete mode 100644 src/platform_impl/linux/wayland/seat/dnd/mod.rs delete mode 100644 src/platform_impl/linux/wayland/seat/mod.rs create mode 100644 winit-core/src/data_transfer.rs create mode 100644 winit-wayland/src/seat/dnd/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 3321df958d..93d6c970a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ calloop = "0.14.3" foldhash = { version = "0.2.0", default-features = false, features = ["std"] } libc = "0.2.64" memmap2 = "0.9.0" +mime = "0.3.17" percent-encoding = "2.0" rustix = { version = "1.0.7", default-features = false } x11-dl = "2.19.1" diff --git a/src/platform_impl/linux/wayland/env.rs b/src/platform_impl/linux/wayland/env.rs deleted file mode 100644 index 924d52c4fd..0000000000 --- a/src/platform_impl/linux/wayland/env.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! SCTK environment setup. - -use sctk::reexports::client::protocol::wl_compositor::WlCompositor; -use sctk::reexports::client::protocol::wl_output::WlOutput; -use sctk::reexports::protocols::unstable::xdg_shell::v6::client::zxdg_shell_v6::ZxdgShellV6; -use sctk::reexports::client::protocol::wl_seat::WlSeat; -use sctk::reexports::protocols::unstable::xdg_decoration::v1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; -use sctk::reexports::client::protocol::wl_shell::WlShell; -use sctk::reexports::client::protocol::wl_subcompositor::WlSubcompositor; -use sctk::reexports::client::{Attached, DispatchData}; -use sctk::reexports::client::protocol::wl_shm::WlShm; -use sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager; -use sctk::reexports::protocols::xdg_shell::client::xdg_wm_base::XdgWmBase; -use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1; -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::environment::{Environment, SimpleGlobal}; -use sctk::output::{OutputHandler, OutputHandling, OutputInfo, OutputStatusListener}; -use sctk::seat::{SeatData, SeatHandler, SeatHandling, SeatListener}; -use sctk::shell::{Shell, ShellHandler, ShellHandling}; -use sctk::shm::ShmHandler; - -/// Set of extra features that are supported by the compositor. -#[derive(Debug, Clone, Copy)] -pub struct WindowingFeatures { - pointer_constraints: bool, - xdg_activation: bool, -} - -impl WindowingFeatures { - /// Create `WindowingFeatures` based on the presented interfaces. - pub fn new(env: &Environment) -> Self { - let pointer_constraints = env.get_global::().is_some(); - let xdg_activation = env.get_global::().is_some(); - Self { - pointer_constraints, - xdg_activation, - } - } - - pub fn pointer_constraints(&self) -> bool { - self.pointer_constraints - } - - pub fn xdg_activation(&self) -> bool { - self.xdg_activation - } -} - -sctk::environment!(WinitEnv, - singles = [ - WlShm => shm, - WlCompositor => compositor, - WlSubcompositor => subcompositor, - WlShell => shell, - XdgWmBase => shell, - ZxdgShellV6 => shell, - ZxdgDecorationManagerV1 => decoration_manager, - ZwpRelativePointerManagerV1 => relative_pointer_manager, - ZwpPointerConstraintsV1 => pointer_constraints, - ZwpTextInputManagerV3 => text_input_manager, - XdgActivationV1 => xdg_activation, - WlDataDeviceManager => data_device_manager, - ], - multis = [ - WlSeat => seats, - WlOutput => outputs, - ] -); - -/// The environment that we utilize. -pub struct WinitEnv { - seats: SeatHandler, - - outputs: OutputHandler, - - shm: ShmHandler, - - compositor: SimpleGlobal, - - subcompositor: SimpleGlobal, - - shell: ShellHandler, - - relative_pointer_manager: SimpleGlobal, - - pointer_constraints: SimpleGlobal, - - text_input_manager: SimpleGlobal, - - decoration_manager: SimpleGlobal, - - xdg_activation: SimpleGlobal, - - data_device_manager: SimpleGlobal, -} - -impl WinitEnv { - pub fn new() -> Self { - // Output tracking for available_monitors, etc. - let outputs = OutputHandler::new(); - - // Keyboard/Pointer/Touch input. - let seats = SeatHandler::new(); - - // Essential globals. - let shm = ShmHandler::new(); - let compositor = SimpleGlobal::new(); - let subcompositor = SimpleGlobal::new(); - - // Gracefully handle shell picking, since SCTK automatically supports multiple - // backends. - let shell = ShellHandler::new(); - - // Server side decorations. - let decoration_manager = SimpleGlobal::new(); - - // Device events for pointer. - let relative_pointer_manager = SimpleGlobal::new(); - - // Pointer grab functionality. - let pointer_constraints = SimpleGlobal::new(); - - // IME handling. - let text_input_manager = SimpleGlobal::new(); - - // Surface activation. - let xdg_activation = SimpleGlobal::new(); - - // Data device manager. - let data_device_manager = SimpleGlobal::new(); - - Self { - seats, - outputs, - shm, - compositor, - subcompositor, - shell, - decoration_manager, - relative_pointer_manager, - pointer_constraints, - text_input_manager, - xdg_activation, - data_device_manager, - } - } -} - -impl ShellHandling for WinitEnv { - fn get_shell(&self) -> Option { - self.shell.get_shell() - } -} - -impl SeatHandling for WinitEnv { - fn listen, &SeatData, DispatchData<'_>) + 'static>( - &mut self, - f: F, - ) -> SeatListener { - self.seats.listen(f) - } -} - -impl OutputHandling for WinitEnv { - fn listen) + 'static>( - &mut self, - f: F, - ) -> OutputStatusListener { - self.outputs.listen(f) - } -} diff --git a/src/platform_impl/linux/wayland/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs deleted file mode 100644 index 60f951fc33..0000000000 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ /dev/null @@ -1,582 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::error::Error; -use std::io::Result as IOResult; -use std::mem; -use std::process; -use std::rc::Rc; -use std::time::{Duration, Instant}; - -use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; - -use sctk::reexports::client::protocol::wl_compositor::WlCompositor; -use sctk::reexports::client::protocol::wl_shm::WlShm; -use sctk::reexports::client::Display; - -use sctk::reexports::calloop; - -use sctk::environment::Environment; -use sctk::seat::pointer::{ThemeManager, ThemeSpec}; -use sctk::WaylandSource; - -use crate::event::{Event, StartCause, WindowEvent}; -use crate::event_loop::{ControlFlow, EventLoopWindowTarget as RootEventLoopWindowTarget}; -use crate::platform_impl::platform::sticky_exit_callback; -use crate::platform_impl::EventLoopWindowTarget as PlatformEventLoopWindowTarget; - -use super::env::{WindowingFeatures, WinitEnv}; -use super::output::OutputManager; -use super::seat::SeatManager; -use super::window::shim::{self, WindowCompositorUpdate, WindowUserRequest}; -use super::{DeviceId, WindowId}; - -mod proxy; -mod sink; -mod state; - -pub use proxy::EventLoopProxy; -pub use sink::EventSink; -pub use state::WinitState; - -type WinitDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; - -pub struct EventLoopWindowTarget { - /// Wayland display. - pub display: Display, - - /// Environment to handle object creation, etc. - pub env: Environment, - - /// Event loop handle. - pub event_loop_handle: calloop::LoopHandle<'static, WinitState>, - - /// Output manager. - pub output_manager: OutputManager, - - /// State that we share across callbacks. - pub state: RefCell, - - /// Dispatcher of Wayland events. - pub wayland_dispatcher: WinitDispatcher, - - /// A proxy to wake up event loop. - pub event_loop_awakener: calloop::ping::Ping, - - /// The available windowing features. - pub windowing_features: WindowingFeatures, - - /// Theme manager to manage cursors. - /// - /// It's being shared between all windows to avoid loading - /// multiple similar themes. - pub theme_manager: ThemeManager, - - _marker: std::marker::PhantomData, -} - -impl EventLoopWindowTarget { - pub fn raw_display_handle(&self) -> RawDisplayHandle { - let mut display_handle = WaylandDisplayHandle::empty(); - display_handle.display = self.display.get_display_ptr() as *mut _; - RawDisplayHandle::Wayland(display_handle) - } -} - -pub struct EventLoop { - /// Dispatcher of Wayland events. - pub wayland_dispatcher: WinitDispatcher, - - /// Event loop. - event_loop: calloop::EventLoop<'static, WinitState>, - - /// Wayland display. - display: Display, - - /// Pending user events. - pending_user_events: Rc>>, - - /// Sender of user events. - user_events_sender: calloop::channel::Sender, - - /// Window target. - window_target: RootEventLoopWindowTarget, - - /// Output manager. - _seat_manager: SeatManager, -} - -impl EventLoop { - pub fn new() -> Result, Box> { - // Connect to wayland server and setup event queue. - let display = Display::connect_to_env()?; - let mut event_queue = display.create_event_queue(); - let display_proxy = display.attach(event_queue.token()); - - // Setup environment. - let env = Environment::new(&display_proxy, &mut event_queue, WinitEnv::new())?; - - // Create event loop. - let event_loop = calloop::EventLoop::<'static, WinitState>::try_new()?; - // Build windowing features. - let windowing_features = WindowingFeatures::new(&env); - - // Create a theme manager. - let compositor = env.require_global::(); - let shm = env.require_global::(); - let theme_manager = ThemeManager::init(ThemeSpec::System, compositor, shm); - - // Setup theme seat and output managers. - let seat_manager = SeatManager::new(&env, event_loop.handle(), theme_manager.clone()); - let output_manager = OutputManager::new(&env); - - // A source of events that we plug into our event loop. - let wayland_source = WaylandSource::new(event_queue); - let wayland_dispatcher = - calloop::Dispatcher::new(wayland_source, |_, queue, winit_state| { - queue.dispatch_pending(winit_state, |event, object, _| { - panic!( - "[calloop] Encountered an orphan event: {}@{} : {}", - event.interface, - object.as_ref().id(), - event.name - ); - }) - }); - - let _wayland_source_dispatcher = event_loop - .handle() - .register_dispatcher(wayland_dispatcher.clone())?; - - // A source of user events. - let pending_user_events = Rc::new(RefCell::new(Vec::new())); - let pending_user_events_clone = pending_user_events.clone(); - let (user_events_sender, user_events_channel) = calloop::channel::channel(); - - // User events channel. - event_loop - .handle() - .insert_source(user_events_channel, move |event, _, _| { - if let calloop::channel::Event::Msg(msg) = event { - pending_user_events_clone.borrow_mut().push(msg); - } - })?; - - // An event's loop awakener to wake up for window events from winit's windows. - let (event_loop_awakener, event_loop_awakener_source) = calloop::ping::make_ping()?; - - // Handler of window requests. - event_loop - .handle() - .insert_source(event_loop_awakener_source, move |_, _, state| { - // Drain events here as well to account for application doing batch event processing - // on RedrawEventsCleared. - shim::handle_window_requests(state); - })?; - - let event_loop_handle = event_loop.handle(); - let window_map = HashMap::new(); - let event_sink = EventSink::new(); - let window_user_requests = HashMap::new(); - let window_compositor_updates = HashMap::new(); - - // Create event loop window target. - let event_loop_window_target = EventLoopWindowTarget { - display: display.clone(), - env, - state: RefCell::new(WinitState { - window_map, - event_sink, - window_user_requests, - window_compositor_updates, - }), - event_loop_handle, - output_manager, - event_loop_awakener, - wayland_dispatcher: wayland_dispatcher.clone(), - windowing_features, - theme_manager, - _marker: std::marker::PhantomData, - }; - - // Create event loop itself. - let event_loop = Self { - event_loop, - display, - pending_user_events, - wayland_dispatcher, - _seat_manager: seat_manager, - user_events_sender, - window_target: RootEventLoopWindowTarget { - p: PlatformEventLoopWindowTarget::Wayland(event_loop_window_target), - _marker: std::marker::PhantomData, - }, - }; - - Ok(event_loop) - } - - pub fn run(mut self, callback: F) -> ! - where - F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow) + 'static, - { - let exit_code = self.run_return(callback); - process::exit(exit_code); - } - - pub fn run_return(&mut self, mut callback: F) -> i32 - where - F: FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), - { - let mut control_flow = ControlFlow::Poll; - let pending_user_events = self.pending_user_events.clone(); - - callback( - Event::NewEvents(StartCause::Init), - &self.window_target, - &mut control_flow, - ); - - // NB: For consistency all platforms must emit a 'resumed' event even though Wayland - // applications don't themselves have a formal suspend/resume lifecycle. - callback(Event::Resumed, &self.window_target, &mut control_flow); - - let mut window_compositor_updates: Vec<(WindowId, WindowCompositorUpdate)> = Vec::new(); - let mut window_user_requests: Vec<(WindowId, WindowUserRequest)> = Vec::new(); - let mut event_sink_back_buffer = Vec::new(); - - // NOTE We break on errors from dispatches, since if we've got protocol error - // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is not - // really an option. Instead we inform that the event loop got destroyed. We may - // communicate an error that something was terminated, but winit doesn't provide us - // with an API to do that via some event. - // Still, we set the exit code to the error's OS error code, or to 1 if not possible. - let exit_code = loop { - // Send pending events to the server. - let _ = self.display.flush(); - - // During the run of the user callback, some other code monitoring and reading the - // Wayland socket may have been run (mesa for example does this with vsync), if that - // is the case, some events may have been enqueued in our event queue. - // - // If some messages are there, the event loop needs to behave as if it was instantly - // woken up by messages arriving from the Wayland socket, to avoid delaying the - // dispatch of these events until we're woken up again. - let instant_wakeup = { - let mut wayland_source = self.wayland_dispatcher.as_source_mut(); - let queue = wayland_source.queue(); - let state = match &mut self.window_target.p { - PlatformEventLoopWindowTarget::Wayland(window_target) => { - window_target.state.get_mut() - } - #[cfg(feature = "x11")] - _ => unreachable!(), - }; - - match queue.dispatch_pending(state, |_, _, _| unimplemented!()) { - Ok(dispatched) => dispatched > 0, - Err(error) => break error.raw_os_error().unwrap_or(1), - } - }; - - match control_flow { - ControlFlow::ExitWithCode(code) => break code, - ControlFlow::Poll => { - // Non-blocking dispatch. - let timeout = Duration::from_millis(0); - if let Err(error) = self.loop_dispatch(Some(timeout)) { - break error.raw_os_error().unwrap_or(1); - } - - callback( - Event::NewEvents(StartCause::Poll), - &self.window_target, - &mut control_flow, - ); - } - ControlFlow::Wait => { - let timeout = if instant_wakeup { - Some(Duration::from_millis(0)) - } else { - None - }; - - if let Err(error) = self.loop_dispatch(timeout) { - break error.raw_os_error().unwrap_or(1); - } - - callback( - Event::NewEvents(StartCause::WaitCancelled { - start: Instant::now(), - requested_resume: None, - }), - &self.window_target, - &mut control_flow, - ); - } - ControlFlow::WaitUntil(deadline) => { - let start = Instant::now(); - - // Compute the amount of time we'll block for. - let duration = if deadline > start && !instant_wakeup { - deadline - start - } else { - Duration::from_millis(0) - }; - - if let Err(error) = self.loop_dispatch(Some(duration)) { - break error.raw_os_error().unwrap_or(1); - } - - let now = Instant::now(); - - if now < deadline { - callback( - Event::NewEvents(StartCause::WaitCancelled { - start, - requested_resume: Some(deadline), - }), - &self.window_target, - &mut control_flow, - ) - } else { - callback( - Event::NewEvents(StartCause::ResumeTimeReached { - start, - requested_resume: deadline, - }), - &self.window_target, - &mut control_flow, - ) - } - } - } - - // Handle pending user events. We don't need back buffer, since we can't dispatch - // user events indirectly via callback to the user. - for user_event in pending_user_events.borrow_mut().drain(..) { - sticky_exit_callback( - Event::UserEvent(user_event), - &self.window_target, - &mut control_flow, - &mut callback, - ); - } - - // Process 'new' pending updates from compositor. - self.with_state(|state| { - window_compositor_updates.clear(); - window_compositor_updates.extend( - state - .window_compositor_updates - .iter_mut() - .map(|(wid, window_update)| (*wid, mem::take(window_update))), - ); - }); - - for (window_id, window_compositor_update) in window_compositor_updates.iter_mut() { - if let Some(scale_factor) = window_compositor_update.scale_factor.map(|f| f as f64) - { - let mut physical_size = self.with_state(|state| { - let window_handle = state.window_map.get(window_id).unwrap(); - let mut size = window_handle.size.lock().unwrap(); - - // Update the new logical size if it was changed. - let window_size = window_compositor_update.size.unwrap_or(*size); - *size = window_size; - - window_size.to_physical(scale_factor) - }); - - sticky_exit_callback( - Event::WindowEvent { - window_id: crate::window::WindowId(*window_id), - event: WindowEvent::ScaleFactorChanged { - scale_factor, - new_inner_size: &mut physical_size, - }, - }, - &self.window_target, - &mut control_flow, - &mut callback, - ); - - // We don't update size on a window handle since we'll do that later - // when handling size update. - let new_logical_size = physical_size.to_logical(scale_factor); - window_compositor_update.size = Some(new_logical_size); - } - - if let Some(size) = window_compositor_update.size.take() { - let physical_size = self.with_state(|state| { - let window_handle = state.window_map.get_mut(window_id).unwrap(); - let mut window_size = window_handle.size.lock().unwrap(); - - // Always issue resize event on scale factor change. - let physical_size = if window_compositor_update.scale_factor.is_none() - && *window_size == size - { - // The size hasn't changed, don't inform downstream about that. - None - } else { - *window_size = size; - let scale_factor = - sctk::get_surface_scale_factor(window_handle.window.surface()); - let physical_size = size.to_physical(scale_factor as f64); - Some(physical_size) - }; - - // We still perform all of those resize related logic even if the size - // hasn't changed, since GNOME relies on `set_geometry` calls after - // configures. - window_handle.window.resize(size.width, size.height); - window_handle.window.refresh(); - - // Mark that refresh isn't required, since we've done it right now. - state - .window_user_requests - .get_mut(window_id) - .unwrap() - .refresh_frame = false; - - physical_size - }); - - if let Some(physical_size) = physical_size { - sticky_exit_callback( - Event::WindowEvent { - window_id: crate::window::WindowId(*window_id), - event: WindowEvent::Resized(physical_size), - }, - &self.window_target, - &mut control_flow, - &mut callback, - ); - } - } - - // If the close is requested, send it here. - if window_compositor_update.close_window { - sticky_exit_callback( - Event::WindowEvent { - window_id: crate::window::WindowId(*window_id), - event: WindowEvent::CloseRequested, - }, - &self.window_target, - &mut control_flow, - &mut callback, - ); - } - } - - // The purpose of the back buffer and that swap is to not hold borrow_mut when - // we're doing callback to the user, since we can double borrow if the user decides - // to create a window in one of those callbacks. - self.with_state(|state| { - std::mem::swap( - &mut event_sink_back_buffer, - &mut state.event_sink.window_events, - ) - }); - - // Handle pending window events. - for event in event_sink_back_buffer.drain(..) { - let event = event.map_nonuser_event().unwrap(); - sticky_exit_callback(event, &self.window_target, &mut control_flow, &mut callback); - } - - // Send events cleared. - sticky_exit_callback( - Event::MainEventsCleared, - &self.window_target, - &mut control_flow, - &mut callback, - ); - - // Apply user requests, so every event required resize and latter surface commit will - // be applied right before drawing. This will also ensure that every `RedrawRequested` - // event will be delivered in time. - self.with_state(|state| { - shim::handle_window_requests(state); - }); - - // Process 'new' pending updates from compositor. - self.with_state(|state| { - window_user_requests.clear(); - window_user_requests.extend( - state - .window_user_requests - .iter_mut() - .map(|(wid, window_request)| (*wid, mem::take(window_request))), - ); - }); - - // Handle RedrawRequested events. - for (window_id, mut window_request) in window_user_requests.iter() { - // Handle refresh of the frame. - if window_request.refresh_frame { - self.with_state(|state| { - let window_handle = state.window_map.get_mut(window_id).unwrap(); - window_handle.window.refresh(); - }); - - // In general refreshing the frame requires surface commit, those force user - // to redraw. - window_request.redraw_requested = true; - } - - // Handle redraw request. - if window_request.redraw_requested { - sticky_exit_callback( - Event::RedrawRequested(crate::window::WindowId(*window_id)), - &self.window_target, - &mut control_flow, - &mut callback, - ); - } - } - - // Send RedrawEventCleared. - sticky_exit_callback( - Event::RedrawEventsCleared, - &self.window_target, - &mut control_flow, - &mut callback, - ); - }; - - callback(Event::LoopDestroyed, &self.window_target, &mut control_flow); - exit_code - } - - #[inline] - pub fn create_proxy(&self) -> EventLoopProxy { - EventLoopProxy::new(self.user_events_sender.clone()) - } - - #[inline] - pub fn window_target(&self) -> &RootEventLoopWindowTarget { - &self.window_target - } - - fn with_state U>(&mut self, f: F) -> U { - let state = match &mut self.window_target.p { - PlatformEventLoopWindowTarget::Wayland(window_target) => window_target.state.get_mut(), - #[cfg(feature = "x11")] - _ => unreachable!(), - }; - - f(state) - } - - fn loop_dispatch>>(&mut self, timeout: D) -> IOResult<()> { - let state = match &mut self.window_target.p { - PlatformEventLoopWindowTarget::Wayland(window_target) => window_target.state.get_mut(), - #[cfg(feature = "x11")] - _ => unreachable!(), - }; - - self.event_loop - .dispatch(timeout, state) - .map_err(|error| error.into()) - } -} diff --git a/src/platform_impl/linux/wayland/event_loop/state.rs b/src/platform_impl/linux/wayland/event_loop/state.rs deleted file mode 100644 index 0cf1c6680e..0000000000 --- a/src/platform_impl/linux/wayland/event_loop/state.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! A state that we pass around in a dispatch. - -use std::collections::HashMap; - -use super::EventSink; -use crate::platform_impl::wayland::window::shim::{ - WindowCompositorUpdate, WindowHandle, WindowUserRequest, -}; -use crate::platform_impl::wayland::WindowId; - -/// Wrapper to carry winit's state. -pub struct WinitState { - /// A sink for window and device events that is being filled during dispatching - /// event loop and forwarded downstream afterwards. - pub event_sink: EventSink, - - /// Window updates comming from the user requests. Those are separatelly dispatched right after - /// `MainEventsCleared`. - pub window_user_requests: HashMap, - - /// Window updates, which are coming from SCTK or the compositor, which require - /// calling back to the winit's downstream. They are handled right in the event loop, - /// unlike the ones coming from buffers on the `WindowHandle`'s. - pub window_compositor_updates: HashMap, - - /// Window map containing all SCTK windows. Since those windows aren't allowed - /// to be sent to other threads, they live on the event loop's thread - /// and requests from winit's windows are being forwarded to them either via - /// `WindowUpdate` or buffer on the associated with it `WindowHandle`. - pub window_map: HashMap, -} diff --git a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs b/src/platform_impl/linux/wayland/seat/dnd/handlers.rs deleted file mode 100644 index 2bfcad49aa..0000000000 --- a/src/platform_impl/linux/wayland/seat/dnd/handlers.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::io::{self, BufRead, Read}; -use std::os::unix::prelude::{AsRawFd, RawFd}; -use std::path::PathBuf; -use std::str; - -use percent_encoding::percent_decode_str; -use sctk::data_device::{DataOffer, DndEvent, ReadPipe}; -use sctk::reexports::calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction}; - -use crate::dpi::PhysicalPosition; -use crate::event::WindowEvent; -use crate::platform_impl::wayland::{event_loop::WinitState, make_wid, DeviceId}; - -use super::DndInner; - -const MIME_TYPE: &str = "text/uri-list"; - -pub(super) fn handle_dnd(event: DndEvent<'_>, inner: &mut DndInner, winit_state: &mut WinitState) { - match event { - DndEvent::Enter { - offer: Some(offer), - surface, - x, - y, - .. - } => { - let window_id = make_wid(&surface); - inner.window_id = Some(window_id); - offer.accept(Some(MIME_TYPE.into())); - let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| { - if !paths.is_empty() { - winit_state.event_sink.push_window_event( - WindowEvent::CursorEntered { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - }, - window_id, - ); - winit_state.event_sink.push_window_event( - WindowEvent::CursorMoved { - device_id: crate::event::DeviceId( - crate::platform_impl::DeviceId::Wayland(DeviceId), - ), - position: PhysicalPosition::new(x, y), - modifiers: Default::default(), - }, - window_id, - ); - - for path in paths { - winit_state - .event_sink - .push_window_event(WindowEvent::HoveredFile(path), window_id); - } - } - }); - } - DndEvent::Drop { offer: Some(offer) } => { - if let Some(window_id) = inner.window_id { - inner.window_id = None; - - let _ = parse_offer(&inner.loop_handle, offer, move |paths, winit_state| { - for path in paths { - winit_state - .event_sink - .push_window_event(WindowEvent::DroppedFile(path), window_id); - } - }); - } - } - DndEvent::Leave => { - if let Some(window_id) = inner.window_id { - inner.window_id = None; - - winit_state - .event_sink - .push_window_event(WindowEvent::HoveredFileCancelled, window_id); - winit_state.event_sink.push_window_event( - WindowEvent::CursorLeft { - device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( - DeviceId, - )), - }, - window_id, - ); - } - } - DndEvent::Motion { x, y, .. } => { - if let Some(window_id) = inner.window_id { - winit_state.event_sink.push_window_event( - WindowEvent::CursorMoved { - device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( - DeviceId, - )), - position: PhysicalPosition::new(x, y), - modifiers: Default::default(), - }, - window_id, - ); - } - } - _ => {} - } -} - -fn parse_offer( - loop_handle: &LoopHandle<'static, WinitState>, - offer: &DataOffer, - mut callback: impl FnMut(Vec, &mut WinitState) + 'static, -) -> io::Result<()> { - let can_accept = offer.with_mime_types(|types| types.iter().any(|s| s == MIME_TYPE)); - if can_accept { - let pipe = offer.receive(MIME_TYPE.into())?; - read_pipe_nonblocking(pipe, loop_handle, move |bytes, winit_state| { - // Format: https://www.iana.org/assignments/media-types/text/uri-list - let mut paths = Vec::new(); - for line in bytes.lines() { - let line = match line { - Ok(line) => line, - Err(_) => continue, - }; - - if line.starts_with('#') { - continue; - } - - let decoded = match percent_decode_str(&line).decode_utf8() { - Ok(decoded) => decoded, - Err(_) => continue, - }; - if let Some(path) = decoded.strip_prefix("file://") { - paths.push(PathBuf::from(path)); - } - } - callback(paths, winit_state); - })?; - } - Ok(()) -} - -fn read_pipe_nonblocking( - pipe: ReadPipe, - loop_handle: &LoopHandle<'static, WinitState>, - mut callback: impl FnMut(Vec, &mut WinitState) + 'static, -) -> io::Result<()> { - unsafe { - make_fd_nonblocking(pipe.as_raw_fd())?; - } - - let mut content = Vec::::with_capacity(u16::MAX as usize); - let mut reader_buffer = [0; u16::MAX as usize]; - let reader = Generic::new(pipe, 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 data = std::mem::take(&mut content); - callback(data, winit_state); - break PostAction::Remove; - } - Ok(n) => content.extend_from_slice(&reader_buffer[..n]), - Err(err) if err.kind() == io::ErrorKind::WouldBlock => break PostAction::Continue, - Err(_) => break PostAction::Remove, - } - }; - - Ok(action) - }); - Ok(()) -} - -unsafe fn make_fd_nonblocking(raw_fd: RawFd) -> io::Result<()> { - let flags = libc::fcntl(raw_fd, libc::F_GETFL); - if flags < 0 { - return Err(io::Error::from_raw_os_error(flags)); - } - let result = libc::fcntl(raw_fd, libc::F_SETFL, flags | libc::O_NONBLOCK); - if result < 0 { - return Err(io::Error::from_raw_os_error(result)); - } - - Ok(()) -} diff --git a/src/platform_impl/linux/wayland/seat/dnd/mod.rs b/src/platform_impl/linux/wayland/seat/dnd/mod.rs deleted file mode 100644 index db1a7d518d..0000000000 --- a/src/platform_impl/linux/wayland/seat/dnd/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -use sctk::{data_device::DataDevice, reexports::calloop::LoopHandle}; -use wayland_client::protocol::{wl_data_device_manager::WlDataDeviceManager, wl_seat::WlSeat}; -use wayland_client::Attached; - -use crate::platform_impl::{wayland::event_loop::WinitState, WindowId}; - -mod handlers; - -pub(crate) struct Dnd { - _data_device: DataDevice, -} - -impl Dnd { - pub fn new( - seat: &Attached, - manager: &WlDataDeviceManager, - loop_handle: LoopHandle<'static, WinitState>, - ) -> Self { - let mut inner = DndInner { - loop_handle, - window_id: None, - }; - let data_device = - DataDevice::init_for_seat(manager, seat, move |event, mut dispatch_data| { - let winit_state = dispatch_data.get::().unwrap(); - handlers::handle_dnd(event, &mut inner, winit_state); - }); - Self { - _data_device: data_device, - } - } -} - -struct DndInner { - loop_handle: LoopHandle<'static, WinitState>, - /// Window ID of the currently hovered window. - window_id: Option, -} diff --git a/src/platform_impl/linux/wayland/seat/mod.rs b/src/platform_impl/linux/wayland/seat/mod.rs deleted file mode 100644 index ccbb829d29..0000000000 --- a/src/platform_impl/linux/wayland/seat/mod.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Seat handling and managing. - -use std::cell::RefCell; -use std::rc::Rc; - -use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1; -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::client::protocol::wl_data_device_manager::WlDataDeviceManager; -use sctk::reexports::client::protocol::wl_seat::WlSeat; -use sctk::reexports::client::Attached; - -use sctk::environment::Environment; -use sctk::reexports::calloop::LoopHandle; -use sctk::seat::pointer::ThemeManager; -use sctk::seat::{SeatData, SeatListener}; - -use super::env::WinitEnv; -use super::event_loop::WinitState; -use crate::event::ModifiersState; - -mod dnd; -mod keyboard; -pub mod pointer; -pub mod text_input; -mod touch; - -use dnd::Dnd; -use keyboard::Keyboard; -use pointer::Pointers; -use text_input::TextInput; -use touch::Touch; - -pub struct SeatManager { - /// Listener for seats. - _seat_listener: SeatListener, -} - -impl SeatManager { - pub fn new( - env: &Environment, - loop_handle: LoopHandle<'static, WinitState>, - theme_manager: ThemeManager, - ) -> Self { - let relative_pointer_manager = env.get_global::(); - let pointer_constraints = env.get_global::(); - let text_input_manager = env.get_global::(); - let data_device_manager = env.get_global::(); - - let mut inner = SeatManagerInner::new( - theme_manager, - relative_pointer_manager, - pointer_constraints, - text_input_manager, - data_device_manager, - loop_handle, - ); - - // Handle existing seats. - for seat in env.get_all_seats() { - let seat_data = match sctk::seat::clone_seat_data(&seat) { - Some(seat_data) => seat_data, - None => continue, - }; - - inner.process_seat_update(&seat, &seat_data); - } - - let seat_listener = env.listen_for_seats(move |seat, seat_data, _| { - inner.process_seat_update(&seat, seat_data); - }); - - Self { - _seat_listener: seat_listener, - } - } -} - -/// Inner state of the seat manager. -struct SeatManagerInner { - /// Currently observed seats. - seats: Vec, - - /// Loop handle. - loop_handle: LoopHandle<'static, WinitState>, - - /// Relative pointer manager. - relative_pointer_manager: Option>, - - /// Pointer constraints. - pointer_constraints: Option>, - - /// Text input manager. - text_input_manager: Option>, - - /// Data device manager (for Drag and Drop). - data_device_manager: Option>, - - /// A theme manager. - theme_manager: ThemeManager, -} - -impl SeatManagerInner { - fn new( - theme_manager: ThemeManager, - relative_pointer_manager: Option>, - pointer_constraints: Option>, - text_input_manager: Option>, - data_device_manager: Option>, - loop_handle: LoopHandle<'static, WinitState>, - ) -> Self { - Self { - seats: Vec::new(), - loop_handle, - relative_pointer_manager, - pointer_constraints, - text_input_manager, - data_device_manager, - theme_manager, - } - } - - /// Handle seats update from the `SeatListener`. - pub fn process_seat_update(&mut self, seat: &Attached, seat_data: &SeatData) { - let detached_seat = seat.detach(); - - let position = self.seats.iter().position(|si| si.seat == detached_seat); - let index = position.unwrap_or_else(|| { - self.seats.push(SeatInfo::new(detached_seat)); - self.seats.len() - 1 - }); - - let seat_info = &mut self.seats[index]; - - // Pointer handling. - if seat_data.has_pointer && !seat_data.defunct { - if seat_info.pointer.is_none() { - seat_info.pointer = Some(Pointers::new( - seat, - &self.theme_manager, - &self.relative_pointer_manager, - &self.pointer_constraints, - seat_info.modifiers_state.clone(), - )); - } - } else { - seat_info.pointer = None; - } - - // Handle keyboard. - if seat_data.has_keyboard && !seat_data.defunct { - if seat_info.keyboard.is_none() { - seat_info.keyboard = Keyboard::new( - seat, - self.loop_handle.clone(), - seat_info.modifiers_state.clone(), - ); - } - } else { - seat_info.keyboard = None; - } - - // Handle touch. - if seat_data.has_touch && !seat_data.defunct { - if seat_info.touch.is_none() { - seat_info.touch = Some(Touch::new(seat)); - } - } else { - seat_info.touch = None; - } - - // Handle text input. - if let Some(text_input_manager) = self.text_input_manager.as_ref() { - if seat_data.defunct { - seat_info.text_input = None; - } else if seat_info.text_input.is_none() { - seat_info.text_input = Some(TextInput::new(seat, text_input_manager)); - } - } - - if let Some(data_device_manager) = self.data_device_manager.as_ref() { - if seat_data.defunct { - seat_info.dnd = None; - } else { - seat_info.dnd = Some(Dnd::new( - seat, - data_device_manager, - self.loop_handle.clone(), - )); - } - } - } -} - -/// Resources associtated with a given seat. -struct SeatInfo { - /// Seat to which this `SeatInfo` belongs. - seat: WlSeat, - - /// A keyboard handle with its repeat rate handling. - keyboard: Option, - - /// All pointers we're using on a seat. - pointer: Option, - - /// Touch handling. - touch: Option, - - /// Text input handling aka IME. - text_input: Option, - - /// Drag and Drop handler. - dnd: Option, - - /// The current state of modifiers observed in keyboard handler. - /// - /// We keep modifiers state on a seat, since it's being used by pointer events as well. - modifiers_state: Rc>, -} - -impl SeatInfo { - pub fn new(seat: WlSeat) -> Self { - Self { - seat, - keyboard: None, - pointer: None, - touch: None, - text_input: None, - dnd: None, - modifiers_state: Rc::new(RefCell::new(ModifiersState::default())), - } - } -} diff --git a/winit-core/Cargo.toml b/winit-core/Cargo.toml index 7b47cc4d33..e6b3adec19 100644 --- a/winit-core/Cargo.toml +++ b/winit-core/Cargo.toml @@ -27,6 +27,8 @@ bitflags.workspace = true cursor-icon.workspace = true dpi.workspace = true keyboard-types.workspace = true +mime.workspace = true +foldhash.workspace = true rwh_06.workspace = true serde = { workspace = true, optional = true } smol_str.workspace = true diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs new file mode 100644 index 0000000000..58a063019c --- /dev/null +++ b/winit-core/src/data_transfer.rs @@ -0,0 +1,210 @@ +use std::{ + collections::BTreeSet, + fmt::Debug, + hash::Hash, + io::{self, BufRead}, + path::PathBuf, + str::FromStr, + sync::Arc, +}; + +/// Identifier of a data transfer. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DataTransferId(i64); + +impl DataTransferId { + /// Convert the [`DataTransferId`] into the underlying integer. + /// + /// This is useful if you need to pass the ID across an FFI boundary, or store it in an atomic. + pub const fn into_raw(self) -> i64 { + self.0 + } + + /// Construct a [`DataTransferId`] from the underlying integer. + /// + /// This should only be called with integers returned from [`DataTransferId::into_raw`]. + pub const fn from_raw(id: i64) -> Self { + Self(id) + } +} + +enum DataTransferDataInner { + Paths(Vec), + Plaintext(String), + Bytes(Box), +} + +pub struct DataTransferData { + inner: DataTransferDataInner, +} + +impl DataTransferData { + pub fn from_bytes(reader: R) -> Self + where + R: BufRead + Send + Sync + 'static, + { + Self { inner: DataTransferDataInner::Bytes(Box::new(reader)) } + } +} + +impl From> for DataTransferData { + fn from(value: Vec) -> Self { + Self { inner: DataTransferDataInner::Paths(value) } + } +} + +impl From for DataTransferData { + fn from(value: String) -> Self { + Self { inner: DataTransferDataInner::Plaintext(value) } + } +} + +impl DataTransferData { + pub fn into_paths(self) -> io::Result> { + fn parse_paths(str: &str) -> io::Result> { + str.split(|c| c == '\n' || c == '\r') + .map(|line| { + PathBuf::from_str(line) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidFilename, err)) + }) + .collect() + } + + match self.inner { + DataTransferDataInner::Paths(paths) => Ok(paths), + DataTransferDataInner::Plaintext(str) => parse_paths(&str), + DataTransferDataInner::Bytes(mut buf_read) => { + let mut string = String::new(); + buf_read.read_to_string(&mut string)?; + parse_paths(&string) + }, + } + } + + pub fn into_string(self) -> io::Result { + match self.inner { + DataTransferDataInner::Paths(_) => { + // TODO: We could probably fudge this. + Err(io::Error::new(io::ErrorKind::InvalidData, "Could not read as string")) + }, + DataTransferDataInner::Plaintext(str) => Ok(str), + DataTransferDataInner::Bytes(mut buf_read) => { + let mut string = String::new(); + buf_read.read_to_string(&mut string)?; + Ok(string) + }, + } + } + + pub fn into_reader(self) -> io::Result { + match self.inner { + DataTransferDataInner::Paths(_) => { + // TODO: We could probably fudge this. + Err(io::Error::new(io::ErrorKind::InvalidData, "Could not read")) + }, + DataTransferDataInner::Plaintext(str) => { + // TODO: We don't need to box here. + Ok(Box::new(io::Cursor::new(str.into_bytes())) as Box) + }, + DataTransferDataInner::Bytes(buf_read) => Ok(buf_read), + } + } +} + +#[derive(Clone)] +pub struct DataTransfer { + id: DataTransferId, + available_types: Arc<[String]>, + fetch_data: Arc io::Result + Send + Sync>, +} + +impl PartialEq for DataTransfer { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.available_types == other.available_types + } +} + +impl Debug for DataTransfer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DataTransfer") + .field("id", &self.id) + .field("available_types", &self.available_types) + .finish_non_exhaustive() + } +} + +#[cfg(test)] +impl DataTransfer { + /// Testing function to create a [`DataTransfer`] from a set of paths. + /// + /// May eventually be exposed, but for now is kept private to reduce the API surface. + pub(crate) fn from_paths(id: DataTransferId, paths: Vec) -> Self { + const URI_LIST_MIME_TYPE: &str = "text/uri-list"; + + Self { + id, + available_types: vec![URI_LIST_MIME_TYPE].into(), + fetch_data: Arc::new(|ty| match ty { + URI_LIST_MIME_TYPE => Ok(paths.clone().into()), + _ => Err(io::Error::new(io::ErrorKind::NotFound, "Invalid MIME type")), + }), + } + } +} + +impl DataTransfer { + /// Create a new [`DataTransfer`] with a given ID and set of MIME types. + pub fn new(id: DataTransferId, available_types: I, fetch_data: F) -> Self + where + I: IntoIterator, + F: Fn(&str) -> io::Result + Send + Sync + 'static, + { + fn normalize_mime_type(type_: String) -> String { + let Ok(mime_type) = mime::Mime::from_str(&type_) else { + return type_; + }; + + mime_type.essence_str().to_ascii_lowercase() + } + + // Even though most platform implementations will be able to ensure + // the required invariants before construction, we normalize within the + // constructor to avoid exposing the precise inner types to the public + // API. + let available_types = available_types + .into_iter() + // First, normalize each MIME type to its canonical form. + .map(normalize_mime_type) + // Deduplicate and sort. + .collect::>() + .into_iter() + // Finally, convert to an `Arc<[String]>`. + .collect::>() + .into(); + + Self { id, available_types, fetch_data: Arc::new(fetch_data) } + } + + /// Display the list of all available MIME types. + /// + /// This is useful if more-complex MIME type matching is required, but for most cases + /// [`has_type`](DataTransfer::has_type) should be used. + pub fn available_types(&self) -> impl Iterator { + self.available_types.iter().map(AsRef::as_ref) + } + + /// Fetch the data of the specified type. + pub fn fetch(&self, mime_type: &str) -> io::Result { + (self.fetch_data)(mime_type) + } + + /// Check if the supplied MIME type is provided by this [`DataTransfer`]. + pub fn has_type(&self, type_: &str) -> bool { + self.available_types.binary_search_by(|haystack| (&**haystack).cmp(type_)).is_err() + } + + /// Get the ID of this [`DataTransfer`]. + pub const fn id(&self) -> DataTransferId { + self.id + } +} diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index e6500c2133..98e49bdd2f 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -2,7 +2,6 @@ use std::cell::LazyCell; use std::cmp::Ordering; use std::f64; -use std::path::PathBuf; use std::sync::{Mutex, Weak}; use dpi::{PhysicalPosition, PhysicalSize}; @@ -11,6 +10,7 @@ use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use crate::Instant; +use crate::data_transfer::DataTransfer; use crate::error::RequestError; use crate::event_loop::AsyncRequestSerial; use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; @@ -77,8 +77,8 @@ pub enum WindowEvent { /// A file drag operation has entered the window. DragEntered { - /// List of paths that are being dragged onto the window. - paths: Vec, + /// Data transfer object specifying the ID and available types. + data: DataTransfer, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -86,6 +86,8 @@ pub enum WindowEvent { }, /// A file drag operation has moved over the window. DragMoved { + /// Data transfer object specifying the ID and available types. + data: DataTransfer, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -93,8 +95,8 @@ pub enum WindowEvent { }, /// The file drag operation has dropped file(s) on the window. DragDropped { - /// List of paths that are being dragged onto the window. - paths: Vec, + /// Data transfer object specifying the ID and available types. + data: DataTransfer, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -102,6 +104,8 @@ pub enum WindowEvent { }, /// The file drag operation has been cancelled or left the window. DragLeft { + /// Data transfer object specifying the ID and available types. + data: DataTransfer, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -1560,16 +1564,19 @@ mod tests { use crate::event::Ime::Enabled; use crate::event::WindowEvent::*; use crate::event::{PointerKind, PointerSource}; + use crate::data_transfer::{DataTransfer, DataTransferId}; + + let data_transfer = DataTransfer::from_paths(DataTransferId::from_raw(0), vec![PathBuf::new("x.txt")]); with_window_event(CloseRequested); with_window_event(Destroyed); with_window_event(Focused(true)); with_window_event(Moved((0, 0).into())); with_window_event(SurfaceResized((0, 0).into())); - with_window_event(DragEntered { paths: vec!["x.txt".into()], position: (0, 0).into() }); - with_window_event(DragMoved { position: (0, 0).into() }); - with_window_event(DragDropped { paths: vec!["x.txt".into()], position: (0, 0).into() }); - with_window_event(DragLeft { position: Some((0, 0).into()) }); + with_window_event(DragEntered { data: data_transfer.clone(), position: (0, 0).into() }); + with_window_event(DragMoved { data: data_transfer.clone(), position: (0, 0).into() }); + with_window_event(DragDropped { data: data_transfer.clone(), position: (0, 0).into() }); + with_window_event(DragLeft { data: data_transfer.clone(), position: Some((0, 0).into()) }); with_window_event(Ime(Enabled)); with_window_event(PointerMoved { device_id: None, @@ -1663,24 +1670,24 @@ mod tests { const TILT_TO_ANGLE: &[(TabletToolTilt, TabletToolAngle)] = &[ (TabletToolTilt { x: 0, y: 0 }, TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }), (TabletToolTilt { x: 0, y: 90 }, TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }), - (TabletToolTilt { x: 0, y: -90 }, TabletToolAngle { - altitude: 0., - azimuth: 3. * FRAC_PI_2, - }), + ( + TabletToolTilt { x: 0, y: -90 }, + TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, + ), (TabletToolTilt { x: 90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: PI }), (TabletToolTilt { x: -90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), - (TabletToolTilt { x: 0, y: 45 }, TabletToolAngle { - altitude: FRAC_PI_4, - azimuth: FRAC_PI_2, - }), - (TabletToolTilt { x: 0, y: -45 }, TabletToolAngle { - altitude: FRAC_PI_4, - azimuth: 3. * FRAC_PI_2, - }), + ( + TabletToolTilt { x: 0, y: 45 }, + TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, + ), + ( + TabletToolTilt { x: 0, y: -45 }, + TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, + ), (TabletToolTilt { x: 45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }), (TabletToolTilt { x: -45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }), ]; @@ -1695,20 +1702,20 @@ mod tests { (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }, TabletToolTilt { x: 45, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }, TabletToolTilt { x: 0, y: 0 }), (TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }, TabletToolTilt { x: 0, y: 90 }), - (TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, TabletToolTilt { - x: 0, - y: 45, - }), + ( + TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, + TabletToolTilt { x: 0, y: 45 }, + ), (TabletToolAngle { altitude: 0., azimuth: PI }, TabletToolTilt { x: -90, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }, TabletToolTilt { x: -45, y: 0 }), - (TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { - x: 0, - y: -90, - }), - (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { - x: 0, - y: -45, - }), + ( + TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, + TabletToolTilt { x: 0, y: -90 }, + ), + ( + TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, + TabletToolTilt { x: 0, y: -45 }, + ), ]; for (angle, tilt) in ANGLE_TO_TILT { diff --git a/winit-core/src/lib.rs b/winit-core/src/lib.rs index 89bc682164..2fe5fc82ee 100644 --- a/winit-core/src/lib.rs +++ b/winit-core/src/lib.rs @@ -13,6 +13,7 @@ pub mod cursor; #[macro_use] pub mod error; pub mod application; +pub mod data_transfer; pub mod event; pub mod event_loop; pub mod icon; diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 5355369acc..023c4aea50 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1,5 +1,5 @@ //! The [`Window`] trait and associated types. -use std::fmt; +use std::fmt::{self, Display}; use bitflags::bitflags; use cursor_icon::CursorIcon; @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::as_any::AsAny; use crate::cursor::Cursor; +use crate::data_transfer::DataTransferId; use crate::error::RequestError; use crate::icon::Icon; use crate::monitor::{Fullscreen, MonitorHandle}; @@ -42,7 +43,8 @@ impl WindowId { impl fmt::Debug for WindowId { fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(fmtr) + let inner = self.0; + write!(fmtr, "{inner:?}") } } @@ -457,6 +459,17 @@ pub trait PlatformWindowAttributes: AsAny + std::fmt::Debug + Send + Sync { impl_dyn_casting!(PlatformWindowAttributes); +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(DataTransferId); + +impl Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + /// Represents a window. /// /// The window is closed when dropped. @@ -1168,6 +1181,40 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { } } + /// Mark a given data transfer ID as being accepted by the window. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data can be dropped. + /// + /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying + /// one or more accepted types. Using this method will mark all available types as accepted. + /// For the most reliable cross-platform behaviour, [`accept_drag_type`](Window::accept_drag_type) + /// is preferred, although in most cases simply conditionally accepting the data transfer + /// based on whether or not it advertises a supported type will do the right thing. + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } + + /// Mark a single type of a given data transfer ID as being accepted by the window. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data can be dropped. + /// + /// If the window may accept more than one of the advertised types, this method should be + /// called multiple times, once for each of the accepted types. + fn accept_drag_type(&self, id: DataTransferId, type_: &str) -> Result<(), UnknownDataTransfer> { + let _ = type_; + self.accept_drag(id) + } + + /// Mark a given data transfer ID as being rejected by the window. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data can _not_ be dropped. + /// + /// This will ensure that the OS/compositor indicates to the user that dropping the dragged data + /// is not possible. + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } + /// Atomically apply request to IME. /// /// For details consult [`ImeRequest`] and [`ImeCapabilities`]. diff --git a/winit-wayland/Cargo.toml b/winit-wayland/Cargo.toml index 27a5972337..c1c77d95ad 100644 --- a/winit-wayland/Cargo.toml +++ b/winit-wayland/Cargo.toml @@ -27,6 +27,7 @@ serde = { workspace = true, optional = true } smol_str.workspace = true tracing.workspace = true winit-core.workspace = true +mime.workspace = true # Platform-specific calloop.workspace = true diff --git a/winit-wayland/src/seat/dnd/mod.rs b/winit-wayland/src/seat/dnd/mod.rs new file mode 100644 index 0000000000..058fd89d92 --- /dev/null +++ b/winit-wayland/src/seat/dnd/mod.rs @@ -0,0 +1,183 @@ +use sctk::data_device_manager::{ + data_device::DataDeviceHandler, data_offer::DataOfferHandler, data_source::DataSourceHandler, +}; +use wayland_client::{ + Connection, QueueHandle, + protocol::{wl_data_device::WlDataDevice, wl_surface::WlSurface}, +}; + +use crate::state::WinitState; + +impl DataSourceHandler for WinitState { + fn accept_mime( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + mime: Option, + ) { + let _ = mime; + let _ = source; + let _ = qh; + let _ = conn; + todo!() + } + + fn send_request( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + mime: String, + fd: sctk::data_device_manager::WritePipe, + ) { + let _ = fd; + let _ = mime; + let _ = source; + let _ = qh; + let _ = conn; + todo!() + } + + fn cancelled( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + todo!() + } + + fn dnd_dropped( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + todo!() + } + + fn dnd_finished( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + todo!() + } + + fn action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + action: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + let _ = action; + let _ = source; + let _ = qh; + let _ = conn; + todo!() + } +} + +impl DataOfferHandler for WinitState { + fn source_actions( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut sctk::data_device_manager::data_offer::DragOffer, + actions: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + todo!() + } + + fn selected_action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut sctk::data_device_manager::data_offer::DragOffer, + actions: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + todo!() + } +} + +impl DataDeviceHandler for WinitState { + fn enter( + &mut self, + conn: &Connection, + qh: &QueueHandle, + data_device: &WlDataDevice, + x: f64, + y: f64, + wl_surface: &WlSurface, + ) { + let _ = wl_surface; + let _ = y; + let _ = x; + let _ = data_device; + let _ = qh; + let _ = conn; + todo!() + } + + fn leave(&mut self, conn: &Connection, qh: &QueueHandle, data_device: &WlDataDevice) { + let _ = data_device; + let _ = qh; + let _ = conn; + todo!() + } + + fn motion( + &mut self, + conn: &Connection, + qh: &QueueHandle, + data_device: &WlDataDevice, + x: f64, + y: f64, + ) { + let _ = y; + let _ = x; + let _ = data_device; + let _ = qh; + let _ = conn; + todo!() + } + + fn selection(&mut self, conn: &Connection, qh: &QueueHandle, data_device: &WlDataDevice) { + let _ = data_device; + let _ = qh; + let _ = conn; + todo!() + } + + fn drop_performed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + data_device: &WlDataDevice, + ) { + let _ = data_device; + let _ = qh; + let _ = conn; + todo!() + } +} diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index ddf0e061df..31c6c9fa45 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use foldhash::HashMap; +use sctk::data_device_manager::data_device::DataDevice; use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_touch::WlTouch; @@ -19,6 +20,7 @@ use winit_core::keyboard::ModifiersState; use crate::state::WinitState; +mod dnd; mod keyboard; mod pointer; mod text_input; @@ -60,6 +62,9 @@ pub struct WinitSeatState { /// The pinch pointer gesture bound on the seat. pointer_gesture_pinch: Option, + /// The drag-and-drop state + data_device: Option, + /// The keyboard bound on the seat. keyboard_state: Option, @@ -125,6 +130,11 @@ impl SeatHandler for WinitState { ) .expect("failed to create pointer with present capability."); + seat_state.data_device = self + .data_device_manager_state + .as_ref() + .map(|device| device.get_data_device(queue_handle, &seat)); + seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| { manager.get_relative_pointer( themed_pointer.pointer(), @@ -209,6 +219,8 @@ impl SeatHandler for WinitState { pointer_gesture_pinch.destroy(); } + seat_state.data_device = None; + if let Some(pointer) = seat_state.pointer.take() { let pointer_data = pointer.pointer().winit_data(); diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index 577c8c42ec..6ba711d13c 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, Mutex}; use foldhash::HashMap; use sctk::compositor::{CompositorHandler, CompositorState}; +use sctk::data_device_manager::DataDeviceManagerState; use sctk::output::{OutputHandler, OutputState}; use sctk::reexports::calloop::LoopHandle; use sctk::reexports::client::backend::ObjectId; @@ -113,6 +114,9 @@ pub struct WinitState { /// Viewporter state on the given window. pub viewporter_state: Option, + /// Data device manager state on the given window. + pub data_device_manager_state: Option, + /// Fractional scaling manager. pub fractional_scaling_manager: Option, @@ -168,6 +172,16 @@ impl WinitState { (None, None) }; + let data_device_manager_state = match DataDeviceManagerState::bind(globals, queue_handle) { + Ok(state) => Some(state), + Err(e) => { + tracing::warn!( + "Data device manager not available, clipboard and drag-and-drop disabled: {e:?}" + ); + None + }, + }; + let shm = Shm::bind(globals, queue_handle).map_err(|err| os_error!(err))?; let image_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); @@ -191,6 +205,7 @@ impl WinitState { window_compositor_updates: Vec::new(), window_events_sink: Default::default(), viewporter_state, + data_device_manager_state, fractional_scaling_manager, blur_manager: BgrEffectManager::new(globals, queue_handle).ok(), @@ -452,3 +467,4 @@ sctk::delegate_registry!(WinitState); sctk::delegate_shm!(WinitState); sctk::delegate_xdg_shell!(WinitState); sctk::delegate_xdg_window!(WinitState); +sctk::delegate_data_device!(WinitState); diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 72c1c295f7..388cdc40a2 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -696,10 +696,14 @@ impl EventProcessor { drop(shared_state_lock); let surface_size = Arc::new(Mutex::new(new_surface_size)); - app.window_event(&self.target, window_id, WindowEvent::ScaleFactorChanged { - scale_factor: new_scale_factor, - surface_size_writer: SurfaceSizeWriter::new(Arc::downgrade(&surface_size)), - }); + app.window_event( + &self.target, + window_id, + WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + surface_size_writer: SurfaceSizeWriter::new(Arc::downgrade(&surface_size)), + }, + ); let new_surface_size = *surface_size.lock().unwrap(); drop(surface_size); From ccaff27d300512056fb72e9576c1f61182796746 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 20 May 2026 10:05:08 +0200 Subject: [PATCH 11/87] Fix cargo fmt --- winit-core/src/data_transfer.rs | 16 +++---- winit-core/src/event.rs | 48 +++++++++---------- winit-core/src/window.rs | 16 ++++--- winit-wayland/src/seat/dnd/mod.rs | 13 +++-- winit-wayland/src/seat/pointer/mod.rs | 8 ++-- .../src/seat/pointer/relative_pointer.rs | 4 +- winit-wayland/src/state.rs | 3 +- winit-x11/src/event_processor.rs | 12 ++--- 8 files changed, 59 insertions(+), 61 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 58a063019c..6d02ab511d 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -1,12 +1,10 @@ -use std::{ - collections::BTreeSet, - fmt::Debug, - hash::Hash, - io::{self, BufRead}, - path::PathBuf, - str::FromStr, - sync::Arc, -}; +use std::collections::BTreeSet; +use std::fmt::Debug; +use std::hash::Hash; +use std::io::{self, BufRead}; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; /// Identifier of a data transfer. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index 98e49bdd2f..18836193cc 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -1670,24 +1670,24 @@ mod tests { const TILT_TO_ANGLE: &[(TabletToolTilt, TabletToolAngle)] = &[ (TabletToolTilt { x: 0, y: 0 }, TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }), (TabletToolTilt { x: 0, y: 90 }, TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }), - ( - TabletToolTilt { x: 0, y: -90 }, - TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, - ), + (TabletToolTilt { x: 0, y: -90 }, TabletToolAngle { + altitude: 0., + azimuth: 3. * FRAC_PI_2, + }), (TabletToolTilt { x: 90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: PI }), (TabletToolTilt { x: -90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), - ( - TabletToolTilt { x: 0, y: 45 }, - TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, - ), - ( - TabletToolTilt { x: 0, y: -45 }, - TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, - ), + (TabletToolTilt { x: 0, y: 45 }, TabletToolAngle { + altitude: FRAC_PI_4, + azimuth: FRAC_PI_2, + }), + (TabletToolTilt { x: 0, y: -45 }, TabletToolAngle { + altitude: FRAC_PI_4, + azimuth: 3. * FRAC_PI_2, + }), (TabletToolTilt { x: 45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }), (TabletToolTilt { x: -45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }), ]; @@ -1702,20 +1702,20 @@ mod tests { (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }, TabletToolTilt { x: 45, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }, TabletToolTilt { x: 0, y: 0 }), (TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }, TabletToolTilt { x: 0, y: 90 }), - ( - TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, - TabletToolTilt { x: 0, y: 45 }, - ), + (TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, TabletToolTilt { + x: 0, + y: 45, + }), (TabletToolAngle { altitude: 0., azimuth: PI }, TabletToolTilt { x: -90, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }, TabletToolTilt { x: -45, y: 0 }), - ( - TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, - TabletToolTilt { x: 0, y: -90 }, - ), - ( - TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, - TabletToolTilt { x: 0, y: -45 }, - ), + (TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { + x: 0, + y: -90, + }), + (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { + x: 0, + y: -45, + }), ]; for (angle, tilt) in ANGLE_TO_TILT { diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 023c4aea50..410613d187 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1183,20 +1183,23 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// Mark a given data transfer ID as being accepted by the window. /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data can be dropped. + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. /// /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying /// one or more accepted types. Using this method will mark all available types as accepted. - /// For the most reliable cross-platform behaviour, [`accept_drag_type`](Window::accept_drag_type) - /// is preferred, although in most cases simply conditionally accepting the data transfer - /// based on whether or not it advertises a supported type will do the right thing. + /// For the most reliable cross-platform behaviour, + /// [`accept_drag_type`](Window::accept_drag_type) is preferred, although in most cases + /// simply conditionally accepting the data transfer based on whether or not it advertises a + /// supported type will do the right thing. fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { Err(UnknownDataTransfer(id)) } /// Mark a single type of a given data transfer ID as being accepted by the window. /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data can be dropped. + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. /// /// If the window may accept more than one of the advertised types, this method should be /// called multiple times, once for each of the accepted types. @@ -1207,7 +1210,8 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// Mark a given data transfer ID as being rejected by the window. /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data can _not_ be dropped. + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can _not_ be dropped. /// /// This will ensure that the OS/compositor indicates to the user that dropping the dragged data /// is not possible. diff --git a/winit-wayland/src/seat/dnd/mod.rs b/winit-wayland/src/seat/dnd/mod.rs index 058fd89d92..406c0c4224 100644 --- a/winit-wayland/src/seat/dnd/mod.rs +++ b/winit-wayland/src/seat/dnd/mod.rs @@ -1,10 +1,9 @@ -use sctk::data_device_manager::{ - data_device::DataDeviceHandler, data_offer::DataOfferHandler, data_source::DataSourceHandler, -}; -use wayland_client::{ - Connection, QueueHandle, - protocol::{wl_data_device::WlDataDevice, wl_surface::WlSurface}, -}; +use sctk::data_device_manager::data_device::DataDeviceHandler; +use sctk::data_device_manager::data_offer::DataOfferHandler; +use sctk::data_device_manager::data_source::DataSourceHandler; +use wayland_client::protocol::wl_data_device::WlDataDevice; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::{Connection, QueueHandle}; use crate::state::WinitState; diff --git a/winit-wayland/src/seat/pointer/mod.rs b/winit-wayland/src/seat/pointer/mod.rs index 11970d7069..6e9aa20092 100644 --- a/winit-wayland/src/seat/pointer/mod.rs +++ b/winit-wayland/src/seat/pointer/mod.rs @@ -22,19 +22,19 @@ use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; use sctk::compositor::SurfaceData; use sctk::globals::GlobalData; +use sctk::seat::SeatState; use sctk::seat::pointer::{ PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler, }; -use sctk::seat::SeatState; use dpi::{LogicalPosition, PhysicalPosition}; use winit_core::event::{ - ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource, TouchPhase, - WindowEvent, ButtonSource, + ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource, + TouchPhase, WindowEvent, }; -use crate::state::WinitState; use crate::WindowId; +use crate::state::WinitState; pub mod pointer_gesture; pub mod relative_pointer; diff --git a/winit-wayland/src/seat/pointer/relative_pointer.rs b/winit-wayland/src/seat/pointer/relative_pointer.rs index 8465b720d6..c130d57398 100644 --- a/winit-wayland/src/seat/pointer/relative_pointer.rs +++ b/winit-wayland/src/seat/pointer/relative_pointer.rs @@ -3,8 +3,8 @@ use std::ops::Deref; use sctk::reexports::client::globals::{BindError, GlobalList}; -use sctk::reexports::client::{delegate_dispatch, Dispatch}; use sctk::reexports::client::{Connection, QueueHandle}; +use sctk::reexports::client::{Dispatch, delegate_dispatch}; use sctk::reexports::protocols::wp::relative_pointer::zv1::{ client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1}, @@ -12,8 +12,8 @@ use sctk::reexports::protocols::wp::relative_pointer::zv1::{ use sctk::globals::GlobalData; -use winit_core::event::DeviceEvent; use crate::state::WinitState; +use winit_core::event::DeviceEvent; /// Wrapper around the relative pointer. #[derive(Debug)] diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index 6ba711d13c..82aba10b60 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -176,7 +176,8 @@ impl WinitState { Ok(state) => Some(state), Err(e) => { tracing::warn!( - "Data device manager not available, clipboard and drag-and-drop disabled: {e:?}" + "Data device manager not available, clipboard and drag-and-drop disabled: \ + {e:?}" ); None }, diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 388cdc40a2..72c1c295f7 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -696,14 +696,10 @@ impl EventProcessor { drop(shared_state_lock); let surface_size = Arc::new(Mutex::new(new_surface_size)); - app.window_event( - &self.target, - window_id, - WindowEvent::ScaleFactorChanged { - scale_factor: new_scale_factor, - surface_size_writer: SurfaceSizeWriter::new(Arc::downgrade(&surface_size)), - }, - ); + app.window_event(&self.target, window_id, WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + surface_size_writer: SurfaceSizeWriter::new(Arc::downgrade(&surface_size)), + }); let new_surface_size = *surface_size.lock().unwrap(); drop(surface_size); From d8fa1844f4fe8dfe688b271616f17c09abcebbcc Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 20 May 2026 13:08:38 +0200 Subject: [PATCH 12/87] Convert to use new data transfer system This changes the data transfer system to better match cross-platform semantics --- winit-core/src/data_transfer.rs | 199 ++++++----------------------- winit-core/src/event.rs | 42 ++++-- winit-core/src/window.rs | 36 +++++- winit-x11/src/dnd.rs | 211 ++++++++++++++++++++++++------- winit-x11/src/event_loop.rs | 11 +- winit-x11/src/event_processor.rs | 119 ++++++++--------- winit-x11/src/lib.rs | 2 + winit-x11/src/window.rs | 123 +++++++++++++++++- winit/examples/application.rs | 1 + 9 files changed, 442 insertions(+), 302 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 6d02ab511d..adf50f5435 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -1,11 +1,10 @@ -use std::collections::BTreeSet; -use std::fmt::Debug; -use std::hash::Hash; -use std::io::{self, BufRead}; -use std::path::PathBuf; -use std::str::FromStr; +use std::fmt::{self, Debug}; +use std::io; +use std::ops::Deref; use std::sync::Arc; +use crate::as_any::AsAny; + /// Identifier of a data transfer. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DataTransferId(i64); @@ -26,183 +25,67 @@ impl DataTransferId { } } -enum DataTransferDataInner { - Paths(Vec), - Plaintext(String), - Bytes(Box), -} - -pub struct DataTransferData { - inner: DataTransferDataInner, +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TypeHint { + Plaintext, + UriList, + Html, + Rtf, + Audio { extension_hint: Option<&'static str> }, + Image { extension_hint: Option<&'static str> }, } -impl DataTransferData { - pub fn from_bytes(reader: R) -> Self - where - R: BufRead + Send + Sync + 'static, - { - Self { inner: DataTransferDataInner::Bytes(Box::new(reader)) } - } +pub trait TransferType: AsAny + Send + Sync + fmt::Debug { + fn hint(&self) -> Option; } -impl From> for DataTransferData { - fn from(value: Vec) -> Self { - Self { inner: DataTransferDataInner::Paths(value) } +impl TransferType for TypeHint { + fn hint(&self) -> Option { + Some(*self) } } -impl From for DataTransferData { - fn from(value: String) -> Self { - Self { inner: DataTransferDataInner::Plaintext(value) } - } -} +impl_dyn_casting!(TransferType); -impl DataTransferData { - pub fn into_paths(self) -> io::Result> { - fn parse_paths(str: &str) -> io::Result> { - str.split(|c| c == '\n' || c == '\r') - .map(|line| { - PathBuf::from_str(line) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidFilename, err)) - }) - .collect() - } - - match self.inner { - DataTransferDataInner::Paths(paths) => Ok(paths), - DataTransferDataInner::Plaintext(str) => parse_paths(&str), - DataTransferDataInner::Bytes(mut buf_read) => { - let mut string = String::new(); - buf_read.read_to_string(&mut string)?; - parse_paths(&string) - }, - } - } - - pub fn into_string(self) -> io::Result { - match self.inner { - DataTransferDataInner::Paths(_) => { - // TODO: We could probably fudge this. - Err(io::Error::new(io::ErrorKind::InvalidData, "Could not read as string")) - }, - DataTransferDataInner::Plaintext(str) => Ok(str), - DataTransferDataInner::Bytes(mut buf_read) => { - let mut string = String::new(); - buf_read.read_to_string(&mut string)?; - Ok(string) - }, - } - } - - pub fn into_reader(self) -> io::Result { - match self.inner { - DataTransferDataInner::Paths(_) => { - // TODO: We could probably fudge this. - Err(io::Error::new(io::ErrorKind::InvalidData, "Could not read")) - }, - DataTransferDataInner::Plaintext(str) => { - // TODO: We don't need to box here. - Ok(Box::new(io::Cursor::new(str.into_bytes())) as Box) - }, - DataTransferDataInner::Bytes(buf_read) => Ok(buf_read), - } - } +pub trait TypedData: AsAny + Send + Sync + fmt::Debug { + fn type_(&self) -> &dyn TransferType; + fn try_read(&mut self) -> Option>; } -#[derive(Clone)] -pub struct DataTransfer { - id: DataTransferId, - available_types: Arc<[String]>, - fetch_data: Arc io::Result + Send + Sync>, -} +#[derive(Debug, Clone)] +pub struct DynTypedData(pub Arc); -impl PartialEq for DataTransfer { +impl PartialEq for DynTypedData { fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.available_types == other.available_types + std::ptr::addr_eq(&**self, &**other) } } -impl Debug for DataTransfer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DataTransfer") - .field("id", &self.id) - .field("available_types", &self.available_types) - .finish_non_exhaustive() - } -} +impl Deref for DynTypedData { + type Target = dyn TypedData; -#[cfg(test)] -impl DataTransfer { - /// Testing function to create a [`DataTransfer`] from a set of paths. - /// - /// May eventually be exposed, but for now is kept private to reduce the API surface. - pub(crate) fn from_paths(id: DataTransferId, paths: Vec) -> Self { - const URI_LIST_MIME_TYPE: &str = "text/uri-list"; - - Self { - id, - available_types: vec![URI_LIST_MIME_TYPE].into(), - fetch_data: Arc::new(|ty| match ty { - URI_LIST_MIME_TYPE => Ok(paths.clone().into()), - _ => Err(io::Error::new(io::ErrorKind::NotFound, "Invalid MIME type")), - }), - } + fn deref(&self) -> &Self::Target { + &*self.0 } } -impl DataTransfer { - /// Create a new [`DataTransfer`] with a given ID and set of MIME types. - pub fn new(id: DataTransferId, available_types: I, fetch_data: F) -> Self - where - I: IntoIterator, - F: Fn(&str) -> io::Result + Send + Sync + 'static, - { - fn normalize_mime_type(type_: String) -> String { - let Ok(mime_type) = mime::Mime::from_str(&type_) else { - return type_; - }; - - mime_type.essence_str().to_ascii_lowercase() - } - - // Even though most platform implementations will be able to ensure - // the required invariants before construction, we normalize within the - // constructor to avoid exposing the precise inner types to the public - // API. - let available_types = available_types - .into_iter() - // First, normalize each MIME type to its canonical form. - .map(normalize_mime_type) - // Deduplicate and sort. - .collect::>() - .into_iter() - // Finally, convert to an `Arc<[String]>`. - .collect::>() - .into(); - - Self { id, available_types, fetch_data: Arc::new(fetch_data) } - } +impl_dyn_casting!(TypedData); +pub trait DataTransfer: AsAny + Send + Sync + fmt::Debug { /// Display the list of all available MIME types. /// /// This is useful if more-complex MIME type matching is required, but for most cases /// [`has_type`](DataTransfer::has_type) should be used. - pub fn available_types(&self) -> impl Iterator { - self.available_types.iter().map(AsRef::as_ref) - } - - /// Fetch the data of the specified type. - pub fn fetch(&self, mime_type: &str) -> io::Result { - (self.fetch_data)(mime_type) - } + // TODO: We should be able to do `&dyn TransferType`, but some implementation details in + // the platforms make that unnecessarily difficult right now. + fn available_types(&self) -> Box> + '_>; /// Check if the supplied MIME type is provided by this [`DataTransfer`]. - pub fn has_type(&self, type_: &str) -> bool { - self.available_types.binary_search_by(|haystack| (&**haystack).cmp(type_)).is_err() - } - - /// Get the ID of this [`DataTransfer`]. - pub const fn id(&self) -> DataTransferId { - self.id + fn has_type(&self, type_: &dyn TransferType) -> bool { + type_.hint().is_some_and(|hint| { + self.available_types().any(|haystack| haystack.hint() == Some(hint)) + }) } } + +impl_dyn_casting!(DataTransfer); diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index 18836193cc..408b09824a 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use crate::Instant; -use crate::data_transfer::DataTransfer; +use crate::data_transfer::{DataTransferId, DynTypedData}; use crate::error::RequestError; use crate::event_loop::AsyncRequestSerial; use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; @@ -46,7 +46,10 @@ pub enum StartCause { #[derive(Debug, Clone, PartialEq)] pub enum WindowEvent { /// The activation token was delivered back and now could be used. - ActivationTokenDone { serial: AsyncRequestSerial, token: ActivationToken }, + ActivationTokenDone { + serial: AsyncRequestSerial, + token: ActivationToken, + }, /// The size of the window's surface has changed. /// @@ -78,7 +81,7 @@ pub enum WindowEvent { /// A file drag operation has entered the window. DragEntered { /// Data transfer object specifying the ID and available types. - data: DataTransfer, + id: DataTransferId, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -87,7 +90,7 @@ pub enum WindowEvent { /// A file drag operation has moved over the window. DragMoved { /// Data transfer object specifying the ID and available types. - data: DataTransfer, + id: DataTransferId, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -96,7 +99,7 @@ pub enum WindowEvent { /// The file drag operation has dropped file(s) on the window. DragDropped { /// Data transfer object specifying the ID and available types. - data: DataTransfer, + id: DataTransferId, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -105,7 +108,7 @@ pub enum WindowEvent { /// The file drag operation has been cancelled or left the window. DragLeft { /// Data transfer object specifying the ID and available types. - data: DataTransfer, + id: DataTransferId, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be /// negative on some platforms if something is dragged over a window's decorations (title /// bar, frame, etc). @@ -116,6 +119,11 @@ pub enum WindowEvent { position: Option>, }, + DataTransferResult { + id: DataTransferId, + data: DynTypedData, + }, + /// The window gained or lost focus. /// /// The parameter is true if the window has gained focus, and false if it has lost focus. @@ -245,7 +253,11 @@ pub enum WindowEvent { }, /// A mouse wheel movement or touchpad scroll occurred. - MouseWheel { device_id: Option, delta: MouseScrollDelta, phase: TouchPhase }, + MouseWheel { + device_id: Option, + delta: MouseScrollDelta, + phase: TouchPhase, + }, /// An mouse button press has been received. PointerButton { @@ -320,7 +332,9 @@ pub enum WindowEvent { /// /// - Only available on **macOS 10.8** and later, and **iOS**. /// - On iOS, not recognized by default. It must be enabled when needed. - DoubleTapGesture { device_id: Option }, + DoubleTapGesture { + device_id: Option, + }, /// Two-finger rotation gesture. /// @@ -1564,19 +1578,19 @@ mod tests { use crate::event::Ime::Enabled; use crate::event::WindowEvent::*; use crate::event::{PointerKind, PointerSource}; - use crate::data_transfer::{DataTransfer, DataTransferId}; + use crate::data_transfer::DataTransferId; - let data_transfer = DataTransfer::from_paths(DataTransferId::from_raw(0), vec![PathBuf::new("x.txt")]); + let dnd_data = DataTransferId::from_raw(123); with_window_event(CloseRequested); with_window_event(Destroyed); with_window_event(Focused(true)); with_window_event(Moved((0, 0).into())); with_window_event(SurfaceResized((0, 0).into())); - with_window_event(DragEntered { data: data_transfer.clone(), position: (0, 0).into() }); - with_window_event(DragMoved { data: data_transfer.clone(), position: (0, 0).into() }); - with_window_event(DragDropped { data: data_transfer.clone(), position: (0, 0).into() }); - with_window_event(DragLeft { data: data_transfer.clone(), position: Some((0, 0).into()) }); + with_window_event(DragEntered { id: dnd_data, position: (0, 0).into() }); + with_window_event(DragMoved { id: dnd_data, position: (0, 0).into() }); + with_window_event(DragDropped { id: dnd_data, position: (0, 0).into() }); + with_window_event(DragLeft { id: dnd_data, position: Some((0, 0).into()) }); with_window_event(Ime(Enabled)); with_window_event(PointerMoved { device_id: None, diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 410613d187..d0e046ef85 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::as_any::AsAny; use crate::cursor::Cursor; -use crate::data_transfer::DataTransferId; +use crate::data_transfer::{DataTransfer, DataTransferId, TransferType}; use crate::error::RequestError; use crate::icon::Icon; use crate::monitor::{Fullscreen, MonitorHandle}; @@ -461,7 +461,7 @@ impl_dyn_casting!(PlatformWindowAttributes); /// An operation was attempted on a data transfer ID, but that ID was invalid. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct UnknownDataTransfer(DataTransferId); +pub struct UnknownDataTransfer(pub DataTransferId); impl Display for UnknownDataTransfer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -1181,6 +1181,32 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { } } + /// Get a [data transfer](DataTransfer) by its ID. + /// + /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will + /// return an error. + fn data_transfer( + &self, + id: DataTransferId, + ) -> Result, UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } + + /// Request to fetch a type from a [data transfer](DataTransfer). + /// + /// The data will be supplied via [`WindowEvent::DataTransferResult`]. + /// + /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will + /// return an error. + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result<(), UnknownDataTransfer> { + let _ = type_; + Err(UnknownDataTransfer(id)) + } + /// Mark a given data transfer ID as being accepted by the window. /// /// This allows the OS/compositor to display the correct UI, indicating that the dragged data @@ -1203,7 +1229,11 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// /// If the window may accept more than one of the advertised types, this method should be /// called multiple times, once for each of the accepted types. - fn accept_drag_type(&self, id: DataTransferId, type_: &str) -> Result<(), UnknownDataTransfer> { + fn accept_drag_type( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result<(), UnknownDataTransfer> { let _ = type_; self.accept_drag(id) } diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 3c85e4fe2e..c7557633b6 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -2,14 +2,17 @@ use std::io; use std::os::raw::*; use std::path::{Path, PathBuf}; use std::str::Utf8Error; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use dpi::PhysicalPosition; use percent_encoding::percent_decode; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; use x11rb::protocol::xproto::{self, ConnectionExt}; use crate::atoms::AtomName::None as DndNone; -use crate::atoms::*; +use crate::atoms::{ + Atoms, TextUriList, XdndActionPrivate, XdndFinished, XdndSelection, XdndStatus, XdndTypeList, +}; use crate::event_loop::{CookieResultExt, X11Error}; use crate::util; use crate::xdisplay::XConnection; @@ -21,7 +24,7 @@ pub enum DndState { } #[derive(Debug)] -pub enum DndDataParseError { +pub enum UriListParseError { EmptyData, InvalidUtf8(#[allow(dead_code)] Utf8Error), HostnameSpecified(#[allow(dead_code)] String), @@ -29,52 +32,194 @@ pub enum DndDataParseError { UnresolvablePath(#[allow(dead_code)] io::Error), } -impl From for DndDataParseError { +impl From for UriListParseError { fn from(e: Utf8Error) -> Self { - DndDataParseError::InvalidUtf8(e) + UriListParseError::InvalidUtf8(e) } } -impl From for DndDataParseError { +impl From for UriListParseError { fn from(e: io::Error) -> Self { - DndDataParseError::UnresolvablePath(e) + UriListParseError::UnresolvablePath(e) + } +} + +#[derive(Clone, Debug)] +pub struct SelectionReader { + type_: SelectionType, + data: Arc<[c_uchar]>, +} + +impl SelectionReader { + pub(crate) fn new(type_: SelectionType, data: Arc<[c_uchar]>) -> Self { + Self { type_, data } + } + + pub fn parse_path_list(&self) -> Result, UriListParseError> { + if !self.data.is_empty() { + let mut path_list = Vec::new(); + let decoded = percent_decode(&self.data).decode_utf8()?.into_owned(); + for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { + // The format is specified as protocol://host/path + // However, it's typically simply protocol:///path + let path_str = if uri.starts_with("file://") { + let path_str = uri.replace("file://", ""); + if !path_str.starts_with('/') { + // A hostname is specified + // Supporting this case is beyond the scope of my mental health + return Err(UriListParseError::HostnameSpecified(path_str)); + } + path_str + } else { + // Only the file protocol is supported + return Err(UriListParseError::UnexpectedProtocol(uri.to_owned())); + }; + + let path = Path::new(&path_str).canonicalize()?; + path_list.push(path); + } + Ok(path_list) + } else { + Err(UriListParseError::EmptyData) + } + } +} + +impl TypedData for SelectionReader { + fn try_read(&mut self) -> Option> { + Some(Box::new(io::Cursor::new(&self.data))) + } + + fn type_(&self) -> &dyn TransferType { + &self.type_ } } #[derive(Debug)] pub struct Dnd { xconn: Arc, + transfer_id: DataTransferId, // Populated by XdndEnter event handler pub version: Option, - pub type_list: Option>, + pub type_list: Option>, // Populated by XdndPosition event handler pub source_window: Option, // Populated by XdndPosition event handler pub position: PhysicalPosition, // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) - pub result: Option, DndDataParseError>>, + pub selection: Option, // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) pub dragging: bool, } +#[derive(Debug)] +pub struct Selection { + dnd: Arc>, +} + +impl Selection { + pub(crate) fn new(dnd: Arc>) -> Selection { + Selection { dnd } + } +} + +#[derive(Clone, Debug)] +pub struct SelectionType { + hint: Option, + atom: xproto::Atom, +} + +impl SelectionType { + pub(crate) fn from_dyn(atoms: &Atoms, type_: &dyn TransferType) -> Option { + type_.cast_ref().cloned().or_else(|| { + let hint = type_.hint()?; + + match hint { + TypeHint::UriList => Some(Self { hint: Some(hint), atom: atoms[TextUriList] }), + _ => None, + } + }) + } + + pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self { + let hint = if atom == atoms[TextUriList] { Some(TypeHint::UriList) } else { None }; + + Self { hint, atom } + } + + pub fn atom(&self) -> xproto::Atom { + self.atom + } +} + +impl PartialEq for SelectionType { + fn eq(&self, other: &Self) -> bool { + self.atom == other.atom + } +} + +impl TransferType for SelectionType { + fn hint(&self) -> Option { + self.hint.clone() + } +} + +impl DataTransfer for Selection { + fn available_types(&self) -> Box> + '_> { + Box::new( + self.dnd + .read() + .unwrap() + .type_list + .clone() + .into_iter() + .flat_map(|types| types.into_iter().map(|val| Box::new(val) as _)), + ) + } + + fn has_type(&self, type_: &dyn TransferType) -> bool { + let dnd = self.dnd.read().unwrap(); + + let Some(types) = dnd.type_list.as_ref() else { + return false; + }; + + if let Some(x11_type) = type_.cast_ref() { + types.contains(x11_type) + } else { + let Some(hint) = type_.hint() else { + return false; + }; + + types.iter().any(|haystack| haystack.hint() == Some(hint)) + } + } +} + impl Dnd { pub fn new(xconn: Arc) -> Result { Ok(Dnd { xconn, + transfer_id: DataTransferId::from_raw(0), version: None, type_list: None, source_window: None, position: PhysicalPosition::default(), - result: None, + selection: None, dragging: false, }) } + pub fn transfer_id(&self) -> DataTransferId { + self.transfer_id + } + pub fn reset(&mut self) { + self.transfer_id = DataTransferId::from_raw(self.transfer_id.into_raw().wrapping_add(1)); self.version = None; self.type_list = None; self.source_window = None; - self.result = None; + self.selection = None; self.dragging = false; } @@ -138,17 +283,16 @@ impl Dnd { ) } - pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { + pub unsafe fn convert_selection( + &self, + window: xproto::Window, + time: xproto::Timestamp, + new_type: xproto::Atom, + ) { let atoms = self.xconn.atoms(); self.xconn .xcb_connection() - .convert_selection( - window, - atoms[XdndSelection], - atoms[TextUriList], - atoms[XdndSelection], - time, - ) + .convert_selection(window, atoms[XdndSelection], new_type, atoms[XdndSelection], time) .expect_then_ignore_error("Failed to send XdndSelection event") } @@ -159,33 +303,4 @@ impl Dnd { let atoms = self.xconn.atoms(); self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList]) } - - pub fn parse_data(&self, data: &mut [c_uchar]) -> Result, DndDataParseError> { - if !data.is_empty() { - let mut path_list = Vec::new(); - let decoded = percent_decode(data).decode_utf8()?.into_owned(); - for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { - // The format is specified as protocol://host/path - // However, it's typically simply protocol:///path - let path_str = if uri.starts_with("file://") { - let path_str = uri.replace("file://", ""); - if !path_str.starts_with('/') { - // A hostname is specified - // Supporting this case is beyond the scope of my mental health - return Err(DndDataParseError::HostnameSpecified(path_str)); - } - path_str - } else { - // Only the file protocol is supported - return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); - }; - - let path = Path::new(&path_str).canonicalize()?; - path_list.push(path); - } - Ok(path_list) - } else { - Err(DndDataParseError::EmptyData) - } - } } diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index b89a06c1e1..ba7ce1b846 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -7,7 +7,7 @@ use std::os::raw::*; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; -use std::sync::{Arc, LazyLock, Mutex, Weak}; +use std::sync::{Arc, LazyLock, Mutex, RwLock, Weak}; use std::time::{Duration, Instant}; use std::{fmt, mem, ptr, slice, str}; @@ -36,7 +36,10 @@ use x11rb::protocol::{xkb, xproto}; use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; -use crate::atoms::*; +use crate::atoms::{ + _NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms, + WM_DELETE_WINDOW, +}; use crate::dnd::Dnd; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; @@ -168,6 +171,7 @@ impl PeekableReceiver { #[derive(Debug)] pub struct ActiveEventLoop { pub(crate) xconn: Arc, + pub(crate) dnd: Arc>, pub(crate) wm_delete_window: xproto::Atom, pub(crate) net_wm_ping: xproto::Atom, pub(crate) net_wm_sync_request: xproto::Atom, @@ -228,6 +232,7 @@ impl EventLoop { let dnd = Dnd::new(Arc::clone(&xconn)) .expect("Failed to call XInternAtoms when initializing drag and drop"); + let dnd = Arc::new(RwLock::new(dnd)); let (ime_sender, ime_receiver) = mpsc::channel(); let (ime_event_sender, ime_event_receiver) = mpsc::channel(); @@ -342,6 +347,7 @@ impl EventLoop { let window_target = ActiveEventLoop { ime, + dnd, root, control_flow: Cell::new(ControlFlow::default()), exit: Cell::new(None), @@ -368,7 +374,6 @@ impl EventLoop { let event_processor = EventProcessor { target: window_target, - dnd, devices: Default::default(), randr_event_offset, ime_receiver, diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 72c1c295f7..c30007cae3 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -30,8 +30,11 @@ use x11rb::protocol::xproto::{self, ConnectionExt as _, ModMask}; use x11rb::x11_utils::{ExtensionInformation, Serialize}; use xkbcommon_dl::xkb_mod_mask_t; -use crate::atoms::*; -use crate::dnd::{Dnd, DndState}; +use crate::atoms::{ + _XSETTINGS_SETTINGS, AtomName, TextUriList, XdndDrop, XdndEnter, XdndLeave, XdndPosition, + XdndSelection, +}; +use crate::dnd::{DndState, SelectionReader, SelectionType}; use crate::event_loop::{ ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType, ScrollOrientation, mkdid, mkwid, @@ -49,7 +52,6 @@ const KEYCODE_OFFSET: u8 = 8; #[derive(Debug)] pub struct EventProcessor { - pub dnd: Dnd, pub ime_receiver: ImeReceiver, pub ime_event_receiver: ImeEventReceiver, pub randr_event_offset: u8, @@ -79,6 +81,8 @@ pub struct EventProcessor { } impl EventProcessor { + const DND_TYPE: AtomName = TextUriList; + pub(crate) fn process_event(&mut self, xev: &mut XEvent, app: &mut dyn ApplicationHandler) { self.process_xevent(xev, app); @@ -359,6 +363,7 @@ impl EventProcessor { fn client_message(&mut self, xev: &XClientMessageEvent, app: &mut dyn ApplicationHandler) { let atoms = self.target.xconn.atoms(); + let mut dnd = self.target.dnd.write().unwrap(); let window = xev.window as xproto::Window; let window_id = mkwid(window); @@ -426,17 +431,25 @@ impl EventProcessor { let source_window = xev.data.get_long(0) as xproto::Window; let flags = xev.data.get_long(1); let version = flags >> 24; - self.dnd.version = Some(version); + dnd.version = Some(version); let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; if !has_more_types { - let type_list = vec![ + let type_list = [ xev.data.get_long(2) as xproto::Atom, xev.data.get_long(3) as xproto::Atom, xev.data.get_long(4) as xproto::Atom, - ]; - self.dnd.type_list = Some(type_list); - } else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } { - self.dnd.type_list = Some(more_types); + ] + .map(|ty_atom| SelectionType::new(atoms, ty_atom)) + .into_iter() + .collect(); + dnd.type_list = Some(type_list); + } else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } { + dnd.type_list = Some( + more_types + .into_iter() + .map(|ty_atom| SelectionType::new(atoms, ty_atom)) + .collect(), + ); } return; } @@ -464,31 +477,12 @@ impl EventProcessor { .xconn .translate_coords(self.target.root, window, x, y) .expect("Failed to translate window coordinates"); - self.dnd.position = PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64); + dnd.position = PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64); // By our own state flow, `version` should never be `None` at this point. - let version = self.dnd.version.unwrap_or(5); - - // Action is specified in versions 2 and up, though we don't need it anyway. - // let action = xev.data.get_long(4); - - let accepted = if let Some(ref type_list) = self.dnd.type_list { - type_list.contains(&atoms[TextUriList]) - } else { - false - }; - - if !accepted { - unsafe { - self.dnd - .send_status(window, source_window, DndState::Rejected) - .expect("Failed to send `XdndStatus` message."); - } - self.dnd.reset(); - return; - } + let version = dnd.version.unwrap_or(5); - self.dnd.source_window = Some(source_window); + dnd.source_window = Some(source_window); let time = if version == 0 { // In version 0, time isn't specified x11rb::CURRENT_TIME @@ -499,26 +493,14 @@ impl EventProcessor { // Log this timestamp. self.target.xconn.set_timestamp(time); - // This results in the `SelectionNotify` event below - unsafe { - self.dnd.convert_selection(window, time); - } - - unsafe { - self.dnd - .send_status(window, source_window, DndState::Accepted) - .expect("Failed to send `XdndStatus` message."); - } return; } if xev.message_type == atoms[XdndDrop] as c_ulong { - let (source_window, state) = if let Some(source_window) = self.dnd.source_window { - if let Some(Ok(ref path_list)) = self.dnd.result { - let event = WindowEvent::DragDropped { - paths: path_list.iter().map(Into::into).collect(), - position: self.dnd.position, - }; + let (source_window, state) = if let Some(source_window) = dnd.source_window { + if dnd.selection.as_ref().is_some() { + let event = + WindowEvent::DragDropped { id: dnd.transfer_id(), position: dnd.position }; app.window_event(&self.target, window_id, event); } (source_window, DndState::Accepted) @@ -530,21 +512,21 @@ impl EventProcessor { }; unsafe { - self.dnd - .send_finished(window, source_window, state) + dnd.send_finished(window, source_window, state) .expect("Failed to send `XdndFinished` message."); } - self.dnd.reset(); + dnd.reset(); return; } if xev.message_type == atoms[XdndLeave] as c_ulong { - if self.dnd.dragging { - let event = WindowEvent::DragLeft { position: Some(self.dnd.position) }; + if dnd.dragging { + let event = + WindowEvent::DragLeft { id: dnd.transfer_id(), position: Some(dnd.position) }; app.window_event(&self.target, window_id, event); } - self.dnd.reset(); + dnd.reset(); } } @@ -561,24 +543,23 @@ impl EventProcessor { return; } + let mut dnd_write = self.target.dnd.write().unwrap(); // This is where we receive data from drag and drop - self.dnd.result = None; - if let Ok(mut data) = unsafe { self.dnd.read_data(window) } { - let parse_result = self.dnd.parse_data(&mut data); - - if let Ok(ref path_list) = parse_result { - let event = if self.dnd.dragging { - WindowEvent::DragMoved { position: self.dnd.position } - } else { - let paths = path_list.iter().map(Into::into).collect(); - self.dnd.dragging = true; - WindowEvent::DragEntered { paths, position: self.dnd.position } - }; - - app.window_event(&self.target, window_id, event); - } + let data = unsafe { dnd_write.read_data(window) }; + let ty_ = SelectionType::new(atoms, atoms[Self::DND_TYPE]); + dnd_write.selection = data.ok().map(|data| SelectionReader::new(ty_, data.into())); + if dnd_write.selection.is_some() { + let event = if dnd_write.dragging { + WindowEvent::DragMoved { id: dnd_write.transfer_id(), position: dnd_write.position } + } else { + dnd_write.dragging = true; + WindowEvent::DragEntered { + id: dnd_write.transfer_id(), + position: dnd_write.position, + } + }; - self.dnd.result = Some(parse_result); + app.window_event(&self.target, window_id, event); } } diff --git a/winit-x11/src/lib.rs b/winit-x11/src/lib.rs index 41bb1382e6..79c9d05e8a 100644 --- a/winit-x11/src/lib.rs +++ b/winit-x11/src/lib.rs @@ -26,6 +26,8 @@ mod window; mod xdisplay; mod xsettings; +pub use dnd::{Selection, SelectionReader, SelectionType, UriListParseError}; + /// X window type. Maps directly to /// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 0277732110..3e57f58506 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -5,13 +5,14 @@ use std::num::NonZeroU32; use std::ops::Deref; use std::os::raw::*; use std::path::Path; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; use std::{cmp, env}; use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use tracing::{debug, info, warn}; use winit_core::application::ApplicationHandler; use winit_core::cursor::Cursor; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType}; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::AsyncRequestSerial; @@ -21,8 +22,8 @@ use winit_core::monitor::{ }; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest as CoreImeRequest, ImeRequestError, - ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, - WindowButtons, WindowId, WindowLevel, + ResizeDirection, Theme, UnknownDataTransfer, UserAttentionType, Window as CoreWindow, + WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use x11rb::connection::{Connection, RequestConnection}; use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; @@ -31,7 +32,15 @@ use x11rb::protocol::sync::{ConnectionExt as _, Int64}; use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle}; use x11rb::protocol::{randr, xinput}; -use crate::atoms::*; +use crate::atoms::{ + _GTK_THEME_VARIANT, _NET_ACTIVE_WINDOW, _NET_WM_ICON, _NET_WM_MOVERESIZE, _NET_WM_NAME, + _NET_WM_PID, _NET_WM_PING, _NET_WM_STATE, _NET_WM_STATE_ABOVE, _NET_WM_STATE_BELOW, + _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_HIDDEN, _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_SYNC_REQUEST, _NET_WM_SYNC_REQUEST_COUNTER, + _NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE, + WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware, +}; +use crate::dnd::{Dnd, DndState, Selection}; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, WakeSender, X11Error, xinput_fp1616_to_float, @@ -40,7 +49,7 @@ use crate::ime::{ImeRequest, ImeSender}; use crate::monitor::MonitorHandle as X11MonitorHandle; use crate::util::{self, CustomCursor, SelectedCursor, rgba_to_cardinals}; use crate::xdisplay::XConnection; -use crate::{WindowAttributesX11, WindowType, ffi}; +use crate::{SelectionType, WindowAttributesX11, WindowType, ffi}; #[derive(Debug)] pub struct Window(Arc); @@ -302,6 +311,104 @@ impl CoreWindow for Window { fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { self } + + fn data_transfer( + &self, + id: DataTransferId, + ) -> Result, UnknownDataTransfer> { + let Some(dnd) = self.dnd.upgrade() else { + return Err(UnknownDataTransfer(id)); + }; + + if dnd.read().unwrap().transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + Ok(Box::new(Selection::new(dnd))) + } + + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let Some(dnd) = self.dnd.upgrade() else { + return Err(UnknownDataTransfer(id)); + }; + + let mut dnd = dnd.write().unwrap(); + if dnd.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + let Some(source_window) = dnd.source_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + let window = self.0.xwindow; + + unsafe { + dnd.send_status(window, source_window, DndState::Rejected) + .expect("Failed to send `XdndStatus` message."); + } + dnd.reset(); + + Ok(()) + } + + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let Some(dnd) = self.dnd.upgrade() else { + return Err(UnknownDataTransfer(id)); + }; + + let dnd = dnd.read().unwrap(); + if dnd.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + let Some(source_window) = dnd.source_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + let window = self.0.xwindow; + + unsafe { + dnd.send_status(window, source_window, DndState::Accepted) + .expect("Failed to send `XdndStatus` message."); + } + + Ok(()) + } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result<(), UnknownDataTransfer> { + let Some(dnd) = self.dnd.upgrade() else { + return Err(UnknownDataTransfer(id)); + }; + + let dnd = dnd.read().unwrap(); + if dnd.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + if dnd.source_window.is_none() { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + } + + let window = self.0.xwindow; + + let atoms = self.0.xconn.atoms(); + + let type_ = SelectionType::from_dyn(atoms, type_).ok_or(UnknownDataTransfer(id))?; + + // This results in the `SelectionNotify` event below + unsafe { + // TODO: Handle this better + dnd.convert_selection(window, self.0.xconn.timestamp(), type_.atom()); + } + + Ok(()) + } } impl rwh_06::HasDisplayHandle for Window { @@ -413,10 +520,11 @@ unsafe impl Sync for UnownedWindow {} #[derive(Debug)] pub struct UnownedWindow { pub(crate) xconn: Arc, // never changes - xwindow: xproto::Window, // never changes + dnd: Weak>, + xwindow: xproto::Window, // never changes #[allow(dead_code)] visual: u32, // never changes - root: xproto::Window, // never changes + root: xproto::Window, // never changes #[allow(dead_code)] screen_id: i32, // never changes sync_counter_id: Option, // never changes @@ -642,6 +750,7 @@ impl UnownedWindow { let mut window = UnownedWindow { xconn: Arc::clone(xconn), xwindow: xwindow as xproto::Window, + dnd: Arc::downgrade(&event_loop.dnd), visual, root, screen_id, diff --git a/winit/examples/application.rs b/winit/examples/application.rs index 4098ebff68..2e0ccded35 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -548,6 +548,7 @@ impl ApplicationHandler for Application { | WindowEvent::DragEntered { .. } | WindowEvent::DragMoved { .. } | WindowEvent::DragDropped { .. } + | WindowEvent::DataTransferResult { .. } | WindowEvent::Destroyed | WindowEvent::Ime(_) | WindowEvent::Moved(_) => (), From 3c01cb8d9db4a137b45b83932e62f1324a26b585 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 20 May 2026 13:24:15 +0200 Subject: [PATCH 13/87] Add plaintext + URI helpers, remove MIME --- Cargo.toml | 1 - winit-core/Cargo.toml | 2 -- winit-core/src/data_transfer.rs | 28 ++++++++++++++++++++++++++++ winit-wayland/Cargo.toml | 1 - 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 93d6c970a2..3321df958d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,6 @@ calloop = "0.14.3" foldhash = { version = "0.2.0", default-features = false, features = ["std"] } libc = "0.2.64" memmap2 = "0.9.0" -mime = "0.3.17" percent-encoding = "2.0" rustix = { version = "1.0.7", default-features = false } x11-dl = "2.19.1" diff --git a/winit-core/Cargo.toml b/winit-core/Cargo.toml index e6b3adec19..7b47cc4d33 100644 --- a/winit-core/Cargo.toml +++ b/winit-core/Cargo.toml @@ -27,8 +27,6 @@ bitflags.workspace = true cursor-icon.workspace = true dpi.workspace = true keyboard-types.workspace = true -mime.workspace = true -foldhash.workspace = true rwh_06.workspace = true serde = { workspace = true, optional = true } smol_str.workspace = true diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index adf50f5435..006a796cfc 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; +use std::ffi::{OsStr, OsString}; use std::fmt::{self, Debug}; use std::io; use std::ops::Deref; @@ -50,6 +52,32 @@ impl_dyn_casting!(TransferType); pub trait TypedData: AsAny + Send + Sync + fmt::Debug { fn type_(&self) -> &dyn TransferType; fn try_read(&mut self) -> Option>; + + fn try_as_uris(&mut self) -> Option>> { + if self.type_().hint() != Some(TypeHint::UriList) { + return None; + } + + let mut reader = self.try_read()?; + let mut out = String::new(); + reader.read_to_string(&mut out).ok()?; + + let uris = out.split(|c| c == '\n' || c == '\r'); + + Some(uris.map(|str| OsString::from(str).into()).collect()) + } + + fn try_as_plaintext(&mut self) -> Option { + if self.type_().hint() != Some(TypeHint::UriList) { + return None; + } + + let mut reader = self.try_read()?; + let mut out = String::new(); + reader.read_to_string(&mut out).ok()?; + + Some(out) + } } #[derive(Debug, Clone)] diff --git a/winit-wayland/Cargo.toml b/winit-wayland/Cargo.toml index c1c77d95ad..27a5972337 100644 --- a/winit-wayland/Cargo.toml +++ b/winit-wayland/Cargo.toml @@ -27,7 +27,6 @@ serde = { workspace = true, optional = true } smol_str.workspace = true tracing.workspace = true winit-core.workspace = true -mime.workspace = true # Platform-specific calloop.workspace = true From b7fcfe0e18dbde51b05c270d971742f4a0d6925d Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 20 May 2026 14:28:51 +0200 Subject: [PATCH 14/87] Add more documentation --- winit-core/src/data_transfer.rs | 100 +++++++++++++++++++++++++++++--- winit-core/src/window.rs | 11 +++- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 006a796cfc..36286f8f12 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -1,3 +1,48 @@ +//! Types related to data transfer, used for clipboard and drag-and-drop. +//! +//! This module contains types and traits implementing cross-application data transfer. +//! While the precise implementation depends on platform, there are a set of types which +//! can be safely transferred between applications on all platforms (see [`TypeHint`]). +//! +//! On all platforms, the process looks something like this: +//! +//! - A data transfer advertises a set of types which the data can be interpreted as +//! - For example, if you copy or drag text from a web page, the browser may advertise the text +//! formatted using HTML, the text formatted as RTF, and the text with all formatting removed +//! simultaneously. +//! - An application receiving a data transfer chooses one or more types that it understands and +//! requests the data in those formats (in practice, it will usually only request a single +//! format). +//! - The source application converts the data stored in its memory to the requested format and +//! asynchronously sends it to the target application +//! +//! On some platforms, the data is sometimes available synchronously, but all platforms have at +//! least some method of sending the data asynchronously and some types of data that may _only_ be +//! sent using the asynchronous interface. Because of this, the API in winit must be asynchronous. +//! +//! The flow for a user application that implements drag-and-drop would look something like this: +//! +//! - The application receives a [`DragEnter`](crate::event::WindowEvent::DragEnter) event. This +//! event supplies a [`DataTransferId`] which can be used to request information or operations on +//! the dragged data by using methods on [`Window`](crate::window::Window). +//! - As the drag operation continues, the window will receive +//! [`DragMoved`](crate::event::WindowEvent::DragEnter) events. +//! - While `DragMoved` events are being received, the receiving application may mark the data as +//! being accepted or rejected. This will update the OS/compositor to display the correct UI to +//! the user. Accepting does not "finalize" the drag operation, nor does rejecting cancel it. +//! - At any point during this operation, the receiving application may request either the available +//! types or even the data being transferred. This may be useful in cases where the application +//! wants to preload the data. For example, an image editor may want to display the image on the +//! canvas during the drag operation. +//! - When the user tries to drop the data onto the window, that window will receive a +//! [`DragDropped`](crate::event::WindowEvent::DragDropped) event. In general, the receiving +//! application should assume that calling [`reject_drag`](crate::window::Window::reject_drag) +//! after `DragDropped` is received ends the lifecycle of the data transfer. +//! +//! If platform-dependent behavior is required, a platform may define internal types +//! implementing the traits in this module, which can then be accessed in an application +//! using the methods defined on [`dyn AsAny`]. See each platform's documentation for details. + use std::borrow::Cow; use std::ffi::{OsStr, OsString}; use std::fmt::{self, Debug}; @@ -7,7 +52,7 @@ use std::sync::Arc; use crate::as_any::AsAny; -/// Identifier of a data transfer. +/// Unique identifier for a data transfer. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DataTransferId(i64); @@ -27,17 +72,49 @@ impl DataTransferId { } } +/// The set of types supported cross-platform. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TypeHint { + /// Plain UTF-8 text (see [`TypedData::try_as_plaintext`]). + /// + /// **Note for platform implementations**: this hint is _only_ for UTF-8 text. If the platform + /// returns plaintext in some format other than UTF-8 by default, a [`TypedData`] + /// implementation marked with this type hint should convert to UTF-8. Plaintext, + /// A list of URIs in the format defined by the `text/uri-list` MIME type, encoded as UTF-8 (see + /// [`TypedData::try_as_uris`]). + /// + /// **Note for platform implementations**: this hint is _only_ for URIs encoded precisely in the + /// format specified above. If the platform uses a different format, a [`TypedData`] + /// implementation marked with this type hint should convert to that format. UriList, + /// A HTML-formatted string Html, + /// An RTF-formatted string Rtf, - Audio { extension_hint: Option<&'static str> }, - Image { extension_hint: Option<&'static str> }, + /// Audio + Audio { + /// An optional hint for the encoding of the supplied bytes, specified using the standard + /// file extension for that audio format, lowercase and without the leading `.`. + extension_hint: Option<&'static str>, + }, + /// Image data + Image { + /// An optional hint for the encoding of the supplied bytes, specified using the standard + /// file extension for that audio format, lowercase and without the leading `.`. + extension_hint: Option<&'static str>, + }, } +/// The type of a data transfer. +/// +/// [`hint`](TransferType::hint) can be called to get the type in +/// a cross-platform format (see [`TypeHint`]) pub trait TransferType: AsAny + Send + Sync + fmt::Debug { + /// Get the cross-platform representation of this type. + /// + /// If this returns `None`, then this is a platform-dependent type that has no cross-platform + /// equivalent. fn hint(&self) -> Option; } @@ -49,6 +126,7 @@ impl TransferType for TypeHint { impl_dyn_casting!(TransferType); +/// Data that has been fetched from a data transfer pub trait TypedData: AsAny + Send + Sync + fmt::Debug { fn type_(&self) -> &dyn TransferType; fn try_read(&mut self) -> Option>; @@ -99,16 +177,24 @@ impl Deref for DynTypedData { impl_dyn_casting!(TypedData); +/// Metadata about a data transfer. This does not allow actually receiving data, as that is an +/// asynchronous operation. To fetch the data from the source application, see +/// [`Window::fetch_data_transfer`](crate::window::Window::fetch_data_transfer) +/// and [`WindowEvent::DataTransferResult`](crate::event::WindowEvent::DataTransferResult). pub trait DataTransfer: AsAny + Send + Sync + fmt::Debug { - /// Display the list of all available MIME types. + /// Display the list of all available types. /// - /// This is useful if more-complex MIME type matching is required, but for most cases + /// This is useful if more-complex type matching is required, but for most cases /// [`has_type`](DataTransfer::has_type) should be used. // TODO: We should be able to do `&dyn TransferType`, but some implementation details in - // the platforms make that unnecessarily difficult right now. + // the platforms make that unnecessarily difficult right now. Specifically, use of `RwLock`. fn available_types(&self) -> Box> + '_>; - /// Check if the supplied MIME type is provided by this [`DataTransfer`]. + /// Check if the supplied type is provided by this [`DataTransfer`]. + /// + /// Supplying a [`TypeHint`] as the type is supported on all platforms, but if some + /// platform-specific type is required then that platform's implementation of `TransferType` can + /// be used. fn has_type(&self, type_: &dyn TransferType) -> bool { type_.hint().is_some_and(|hint| { self.available_types().any(|haystack| haystack.hint() == Some(hint)) diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index d0e046ef85..7a690f6468 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1194,7 +1194,12 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// Request to fetch a type from a [data transfer](DataTransfer). /// - /// The data will be supplied via [`WindowEvent::DataTransferResult`]. + /// The data will be supplied via [`WindowEvent::DataTransferResult`], when it is + /// ready. + /// + /// This does not require [`accept_drag`](Window::accept_drag) or + /// [`accept_drag_type`](Window::accept_drag_type) to be called first. If that is a requirement + /// of the platform, then the platform implementation should handle that internally. /// /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will /// return an error. @@ -1213,7 +1218,9 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// can be dropped. /// /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying - /// one or more accepted types. Using this method will mark all available types as accepted. + /// one or more accepted types. For platforms that require specifying a type, `accept_drag` will + /// mark all available types as accepted. + /// /// For the most reliable cross-platform behaviour, /// [`accept_drag_type`](Window::accept_drag_type) is preferred, although in most cases /// simply conditionally accepting the data transfer based on whether or not it advertises a From 2234a27a431c48f5b5b97a5954c8764811c45f9b Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 21 May 2026 13:38:57 +0200 Subject: [PATCH 15/87] Fix drag-and-drop on X11 --- winit-core/src/data_transfer.rs | 64 ++++--------- winit-core/src/event.rs | 49 +++------- winit-core/src/window.rs | 62 +++++++------ winit-x11/src/dnd.rs | 95 +++++++++++++------- winit-x11/src/event_loop.rs | 3 +- winit-x11/src/event_processor.rs | 150 ++++++++++++++++++------------- winit-x11/src/window.rs | 68 +++++++++++--- winit/examples/application.rs | 2 +- winit/examples/dnd.rs | 87 ++++++++++++++++-- winit/src/lib.rs | 4 +- 10 files changed, 353 insertions(+), 231 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 36286f8f12..2457bc8a46 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -43,12 +43,8 @@ //! implementing the traits in this module, which can then be accessed in an application //! using the methods defined on [`dyn AsAny`]. See each platform's documentation for details. -use std::borrow::Cow; -use std::ffi::{OsStr, OsString}; use std::fmt::{self, Debug}; use std::io; -use std::ops::Deref; -use std::sync::Arc; use crate::as_any::AsAny; @@ -128,51 +124,24 @@ impl_dyn_casting!(TransferType); /// Data that has been fetched from a data transfer pub trait TypedData: AsAny + Send + Sync + fmt::Debug { + /// The type of this `TypedData`. fn type_(&self) -> &dyn TransferType; - fn try_read(&mut self) -> Option>; - - fn try_as_uris(&mut self) -> Option>> { - if self.type_().hint() != Some(TypeHint::UriList) { - return None; - } - - let mut reader = self.try_read()?; - let mut out = String::new(); - reader.read_to_string(&mut out).ok()?; - - let uris = out.split(|c| c == '\n' || c == '\r'); - - Some(uris.map(|str| OsString::from(str).into()).collect()) - } - - fn try_as_plaintext(&mut self) -> Option { - if self.type_().hint() != Some(TypeHint::UriList) { - return None; - } - - let mut reader = self.try_read()?; - let mut out = String::new(); - reader.read_to_string(&mut out).ok()?; - Some(out) - } -} - -#[derive(Debug, Clone)] -pub struct DynTypedData(pub Arc); - -impl PartialEq for DynTypedData { - fn eq(&self, other: &Self) -> bool { - std::ptr::addr_eq(&**self, &**other) - } -} + /// If this value is readable as bytes, return a reader than can be used to read those bytes. + fn try_read(&mut self) -> Option>; -impl Deref for DynTypedData { - type Target = dyn TypedData; + /// Read this value as a list of URIs. + /// + /// If this value is not readable as URIs, return `None`. + /// + /// The format of the returned URIs is simply a vector of strings. No validation is done + /// to ensure that the URIs are valid or in the format + fn try_as_uris(&mut self) -> Option>; - fn deref(&self) -> &Self::Target { - &*self.0 - } + /// Read this value as a plain text string. + /// + /// If this value is not readable as a string, return `None`. + fn try_as_plaintext(&mut self) -> Option; } impl_dyn_casting!(TypedData); @@ -188,7 +157,7 @@ pub trait DataTransfer: AsAny + Send + Sync + fmt::Debug { /// [`has_type`](DataTransfer::has_type) should be used. // TODO: We should be able to do `&dyn TransferType`, but some implementation details in // the platforms make that unnecessarily difficult right now. Specifically, use of `RwLock`. - fn available_types(&self) -> Box> + '_>; + fn available_types(&self) -> Vec>; /// Check if the supplied type is provided by this [`DataTransfer`]. /// @@ -196,8 +165,9 @@ pub trait DataTransfer: AsAny + Send + Sync + fmt::Debug { /// platform-specific type is required then that platform's implementation of `TransferType` can /// be used. fn has_type(&self, type_: &dyn TransferType) -> bool { + let available_types = self.available_types(); type_.hint().is_some_and(|hint| { - self.available_types().any(|haystack| haystack.hint() == Some(hint)) + available_types.iter().any(|haystack| haystack.hint() == Some(hint)) }) } } diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index 408b09824a..ea2ea38c00 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use crate::Instant; -use crate::data_transfer::{DataTransferId, DynTypedData}; +use crate::data_transfer::DataTransferId; use crate::error::RequestError; use crate::event_loop::AsyncRequestSerial; use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; @@ -46,10 +46,7 @@ pub enum StartCause { #[derive(Debug, Clone, PartialEq)] pub enum WindowEvent { /// The activation token was delivered back and now could be used. - ActivationTokenDone { - serial: AsyncRequestSerial, - token: ActivationToken, - }, + ActivationTokenDone { serial: AsyncRequestSerial, token: ActivationToken }, /// The size of the window's surface has changed. /// @@ -82,13 +79,9 @@ pub enum WindowEvent { DragEntered { /// Data transfer object specifying the ID and available types. id: DataTransferId, - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). - position: PhysicalPosition, }, /// A file drag operation has moved over the window. - DragMoved { + DragPosition { /// Data transfer object specifying the ID and available types. id: DataTransferId, /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be @@ -100,29 +93,15 @@ pub enum WindowEvent { DragDropped { /// Data transfer object specifying the ID and available types. id: DataTransferId, - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). - position: PhysicalPosition, }, /// The file drag operation has been cancelled or left the window. DragLeft { /// Data transfer object specifying the ID and available types. id: DataTransferId, - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). - /// - /// ## Platform-specific - /// - /// - **Windows:** Always emits [`None`]. - position: Option>, }, - DataTransferResult { - id: DataTransferId, - data: DynTypedData, - }, + /// The result of a data transfer is available. + DataTransferResult { id: DataTransferId, serial: AsyncRequestSerial }, /// The window gained or lost focus. /// @@ -253,11 +232,7 @@ pub enum WindowEvent { }, /// A mouse wheel movement or touchpad scroll occurred. - MouseWheel { - device_id: Option, - delta: MouseScrollDelta, - phase: TouchPhase, - }, + MouseWheel { device_id: Option, delta: MouseScrollDelta, phase: TouchPhase }, /// An mouse button press has been received. PointerButton { @@ -332,9 +307,7 @@ pub enum WindowEvent { /// /// - Only available on **macOS 10.8** and later, and **iOS**. /// - On iOS, not recognized by default. It must be enabled when needed. - DoubleTapGesture { - device_id: Option, - }, + DoubleTapGesture { device_id: Option }, /// Two-finger rotation gesture. /// @@ -1587,10 +1560,10 @@ mod tests { with_window_event(Focused(true)); with_window_event(Moved((0, 0).into())); with_window_event(SurfaceResized((0, 0).into())); - with_window_event(DragEntered { id: dnd_data, position: (0, 0).into() }); - with_window_event(DragMoved { id: dnd_data, position: (0, 0).into() }); - with_window_event(DragDropped { id: dnd_data, position: (0, 0).into() }); - with_window_event(DragLeft { id: dnd_data, position: Some((0, 0).into()) }); + with_window_event(DragEntered { id: dnd_data}); + with_window_event(DragPosition { id: dnd_data, position: (0, 0).into() }); + with_window_event(DragDropped { id: dnd_data }); + with_window_event(DragLeft { id: dnd_data }); with_window_event(Ime(Enabled)); with_window_event(PointerMoved { device_id: None, diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 7a690f6468..4900b939f9 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -11,8 +11,9 @@ use serde::{Deserialize, Serialize}; use crate::as_any::AsAny; use crate::cursor::Cursor; -use crate::data_transfer::{DataTransfer, DataTransferId, TransferType}; -use crate::error::RequestError; +use crate::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; +use crate::error::{NotSupportedError, RequestError}; +use crate::event_loop::AsyncRequestSerial; use crate::icon::Icon; use crate::monitor::{Fullscreen, MonitorHandle}; @@ -1188,27 +1189,7 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { fn data_transfer( &self, id: DataTransferId, - ) -> Result, UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } - - /// Request to fetch a type from a [data transfer](DataTransfer). - /// - /// The data will be supplied via [`WindowEvent::DataTransferResult`], when it is - /// ready. - /// - /// This does not require [`accept_drag`](Window::accept_drag) or - /// [`accept_drag_type`](Window::accept_drag_type) to be called first. If that is a requirement - /// of the platform, then the platform implementation should handle that internally. - /// - /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will - /// return an error. - fn fetch_data_transfer( - &self, - id: DataTransferId, - type_: &dyn TransferType, - ) -> Result<(), UnknownDataTransfer> { - let _ = type_; + ) -> Result, UnknownDataTransfer> { Err(UnknownDataTransfer(id)) } @@ -1222,9 +1203,7 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// mark all available types as accepted. /// /// For the most reliable cross-platform behaviour, - /// [`accept_drag_type`](Window::accept_drag_type) is preferred, although in most cases - /// simply conditionally accepting the data transfer based on whether or not it advertises a - /// supported type will do the right thing. + /// [`accept_drag_type`](Window::accept_drag_type) is preferred. fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { Err(UnknownDataTransfer(id)) } @@ -1256,6 +1235,37 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { Err(UnknownDataTransfer(id)) } + /// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer). + /// + /// This may be called multiple times on the same [`DataTransferId`] with different types. + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let _ = id; + let _ = type_; + Err(RequestError::NotSupported(NotSupportedError::new( + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ + this platform", + ))) + } + + /// Get the resolved data for a data transfer. + /// + /// Requires first calling [`fetch_data_transfer`](ActiveEventLoop::fetch_data_transfer) and + /// waiting for the corresponding `DataTransferResult` event. + fn data_transfer_result( + &self, + serial: AsyncRequestSerial, + ) -> Result, RequestError> { + let _ = serial; + Err(RequestError::NotSupported(NotSupportedError::new( + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ + this platform", + ))) + } + /// Atomically apply request to IME. /// /// For details consult [`ImeRequest`] and [`ImeCapabilities`]. diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index c7557633b6..a1e005bb22 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -4,9 +4,9 @@ use std::path::{Path, PathBuf}; use std::str::Utf8Error; use std::sync::{Arc, RwLock}; -use dpi::PhysicalPosition; use percent_encoding::percent_decode; use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use winit_core::event_loop::AsyncRequestSerial; use x11rb::protocol::xproto::{self, ConnectionExt}; use crate::atoms::AtomName::None as DndNone; @@ -93,6 +93,39 @@ impl TypedData for SelectionReader { fn type_(&self) -> &dyn TransferType { &self.type_ } + + fn try_as_plaintext(&mut self) -> Option { + // We don't check that the type of this data is plaintext, as other types (e.g. HTML, URI + // list) are valid to read as plaintext + str::from_utf8(&self.data).ok().map(Into::into) + } + + fn try_as_uris(&mut self) -> Option> { + if self.type_().hint() != Some(TypeHint::UriList) { + return None; + } + + Some( + self.try_as_plaintext()? + .split(|c| c == '\n' || c == '\r') + .filter(|s| !s.is_empty()) + .map(Into::into) + .collect(), + ) + } +} + +#[derive(Debug)] +pub struct SelectionFetchState { + pub serial: AsyncRequestSerial, + // Populated by SelectionNotify event handler + pub value: Option>>, +} + +impl SelectionFetchState { + pub fn new() -> Self { + Self { serial: AsyncRequestSerial::get(), value: None } + } } #[derive(Debug)] @@ -101,15 +134,11 @@ pub struct Dnd { transfer_id: DataTransferId, // Populated by XdndEnter event handler pub version: Option, - pub type_list: Option>, + pub type_infos: Option>, // Populated by XdndPosition event handler pub source_window: Option, - // Populated by XdndPosition event handler - pub position: PhysicalPosition, - // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) - pub selection: Option, - // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) - pub dragging: bool, + // Populated by `fetch_data_transfer` + pub last_fetched_selection: Option, } #[derive(Debug)] @@ -165,27 +194,26 @@ impl TransferType for SelectionType { } impl DataTransfer for Selection { - fn available_types(&self) -> Box> + '_> { - Box::new( - self.dnd - .read() - .unwrap() - .type_list - .clone() - .into_iter() - .flat_map(|types| types.into_iter().map(|val| Box::new(val) as _)), - ) + fn available_types(&self) -> Vec> { + self.dnd + .read() + .unwrap() + .type_infos + .as_ref() + .into_iter() + .flat_map(|types| types.iter().map(|val| Box::new(val.clone()) as _)) + .collect() } fn has_type(&self, type_: &dyn TransferType) -> bool { let dnd = self.dnd.read().unwrap(); - let Some(types) = dnd.type_list.as_ref() else { + let Some(types) = dnd.type_infos.as_ref() else { return false; }; if let Some(x11_type) = type_.cast_ref() { - types.contains(x11_type) + types.iter().any(|haystack| haystack == x11_type) } else { let Some(hint) = type_.hint() else { return false; @@ -197,17 +225,19 @@ impl DataTransfer for Selection { } impl Dnd { - pub fn new(xconn: Arc) -> Result { - Ok(Dnd { + pub fn new(xconn: Arc) -> Self { + Self::with_id(xconn, DataTransferId::from_raw(0)) + } + + fn with_id(xconn: Arc, transfer_id: DataTransferId) -> Self { + Dnd { xconn, - transfer_id: DataTransferId::from_raw(0), + transfer_id, version: None, - type_list: None, + type_infos: None, source_window: None, - position: PhysicalPosition::default(), - selection: None, - dragging: false, - }) + last_fetched_selection: None, + } } pub fn transfer_id(&self) -> DataTransferId { @@ -215,12 +245,9 @@ impl Dnd { } pub fn reset(&mut self) { - self.transfer_id = DataTransferId::from_raw(self.transfer_id.into_raw().wrapping_add(1)); - self.version = None; - self.type_list = None; - self.source_window = None; - self.selection = None; - self.dragging = false; + let xconn = self.xconn.clone(); + let new_id = DataTransferId::from_raw(self.transfer_id.into_raw().wrapping_add(1)); + *self = Self::with_id(xconn, new_id); } pub unsafe fn send_status( diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index ba7ce1b846..b86a292b22 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -230,8 +230,7 @@ impl EventLoop { let net_wm_ping = atoms[_NET_WM_PING]; let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST]; - let dnd = Dnd::new(Arc::clone(&xconn)) - .expect("Failed to call XInternAtoms when initializing drag and drop"); + let dnd = Dnd::new(Arc::clone(&xconn)); let dnd = Arc::new(RwLock::new(dnd)); let (ime_sender, ime_receiver) = mpsc::channel(); diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index c30007cae3..3125f74a7c 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -1,8 +1,8 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, VecDeque}; use std::os::raw::{c_char, c_int, c_long, c_ulong}; -use std::slice; use std::sync::{Arc, Mutex}; +use std::{io, slice}; use dpi::{PhysicalPosition, PhysicalSize}; use winit_common::xkb::{self, Context, XkbState}; @@ -363,7 +363,6 @@ impl EventProcessor { fn client_message(&mut self, xev: &XClientMessageEvent, app: &mut dyn ApplicationHandler) { let atoms = self.target.xconn.atoms(); - let mut dnd = self.target.dnd.write().unwrap(); let window = xev.window as xproto::Window; let window_id = mkwid(window); @@ -428,29 +427,43 @@ impl EventProcessor { } if xev.message_type == atoms[XdndEnter] as c_ulong { - let source_window = xev.data.get_long(0) as xproto::Window; - let flags = xev.data.get_long(1); - let version = flags >> 24; - dnd.version = Some(version); - let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; - if !has_more_types { - let type_list = [ - xev.data.get_long(2) as xproto::Atom, - xev.data.get_long(3) as xproto::Atom, - xev.data.get_long(4) as xproto::Atom, - ] - .map(|ty_atom| SelectionType::new(atoms, ty_atom)) - .into_iter() - .collect(); - dnd.type_list = Some(type_list); - } else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } { - dnd.type_list = Some( - more_types - .into_iter() - .map(|ty_atom| SelectionType::new(atoms, ty_atom)) - .collect(), - ); - } + let transfer_id = { + let mut dnd = self.target.dnd.write().unwrap(); + // We only reset when a new drag-and-drop enters, since that means that the user can + // read the drag info in the window event handler. + dnd.reset(); + + let source_window = xev.data.get_long(0) as xproto::Window; + let flags = xev.data.get_long(1); + + let version = flags >> 24; + dnd.version = Some(version); + dnd.source_window = Some(source_window); + + let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; + if !has_more_types { + let type_list = [ + xev.data.get_long(2) as xproto::Atom, + xev.data.get_long(3) as xproto::Atom, + xev.data.get_long(4) as xproto::Atom, + ] + .map(|ty_atom| SelectionType::new(atoms, ty_atom).into()) + .into_iter() + .collect(); + dnd.type_infos = Some(type_list); + } else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } { + dnd.type_infos = Some( + more_types + .into_iter() + .map(|ty_atom| SelectionType::new(atoms, ty_atom).into()) + .collect(), + ); + } + + dnd.transfer_id() + }; + + app.window_event(&self.target, window_id, WindowEvent::DragEntered { id: transfer_id }); return; } @@ -477,32 +490,37 @@ impl EventProcessor { .xconn .translate_coords(self.target.root, window, x, y) .expect("Failed to translate window coordinates"); - dnd.position = PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64); - // By our own state flow, `version` should never be `None` at this point. - let version = dnd.version.unwrap_or(5); + let transfer_id = { + let mut dnd = self.target.dnd.write().unwrap(); + // By our own state flow, `version` should never be `None` at this point. + let version = dnd.version.unwrap_or(5); - dnd.source_window = Some(source_window); - let time = if version == 0 { - // In version 0, time isn't specified - x11rb::CURRENT_TIME - } else { - xev.data.get_long(3) as xproto::Timestamp + dnd.source_window = Some(source_window); + let time = if version == 0 { + // In version 0, time isn't specified + x11rb::CURRENT_TIME + } else { + xev.data.get_long(3) as xproto::Timestamp + }; + + // Log this timestamp. + self.target.xconn.set_timestamp(time); + + dnd.transfer_id() }; - // Log this timestamp. - self.target.xconn.set_timestamp(time); + app.window_event(&self.target, window_id, WindowEvent::DragPosition { + id: transfer_id, + position: PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64), + }); return; } if xev.message_type == atoms[XdndDrop] as c_ulong { + let dnd = self.target.dnd.read().unwrap(); let (source_window, state) = if let Some(source_window) = dnd.source_window { - if dnd.selection.as_ref().is_some() { - let event = - WindowEvent::DragDropped { id: dnd.transfer_id(), position: dnd.position }; - app.window_event(&self.target, window_id, event); - } (source_window, DndState::Accepted) } else { // `source_window` won't be part of our DND state if we already rejected the drop in @@ -511,22 +529,23 @@ impl EventProcessor { (source_window, DndState::Rejected) }; + // TODO: Ensure that this is sent after `dnd` lock is released, to prevent + // accidentally introducing a deadlock later down the line. + app.window_event(&self.target, window_id, WindowEvent::DragDropped { + id: dnd.transfer_id(), + }); + unsafe { dnd.send_finished(window, source_window, state) .expect("Failed to send `XdndFinished` message."); } - dnd.reset(); return; } if xev.message_type == atoms[XdndLeave] as c_ulong { - if dnd.dragging { - let event = - WindowEvent::DragLeft { id: dnd.transfer_id(), position: Some(dnd.position) }; - app.window_event(&self.target, window_id, event); - } - dnd.reset(); + let transfer_id = self.target.dnd.read().unwrap().transfer_id(); + app.window_event(&self.target, window_id, WindowEvent::DragLeft { id: transfer_id }); } } @@ -539,28 +558,33 @@ impl EventProcessor { // Set the timestamp. self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); + // For now, winit only supports selections for drag-and-drop. This should be changed + // when clipboard support is implemented. if xev.property != atoms[XdndSelection] as c_ulong { return; } - let mut dnd_write = self.target.dnd.write().unwrap(); // This is where we receive data from drag and drop - let data = unsafe { dnd_write.read_data(window) }; - let ty_ = SelectionType::new(atoms, atoms[Self::DND_TYPE]); - dnd_write.selection = data.ok().map(|data| SelectionReader::new(ty_, data.into())); - if dnd_write.selection.is_some() { - let event = if dnd_write.dragging { - WindowEvent::DragMoved { id: dnd_write.transfer_id(), position: dnd_write.position } - } else { - dnd_write.dragging = true; - WindowEvent::DragEntered { - id: dnd_write.transfer_id(), - position: dnd_write.position, - } + let (serial, transfer_id) = { + let mut dnd = self.target.dnd.write().unwrap(); + let data = unsafe { dnd.read_data(window) }; + let Some(selection_fetch_state) = &mut dnd.last_fetched_selection else { + return; }; + let ty_ = SelectionType::new(atoms, atoms[Self::DND_TYPE]); + let new_value = data + .map(|data| Box::new(SelectionReader::new(ty_, data.into()))) + .map_err(io::Error::other); - app.window_event(&self.target, window_id, event); - } + selection_fetch_state.value = Some(new_value); + + (selection_fetch_state.serial, dnd.transfer_id()) + }; + + app.window_event(&self.target, window_id, WindowEvent::DataTransferResult { + id: transfer_id, + serial, + }); } fn configure_notify(&self, xev: &XConfigureEvent, app: &mut dyn ApplicationHandler) { diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 3e57f58506..84b4b8af7a 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -12,7 +12,7 @@ use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use tracing::{debug, info, warn}; use winit_core::application::ApplicationHandler; use winit_core::cursor::Cursor; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::AsyncRequestSerial; @@ -40,7 +40,7 @@ use crate::atoms::{ _NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE, WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware, }; -use crate::dnd::{Dnd, DndState, Selection}; +use crate::dnd::{Dnd, DndState, Selection, SelectionFetchState}; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, WakeSender, X11Error, xinput_fp1616_to_float, @@ -315,7 +315,7 @@ impl CoreWindow for Window { fn data_transfer( &self, id: DataTransferId, - ) -> Result, UnknownDataTransfer> { + ) -> Result, UnknownDataTransfer> { let Some(dnd) = self.dnd.upgrade() else { return Err(UnknownDataTransfer(id)); }; @@ -380,26 +380,43 @@ impl CoreWindow for Window { &self, id: DataTransferId, type_: &dyn TransferType, - ) -> Result<(), UnknownDataTransfer> { + ) -> Result { let Some(dnd) = self.dnd.upgrade() else { - return Err(UnknownDataTransfer(id)); + return Err(RequestError::NotSupported(NotSupportedError::new( + "Drag-and-drop unavailable", + ))); }; - let dnd = dnd.read().unwrap(); + let mut dnd = dnd.write().unwrap(); if dnd.transfer_id() != id { - return Err(UnknownDataTransfer(id)); + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown data transfer", + ))); } if dnd.source_window.is_none() { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown source window", + ))); + } + + if let Some(state) = &dnd.last_fetched_selection + && state.value.is_some() + { + return Ok(state.serial); } let window = self.0.xwindow; let atoms = self.0.xconn.atoms(); - let type_ = SelectionType::from_dyn(atoms, type_).ok_or(UnknownDataTransfer(id))?; + let type_ = SelectionType::from_dyn(atoms, type_) + .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; + + let new_fetch_state = SelectionFetchState::new(); + let serial = new_fetch_state.serial; + + dnd.last_fetched_selection = Some(new_fetch_state); // This results in the `SelectionNotify` event below unsafe { @@ -407,7 +424,36 @@ impl CoreWindow for Window { dnd.convert_selection(window, self.0.xconn.timestamp(), type_.atom()); } - Ok(()) + Ok(serial) + } + + fn data_transfer_result( + &self, + serial: AsyncRequestSerial, + ) -> Result, RequestError> { + let Some(dnd) = self.dnd.upgrade() else { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Drag-and-drop unavailable", + ))); + }; + + let dnd = dnd.read().unwrap(); + + if dnd.source_window.is_none() { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown source window", + ))); + } + + dnd.last_fetched_selection + .as_ref() + .filter(|state| state.serial == serial) + .and_then(|state| state.value.as_ref()) + // TODO: Actually return the error to the user somehow, maybe through a dummy + // `TypedData` impl? + .and_then(|res| res.as_ref().ok()) + .map(|reader| reader.clone() as _) + .ok_or(RequestError::Ignored) } } diff --git a/winit/examples/application.rs b/winit/examples/application.rs index 2e0ccded35..ad005d23af 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -546,7 +546,7 @@ impl ApplicationHandler for Application { | WindowEvent::KeyboardInput { .. } | WindowEvent::PointerEntered { .. } | WindowEvent::DragEntered { .. } - | WindowEvent::DragMoved { .. } + | WindowEvent::DragPosition { .. } | WindowEvent::DragDropped { .. } | WindowEvent::DataTransferResult { .. } | WindowEvent::Destroyed diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index c8d0332656..7913decb0a 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -1,9 +1,10 @@ use std::error::Error; -use tracing::info; +use tracing::{error, info}; use winit::application::ApplicationHandler; +use winit::data_transfer::TypeHint; use winit::event::WindowEvent; -use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::event_loop::{ActiveEventLoop, AsyncRequestSerial, EventLoop}; use winit::window::{Window, WindowAttributes, WindowId}; #[path = "util/fill.rs"] @@ -21,14 +22,15 @@ fn main() -> Result<(), Box> { } /// Application state and event handling. -#[derive(Debug)] +#[derive(Debug, Default)] struct Application { window: Option>, + last_dnd_fetch: Option<(AsyncRequestSerial, bool)>, } impl Application { fn new() -> Self { - Self { window: None } + Self::default() } } @@ -46,11 +48,80 @@ impl ApplicationHandler for Application { event: WindowEvent, ) { match event { - WindowEvent::DragLeft { .. } - | WindowEvent::DragEntered { .. } - | WindowEvent::DragMoved { .. } - | WindowEvent::DragDropped { .. } => { + WindowEvent::DragLeft { .. } => { info!("{event:?}"); + self.last_dnd_fetch = None; + }, + WindowEvent::DragPosition { .. } => { + info!("{event:?}"); + }, + WindowEvent::DragDropped { .. } => { + info!("{event:?}"); + + if let Some((last_fetch_serial, received)) = &self.last_dnd_fetch + && let Some(window) = self.window.as_ref() + { + if *received { + let mut data = window.data_transfer_result(*last_fetch_serial).unwrap(); + let uris = data.try_as_uris().unwrap(); + info!("{uris:#?}"); + } else { + info!("Never received"); + } + } + + self.last_dnd_fetch = None; + }, + WindowEvent::DataTransferResult { serial, .. } => { + info!("{event:?}"); + + if let Some((last_fetch_serial, received)) = &mut self.last_dnd_fetch + && serial == *last_fetch_serial + { + *received = true; + } + + if let Some(window) = self.window.as_ref() { + let mut data = window.data_transfer_result(serial).unwrap(); + let uris = data.try_as_uris().unwrap(); + info!("{uris:#?}"); + } + }, + WindowEvent::DragEntered { id } => { + info!("{event:?}"); + + let type_ = TypeHint::UriList; + + if let Some(window) = self.window.as_ref() { + let data_transfer = match window.data_transfer(id) { + Ok(dt) => dt, + Err(e) => { + error!("{e}"); + return; + }, + }; + + info!( + "Types: {:#?}", + data_transfer + .available_types() + .into_iter() + .filter_map(|ty| ty.hint()) + .collect::>() + ); + + if !data_transfer.has_type(&type_) { + info!("Cannot drop (cannot interpret input as URI list)"); + return; + } + + window.accept_drag_type(id, &type_).unwrap(); + + self.last_dnd_fetch = + window.fetch_data_transfer(id, &type_).ok().map(|serial| (serial, false)); + } else { + error!("No window!"); + } }, WindowEvent::RedrawRequested => { let window = self.window.as_ref().unwrap(); diff --git a/winit/src/lib.rs b/winit/src/lib.rs index beb5c809ec..bab3fd9900 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -292,7 +292,9 @@ pub use rwh_06 as raw_window_handle; #[cfg(any(doc, doctest, test))] pub mod changelog; pub mod event_loop; -pub use winit_core::{application, cursor, error, event, icon, keyboard, monitor, window}; +pub use winit_core::{ + application, cursor, data_transfer, error, event, icon, keyboard, monitor, window, +}; #[macro_use] mod os_error; mod platform_impl; From 8d3111b15d24ef16053a99fd262407f4484335ab Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 21 May 2026 14:01:12 +0200 Subject: [PATCH 16/87] Fix use of `XDndStatus` --- winit-core/src/window.rs | 9 ++++++--- winit-x11/src/dnd.rs | 7 ++++++- winit-x11/src/event_processor.rs | 15 +++++++++++++++ winit-x11/src/window.rs | 14 +++++++++++++- winit/examples/dnd.rs | 7 +------ 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 4900b939f9..531fb6734f 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1193,7 +1193,8 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { Err(UnknownDataTransfer(id)) } - /// Mark a given data transfer ID as being accepted by the window. + /// Mark a given data transfer ID as being accepted by the window. By default, a drag will be + /// rejected. /// /// This allows the OS/compositor to display the correct UI, indicating that the dragged data /// can be dropped. @@ -1208,7 +1209,8 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { Err(UnknownDataTransfer(id)) } - /// Mark a single type of a given data transfer ID as being accepted by the window. + /// Mark a single type of a given data transfer ID as being accepted by the window. By default, + /// a drag will be rejected. /// /// This allows the OS/compositor to display the correct UI, indicating that the dragged data /// can be dropped. @@ -1224,7 +1226,8 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { self.accept_drag(id) } - /// Mark a given data transfer ID as being rejected by the window. + /// Mark a given data transfer ID as being rejected by the window. This is the default if + /// `accept_drag`/`accept_drag_type` is never called. /// /// This allows the OS/compositor to display the correct UI, indicating that the dragged data /// can _not_ be dropped. diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index a1e005bb22..6de2d0dc7b 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -97,7 +97,7 @@ impl TypedData for SelectionReader { fn try_as_plaintext(&mut self) -> Option { // We don't check that the type of this data is plaintext, as other types (e.g. HTML, URI // list) are valid to read as plaintext - str::from_utf8(&self.data).ok().map(Into::into) + percent_decode(&self.data).decode_utf8().ok().map(Into::into) } fn try_as_uris(&mut self) -> Option> { @@ -132,6 +132,10 @@ impl SelectionFetchState { pub struct Dnd { xconn: Arc, transfer_id: DataTransferId, + /// Whether the drag operation is accepted (or `None` if the user never indicated that it's + /// accepted or rejected) + // Populated by `Window::accept_drag`/`Window::reject_drag`. + pub accepted: Option, // Populated by XdndEnter event handler pub version: Option, pub type_infos: Option>, @@ -233,6 +237,7 @@ impl Dnd { Dnd { xconn, transfer_id, + accepted: None, version: None, type_infos: None, source_window: None, diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 3125f74a7c..5b7f9cddb4 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -427,6 +427,8 @@ impl EventProcessor { } if xev.message_type == atoms[XdndEnter] as c_ulong { + // Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event` + // never contending the lock. let transfer_id = { let mut dnd = self.target.dnd.write().unwrap(); // We only reset when a new drag-and-drop enters, since that means that the user can @@ -491,6 +493,8 @@ impl EventProcessor { .translate_coords(self.target.root, window, x, y) .expect("Failed to translate window coordinates"); + // Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event` + // never contending the lock. let transfer_id = { let mut dnd = self.target.dnd.write().unwrap(); // By our own state flow, `version` should never be `None` at this point. @@ -507,6 +511,17 @@ impl EventProcessor { // Log this timestamp. self.target.xconn.set_timestamp(time); + let status = if dnd.accepted.unwrap_or_default() { + DndState::Accepted + } else { + DndState::Rejected + }; + + unsafe { + dnd.send_status(window, source_window, status) + .expect("Failed to send `XdndStatus` message."); + } + dnd.transfer_id() }; diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 84b4b8af7a..2760d6a70e 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -337,6 +337,12 @@ impl CoreWindow for Window { return Err(UnknownDataTransfer(id)); } + if dnd.accepted == Some(false) { + return Ok(()); + } + + dnd.accepted = Some(false); + let Some(source_window) = dnd.source_window else { // TODO: Should have "other error" since this isn't an unknown data transfer. return Err(UnknownDataTransfer(id)); @@ -357,11 +363,17 @@ impl CoreWindow for Window { return Err(UnknownDataTransfer(id)); }; - let dnd = dnd.read().unwrap(); + let mut dnd = dnd.write().unwrap(); if dnd.transfer_id() != id { return Err(UnknownDataTransfer(id)); } + if dnd.accepted == Some(true) { + return Ok(()); + } + + dnd.accepted = Some(true); + let Some(source_window) = dnd.source_window else { // TODO: Should have "other error" since this isn't an unknown data transfer. return Err(UnknownDataTransfer(id)); diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 7913decb0a..c276312243 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -80,12 +80,6 @@ impl ApplicationHandler for Application { { *received = true; } - - if let Some(window) = self.window.as_ref() { - let mut data = window.data_transfer_result(serial).unwrap(); - let uris = data.try_as_uris().unwrap(); - info!("{uris:#?}"); - } }, WindowEvent::DragEntered { id } => { info!("{event:?}"); @@ -112,6 +106,7 @@ impl ApplicationHandler for Application { if !data_transfer.has_type(&type_) { info!("Cannot drop (cannot interpret input as URI list)"); + window.reject_drag(id).unwrap(); return; } From e867501d1ebb27c4567acabe49418baa26fc3882 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 21 May 2026 14:02:47 +0200 Subject: [PATCH 17/87] Change comment --- winit-x11/src/event_processor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 5b7f9cddb4..33a97a3f05 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -431,8 +431,8 @@ impl EventProcessor { // never contending the lock. let transfer_id = { let mut dnd = self.target.dnd.write().unwrap(); - // We only reset when a new drag-and-drop enters, since that means that the user can - // read the drag info in the window event handler. + // We only reset when a new drag-and-drop enters, to maximize the amount of time + // that the drag data can be accessed. dnd.reset(); let source_window = xev.data.get_long(0) as xproto::Window; From 4dc8f6f4e4efff28e55c437695483671f606cb57 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 21 May 2026 14:53:59 +0200 Subject: [PATCH 18/87] Fix conversion from type hint to "real" type --- winit-x11/src/dnd.rs | 23 ++++++++--------------- winit-x11/src/event_processor.rs | 4 ++-- winit-x11/src/window.rs | 7 ++++--- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 6de2d0dc7b..49ff8c6b52 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -138,7 +138,7 @@ pub struct Dnd { pub accepted: Option, // Populated by XdndEnter event handler pub version: Option, - pub type_infos: Option>, + pub types: Option>, // Populated by XdndPosition event handler pub source_window: Option, // Populated by `fetch_data_transfer` @@ -163,17 +163,6 @@ pub struct SelectionType { } impl SelectionType { - pub(crate) fn from_dyn(atoms: &Atoms, type_: &dyn TransferType) -> Option { - type_.cast_ref().cloned().or_else(|| { - let hint = type_.hint()?; - - match hint { - TypeHint::UriList => Some(Self { hint: Some(hint), atom: atoms[TextUriList] }), - _ => None, - } - }) - } - pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self { let hint = if atom == atoms[TextUriList] { Some(TypeHint::UriList) } else { None }; @@ -202,7 +191,7 @@ impl DataTransfer for Selection { self.dnd .read() .unwrap() - .type_infos + .types .as_ref() .into_iter() .flat_map(|types| types.iter().map(|val| Box::new(val.clone()) as _)) @@ -212,7 +201,7 @@ impl DataTransfer for Selection { fn has_type(&self, type_: &dyn TransferType) -> bool { let dnd = self.dnd.read().unwrap(); - let Some(types) = dnd.type_infos.as_ref() else { + let Some(types) = dnd.types.as_ref() else { return false; }; @@ -233,13 +222,17 @@ impl Dnd { Self::with_id(xconn, DataTransferId::from_raw(0)) } + pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> { + self.types.as_ref()?.iter().find(|haystack| haystack.hint() == Some(hint)) + } + fn with_id(xconn: Arc, transfer_id: DataTransferId) -> Self { Dnd { xconn, transfer_id, accepted: None, version: None, - type_infos: None, + types: None, source_window: None, last_fetched_selection: None, } diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 33a97a3f05..e7f1b5c15f 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -452,9 +452,9 @@ impl EventProcessor { .map(|ty_atom| SelectionType::new(atoms, ty_atom).into()) .into_iter() .collect(); - dnd.type_infos = Some(type_list); + dnd.types = Some(type_list); } else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } { - dnd.type_infos = Some( + dnd.types = Some( more_types .into_iter() .map(|ty_atom| SelectionType::new(atoms, ty_atom).into()) diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 2760d6a70e..419a1533ff 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -420,9 +420,10 @@ impl CoreWindow for Window { let window = self.0.xwindow; - let atoms = self.0.xconn.atoms(); - - let type_ = SelectionType::from_dyn(atoms, type_) + let type_ = type_ + .cast_ref::() + .or_else(|| dnd.find_type_by_hint(type_.hint()?)) + .cloned() .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; let new_fetch_state = SelectionFetchState::new(); From 1fec92c32a123ad2db6ece65f111e39643f98b78 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 21 May 2026 15:31:10 +0200 Subject: [PATCH 19/87] Fill out type hints for X11 --- winit-x11/src/atoms.rs | 29 ++++++++++++++ winit-x11/src/dnd.rs | 57 +++++++++++++++++++++++---- winit-x11/src/event_loop.rs | 5 +-- winit-x11/src/event_processor.rs | 16 +++----- winit-x11/src/util/window_property.rs | 2 + winit-x11/src/window.rs | 11 +----- 6 files changed, 89 insertions(+), 31 deletions(-) diff --git a/winit-x11/src/atoms.rs b/winit-x11/src/atoms.rs index e5ac78f4dd..398ab61080 100644 --- a/winit-x11/src/atoms.rs +++ b/winit-x11/src/atoms.rs @@ -34,7 +34,10 @@ macro_rules! atom_manager { atom_manager! { // General Use Atoms CARD32, + STRING, UTF8_STRING, + TARGETS, + SAVE_TARGETS, WM_CHANGE_STATE, WM_CLIENT_MACHINE, WM_DELETE_WINDOW, @@ -91,7 +94,33 @@ atom_manager! { XdndSelection, XdndFinished, XdndTypeList, + + // MIME types for reading selections TextUriList: b"text/uri-list", + TextPlain: b"text/plain", + TextHtml: b"text/html", + ApplicationRtf: b"application/rtf", + AudioAac: b"audio/aac", + AudioAiff: b"audio/aiff", + AudioFlac: b"audio/flac", + AudioWav: b"audio/wav", + AudioWave: b"audio/wave", + AudioXWav: b"audio/x-wav", + AudioVndWav: b"audio/vnd.wav", + AudioVndWave: b"audio/vnd.wave", + AudioMpeg: b"audio/mpeg", + AudioOgg: b"audio/ogg", + ImageBmp: b"image/bmp", + ImageGif: b"image/gif", + ImageJpeg: b"image/jpeg", + ImagePjpeg: b"image/pjpeg", + ImagePng: b"image/png", + ImageSvg: b"image/svg+xml", + ImageTiff: b"image/tiff", + ImageWebp: b"image/webp", + ImageXIcon: b"image/x-icon", + ImageRaw: b"image/x-panasonic-raw", + None: b"None", // Miscellaneous Atoms diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 49ff8c6b52..2b9859a893 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -10,9 +10,7 @@ use winit_core::event_loop::AsyncRequestSerial; use x11rb::protocol::xproto::{self, ConnectionExt}; use crate::atoms::AtomName::None as DndNone; -use crate::atoms::{ - Atoms, TextUriList, XdndActionPrivate, XdndFinished, XdndSelection, XdndStatus, XdndTypeList, -}; +use crate::atoms::*; use crate::event_loop::{CookieResultExt, X11Error}; use crate::util; use crate::xdisplay::XConnection; @@ -118,13 +116,14 @@ impl TypedData for SelectionReader { #[derive(Debug)] pub struct SelectionFetchState { pub serial: AsyncRequestSerial, + pub type_: xproto::Atom, // Populated by SelectionNotify event handler pub value: Option>>, } impl SelectionFetchState { - pub fn new() -> Self { - Self { serial: AsyncRequestSerial::get(), value: None } + pub fn new(type_: xproto::Atom) -> Self { + Self { serial: AsyncRequestSerial::get(), type_, value: None } } } @@ -164,7 +163,44 @@ pub struct SelectionType { impl SelectionType { pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self { - let hint = if atom == atoms[TextUriList] { Some(TypeHint::UriList) } else { None }; + let atom_to_hint = [ + // Files + (atoms[TextUriList], TypeHint::UriList), + (atoms[TARGETS], TypeHint::UriList), + (atoms[SAVE_TARGETS], TypeHint::UriList), + // Plaintext + (atoms[STRING], TypeHint::Plaintext), + (atoms[UTF8_STRING], TypeHint::Plaintext), + (atoms[TextPlain], TypeHint::Plaintext), + // HTML + (atoms[TextHtml], TypeHint::Html), + // RTF + (atoms[ApplicationRtf], TypeHint::Rtf), + // Audio + (atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }), + (atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }), + (atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }), + (atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }), + (atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }), + // Image + (atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }), + (atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }), + (atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }), + (atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }), + (atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }), + (atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }), + (atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }), + (atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }), + (atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }), + (atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }), + ]; + let hint = + atom_to_hint.iter().find_map(|(haystack, hint)| (*haystack == atom).then_some(*hint)); Self { hint, atom } } @@ -324,8 +360,13 @@ impl Dnd { pub unsafe fn read_data( &self, window: xproto::Window, - ) -> Result, util::GetPropertyError> { + ) -> Result<(xproto::Atom, Vec), util::GetPropertyError> { let atoms = self.xconn.atoms(); - self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList]) + let type_ = self + .last_fetched_selection + .as_ref() + .map(|state| state.type_) + .ok_or(util::GetPropertyError::Unknown)?; + Ok((type_, self.xconn.get_property(window, atoms[XdndSelection], type_)?)) } } diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index b86a292b22..170b8c97a9 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -36,10 +36,7 @@ use x11rb::protocol::{xkb, xproto}; use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; -use crate::atoms::{ - _NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms, - WM_DELETE_WINDOW, -}; +use crate::atoms::*; use crate::dnd::Dnd; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index e7f1b5c15f..24c8a28347 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -30,10 +30,7 @@ use x11rb::protocol::xproto::{self, ConnectionExt as _, ModMask}; use x11rb::x11_utils::{ExtensionInformation, Serialize}; use xkbcommon_dl::xkb_mod_mask_t; -use crate::atoms::{ - _XSETTINGS_SETTINGS, AtomName, TextUriList, XdndDrop, XdndEnter, XdndLeave, XdndPosition, - XdndSelection, -}; +use crate::atoms::*; use crate::dnd::{DndState, SelectionReader, SelectionType}; use crate::event_loop::{ ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType, @@ -81,8 +78,6 @@ pub struct EventProcessor { } impl EventProcessor { - const DND_TYPE: AtomName = TextUriList; - pub(crate) fn process_event(&mut self, xev: &mut XEvent, app: &mut dyn ApplicationHandler) { self.process_xevent(xev, app); @@ -582,13 +577,14 @@ impl EventProcessor { // This is where we receive data from drag and drop let (serial, transfer_id) = { let mut dnd = self.target.dnd.write().unwrap(); - let data = unsafe { dnd.read_data(window) }; + let result = unsafe { dnd.read_data(window) }; let Some(selection_fetch_state) = &mut dnd.last_fetched_selection else { return; }; - let ty_ = SelectionType::new(atoms, atoms[Self::DND_TYPE]); - let new_value = data - .map(|data| Box::new(SelectionReader::new(ty_, data.into()))) + let new_value = result + .map(|(ty, data)| { + Box::new(SelectionReader::new(SelectionType::new(atoms, ty), data.into())) + }) .map_err(io::Error::other); selection_fetch_state.value = Some(new_value); diff --git a/winit-x11/src/util/window_property.rs b/winit-x11/src/util/window_property.rs index 7329e7132f..0f89a0d9db 100644 --- a/winit-x11/src/util/window_property.rs +++ b/winit-x11/src/util/window_property.rs @@ -17,6 +17,7 @@ pub enum GetPropertyError { X11rbError(Arc), TypeMismatch(xproto::Atom), FormatMismatch(c_int), + Unknown, } impl GetPropertyError { @@ -41,6 +42,7 @@ impl fmt::Display for GetPropertyError { GetPropertyError::X11rbError(err) => err.fmt(f), GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"), GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"), + GetPropertyError::Unknown => write!(f, "internal error"), } } } diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 419a1533ff..2eb512ba55 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -32,14 +32,7 @@ use x11rb::protocol::sync::{ConnectionExt as _, Int64}; use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle}; use x11rb::protocol::{randr, xinput}; -use crate::atoms::{ - _GTK_THEME_VARIANT, _NET_ACTIVE_WINDOW, _NET_WM_ICON, _NET_WM_MOVERESIZE, _NET_WM_NAME, - _NET_WM_PID, _NET_WM_PING, _NET_WM_STATE, _NET_WM_STATE_ABOVE, _NET_WM_STATE_BELOW, - _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_HIDDEN, _NET_WM_STATE_MAXIMIZED_HORZ, - _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_SYNC_REQUEST, _NET_WM_SYNC_REQUEST_COUNTER, - _NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE, - WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware, -}; +use crate::atoms::*; use crate::dnd::{Dnd, DndState, Selection, SelectionFetchState}; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, @@ -426,7 +419,7 @@ impl CoreWindow for Window { .cloned() .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; - let new_fetch_state = SelectionFetchState::new(); + let new_fetch_state = SelectionFetchState::new(type_.atom()); let serial = new_fetch_state.serial; dnd.last_fetched_selection = Some(new_fetch_state); From 61ab9c8b2fd52f5ada6a2cceb976e8337fc9b01e Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 21 May 2026 16:42:40 +0200 Subject: [PATCH 20/87] Fix transferring text and add an example --- winit-core/src/data_transfer.rs | 2 +- winit-core/src/event_loop/mod.rs | 105 ++++++++++++++++++- winit-core/src/window.rs | 104 +------------------ winit-x11/src/atoms.rs | 2 + winit-x11/src/dnd.rs | 50 +++++++-- winit-x11/src/event_loop.rs | 162 ++++++++++++++++++++++++++++- winit-x11/src/event_processor.rs | 8 ++ winit-x11/src/window.rs | 173 ++----------------------------- winit/examples/dnd.rs | 107 +++++++++++-------- 9 files changed, 386 insertions(+), 327 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 2457bc8a46..7f614f5930 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -141,7 +141,7 @@ pub trait TypedData: AsAny + Send + Sync + fmt::Debug { /// Read this value as a plain text string. /// /// If this value is not readable as a string, return `None`. - fn try_as_plaintext(&mut self) -> Option; + fn try_as_string(&mut self) -> Option; } impl_dyn_casting!(TypedData); diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index 3ef3ed5f59..629cc96b40 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -3,7 +3,7 @@ pub mod pump_events; pub mod register; pub mod run_on_demand; -use std::fmt::{self, Debug}; +use std::fmt::{self, Debug, Display}; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; @@ -13,10 +13,24 @@ use rwh_06::{DisplayHandle, HandleError, HasDisplayHandle}; use crate::Instant; use crate::as_any::AsAny; use crate::cursor::{CustomCursor, CustomCursorSource}; -use crate::error::RequestError; +use crate::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; +use crate::error::{NotSupportedError, RequestError}; use crate::monitor::MonitorHandle; use crate::window::{Theme, Window, WindowAttributes}; +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Creates an [`EventLoopProxy`] that can be used to dispatch user events /// to the main event loop, possibly from another thread. @@ -114,6 +128,93 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Get the raw-window-handle handle. fn rwh_06_handle(&self) -> &dyn HasDisplayHandle; + + /// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer). + /// + /// This may be called multiple times on the same [`DataTransferId`] with different types. + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let _ = id; + let _ = type_; + Err(RequestError::NotSupported(NotSupportedError::new( + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ + this platform", + ))) + } + + /// Get the resolved data for a data transfer. + /// + /// Requires first calling [`fetch_data_transfer`](ActiveEventLoop::fetch_data_transfer) and + /// waiting for the corresponding `DataTransferResult` event. + fn data_transfer_result( + &self, + serial: AsyncRequestSerial, + ) -> Result, RequestError> { + let _ = serial; + Err(RequestError::NotSupported(NotSupportedError::new( + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ + this platform", + ))) + } + + /// Get a [data transfer](DataTransfer) by its ID. + /// + /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will + /// return an error. + fn data_transfer( + &self, + id: DataTransferId, + ) -> Result, UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } + + /// Mark a given data transfer ID as being accepted by the window. By default, a drag will be + /// rejected. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. + /// + /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying + /// one or more accepted types. For platforms that require specifying a type, `accept_drag` will + /// mark all available types as accepted. + /// + /// For the most reliable cross-platform behaviour, + /// [`accept_drag_type`](Window::accept_drag_type) is preferred. + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } + + /// Mark a single type of a given data transfer ID as being accepted by the window. By default, + /// a drag will be rejected. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. + /// + /// If the window may accept more than one of the advertised types, this method should be + /// called multiple times, once for each of the accepted types. + fn accept_drag_type( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result<(), UnknownDataTransfer> { + let _ = type_; + self.accept_drag(id) + } + + /// Mark a given data transfer ID as being rejected by the window. This is the default if + /// `accept_drag`/`accept_drag_type` is never called. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can _not_ be dropped. + /// + /// This will ensure that the OS/compositor indicates to the user that dropping the dragged data + /// is not possible. + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } } impl HasDisplayHandle for dyn ActiveEventLoop + '_ { diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 531fb6734f..35e604ed2f 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1,5 +1,5 @@ //! The [`Window`] trait and associated types. -use std::fmt::{self, Display}; +use std::fmt; use bitflags::bitflags; use cursor_icon::CursorIcon; @@ -11,9 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::as_any::AsAny; use crate::cursor::Cursor; -use crate::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; -use crate::error::{NotSupportedError, RequestError}; -use crate::event_loop::AsyncRequestSerial; +use crate::error::RequestError; use crate::icon::Icon; use crate::monitor::{Fullscreen, MonitorHandle}; @@ -460,17 +458,6 @@ pub trait PlatformWindowAttributes: AsAny + std::fmt::Debug + Send + Sync { impl_dyn_casting!(PlatformWindowAttributes); -/// An operation was attempted on a data transfer ID, but that ID was invalid. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct UnknownDataTransfer(pub DataTransferId); - -impl Display for UnknownDataTransfer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let id = self.0.into_raw(); - write!(f, "Unknown data transfer with ID {id}") - } -} - /// Represents a window. /// /// The window is closed when dropped. @@ -1182,93 +1169,6 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { } } - /// Get a [data transfer](DataTransfer) by its ID. - /// - /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will - /// return an error. - fn data_transfer( - &self, - id: DataTransferId, - ) -> Result, UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } - - /// Mark a given data transfer ID as being accepted by the window. By default, a drag will be - /// rejected. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can be dropped. - /// - /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying - /// one or more accepted types. For platforms that require specifying a type, `accept_drag` will - /// mark all available types as accepted. - /// - /// For the most reliable cross-platform behaviour, - /// [`accept_drag_type`](Window::accept_drag_type) is preferred. - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } - - /// Mark a single type of a given data transfer ID as being accepted by the window. By default, - /// a drag will be rejected. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can be dropped. - /// - /// If the window may accept more than one of the advertised types, this method should be - /// called multiple times, once for each of the accepted types. - fn accept_drag_type( - &self, - id: DataTransferId, - type_: &dyn TransferType, - ) -> Result<(), UnknownDataTransfer> { - let _ = type_; - self.accept_drag(id) - } - - /// Mark a given data transfer ID as being rejected by the window. This is the default if - /// `accept_drag`/`accept_drag_type` is never called. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can _not_ be dropped. - /// - /// This will ensure that the OS/compositor indicates to the user that dropping the dragged data - /// is not possible. - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } - - /// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer). - /// - /// This may be called multiple times on the same [`DataTransferId`] with different types. - fn fetch_data_transfer( - &self, - id: DataTransferId, - type_: &dyn TransferType, - ) -> Result { - let _ = id; - let _ = type_; - Err(RequestError::NotSupported(NotSupportedError::new( - "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ - this platform", - ))) - } - - /// Get the resolved data for a data transfer. - /// - /// Requires first calling [`fetch_data_transfer`](ActiveEventLoop::fetch_data_transfer) and - /// waiting for the corresponding `DataTransferResult` event. - fn data_transfer_result( - &self, - serial: AsyncRequestSerial, - ) -> Result, RequestError> { - let _ = serial; - Err(RequestError::NotSupported(NotSupportedError::new( - "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ - this platform", - ))) - } - /// Atomically apply request to IME. /// /// For details consult [`ImeRequest`] and [`ImeCapabilities`]. diff --git a/winit-x11/src/atoms.rs b/winit-x11/src/atoms.rs index 398ab61080..52972bf4da 100644 --- a/winit-x11/src/atoms.rs +++ b/winit-x11/src/atoms.rs @@ -98,7 +98,9 @@ atom_manager! { // MIME types for reading selections TextUriList: b"text/uri-list", TextPlain: b"text/plain", + TextPlainCharsetUtf8: b"text/plain; charset=utf-8", TextHtml: b"text/html", + TextHtmlCharsetUtf8: b"text/html; charset=utf-8", ApplicationRtf: b"application/rtf", AudioAac: b"audio/aac", AudioAiff: b"audio/aiff", diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 2b9859a893..e62a65c795 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -92,10 +92,41 @@ impl TypedData for SelectionReader { &self.type_ } - fn try_as_plaintext(&mut self) -> Option { - // We don't check that the type of this data is plaintext, as other types (e.g. HTML, URI - // list) are valid to read as plaintext - percent_decode(&self.data).decode_utf8().ok().map(Into::into) + fn try_as_string(&mut self) -> Option { + fn decode_utf16_bytes(bytes: &[u8]) -> Option { + let utf16 = bytes + .chunks_exact(2) + .into_iter() + .map(|chunk| { + let bytes: &[u8; 2] = chunk.try_into().unwrap(); + u16::from_ne_bytes(*bytes) + }) + .collect::>(); + String::from_utf16(&utf16).ok() + } + + match self.type_.hint() { + Some(TypeHint::Plaintext) | Some(TypeHint::Html) => { + // Bad way to detect UTF-16 - some applications (confirmed to at least happen with + // Firefox) don't emit a BOM when passing HTML, so we need to check: + // A) Does the string contain a null + // B) Can the string be decoded as UTF-8 + if self.data.contains(&0) { + decode_utf16_bytes(&self.data) + // Even if we guess that it's utf-16, we'll still try utf-8 just in case + .or_else(|| str::from_utf8(&self.data).ok().map(|str| str.to_owned())) + } else { + str::from_utf8(&self.data) + .map(|str| str.to_owned()) + .ok() + .or_else(|| decode_utf16_bytes(&self.data)) + } + }, + Some(TypeHint::UriList) => { + percent_decode(&self.data).decode_utf8().ok().map(Into::into) + }, + _ => None, + } } fn try_as_uris(&mut self) -> Option> { @@ -104,7 +135,7 @@ impl TypedData for SelectionReader { } Some( - self.try_as_plaintext()? + self.try_as_string()? .split(|c| c == '\n' || c == '\r') .filter(|s| !s.is_empty()) .map(Into::into) @@ -138,8 +169,10 @@ pub struct Dnd { // Populated by XdndEnter event handler pub version: Option, pub types: Option>, - // Populated by XdndPosition event handler + // Populated by Xdnd* event handlers pub source_window: Option, + // Populated by Xdnd* event handlers + pub target_window: Option, // Populated by `fetch_data_transfer` pub last_fetched_selection: Option, } @@ -172,8 +205,10 @@ impl SelectionType { (atoms[STRING], TypeHint::Plaintext), (atoms[UTF8_STRING], TypeHint::Plaintext), (atoms[TextPlain], TypeHint::Plaintext), + (atoms[TextPlainCharsetUtf8], TypeHint::Plaintext), // HTML (atoms[TextHtml], TypeHint::Html), + (atoms[TextHtmlCharsetUtf8], TypeHint::Html), // RTF (atoms[ApplicationRtf], TypeHint::Rtf), // Audio @@ -270,6 +305,7 @@ impl Dnd { version: None, types: None, source_window: None, + target_window: None, last_fetched_selection: None, } } @@ -353,6 +389,8 @@ impl Dnd { let atoms = self.xconn.atoms(); self.xconn .xcb_connection() + // TODO: We store the converted selection back to `XdndSelection`. We should store to + // some new place so that `XdndSelection` remains untouched. .convert_selection(window, atoms[XdndSelection], new_type, atoms[XdndSelection], time) .expect_then_ignore_error("Failed to send XdndSelection event") } diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 170b8c97a9..538c63a202 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -19,13 +19,14 @@ use tracing::warn; use winit_common::xkb::Context; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; -use winit_core::error::{EventLoopError, RequestError}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; +use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::event::{DeviceId, StartCause, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider, - OwnedDisplayHandle as CoreOwnedDisplayHandle, + OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::{Theme, Window as CoreWindow, WindowAttributes, WindowId}; @@ -37,13 +38,13 @@ use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; use crate::atoms::*; -use crate::dnd::Dnd; +use crate::dnd::{Dnd, DndState, SelectionFetchState}; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; use crate::util::{self, CustomCursor}; use crate::window::{UnownedWindow, Window}; use crate::xdisplay::{XConnection, XError, XNotSupported}; -use crate::{XlibErrorHook, ffi, xsettings}; +use crate::{Selection, SelectionType, XlibErrorHook, ffi, xsettings}; // Xinput constants not defined in x11rb pub(crate) const ALL_DEVICES: u16 = 0; @@ -760,6 +761,157 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn data_transfer( + &self, + id: DataTransferId, + ) -> Result, UnknownDataTransfer> { + if self.dnd.read().unwrap().transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + Ok(Box::new(Selection::new(self.dnd.clone()))) + } + + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let mut dnd = self.dnd.write().unwrap(); + if dnd.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + if dnd.accepted == Some(false) { + return Ok(()); + } + + dnd.accepted = Some(false); + + let Some(source_window) = dnd.source_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + let Some(window) = dnd.target_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + unsafe { + dnd.send_status(window, source_window, DndState::Rejected) + .expect("Failed to send `XdndStatus` message."); + } + dnd.reset(); + + Ok(()) + } + + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let mut dnd = self.dnd.write().unwrap(); + if dnd.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + if dnd.accepted == Some(true) { + return Ok(()); + } + + dnd.accepted = Some(true); + + let Some(source_window) = dnd.source_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + let Some(window) = dnd.target_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + unsafe { + dnd.send_status(window, source_window, DndState::Accepted) + .expect("Failed to send `XdndStatus` message."); + } + + Ok(()) + } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let mut dnd = self.dnd.write().unwrap(); + if dnd.transfer_id() != id { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown data transfer", + ))); + } + + if dnd.source_window.is_none() { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown source window", + ))); + } + + if let Some(state) = &dnd.last_fetched_selection + && state.value.is_some() + { + return Ok(state.serial); + } + + let Some(window) = dnd.target_window else { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown target window", + ))); + }; + + let type_ = type_ + .cast_ref::() + .or_else(|| dnd.find_type_by_hint(type_.hint()?)) + .cloned() + .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; + + let new_fetch_state = SelectionFetchState::new(type_.atom()); + let serial = new_fetch_state.serial; + + dnd.last_fetched_selection = Some(new_fetch_state); + + // This results in the `SelectionNotify` event below + unsafe { + // TODO: Handle this better + dnd.convert_selection(window, self.xconn.timestamp(), type_.atom()); + } + + Ok(serial) + } + + fn data_transfer_result( + &self, + serial: AsyncRequestSerial, + ) -> Result, RequestError> { + let dnd = self.dnd.read().unwrap(); + + if dnd.source_window.is_none() { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown source window", + ))); + } + + if dnd.target_window.is_none() { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown target window", + ))); + } + + dnd.last_fetched_selection + .as_ref() + .filter(|state| state.serial == serial) + .and_then(|state| state.value.as_ref()) + // TODO: Actually return the error to the user somehow, maybe through a dummy + // `TypedData` impl? + .and_then(|res| res.as_ref().ok()) + .map(|reader| reader.clone() as _) + .ok_or(RequestError::Ignored) + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 24c8a28347..7dd1af9039 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex}; use std::{io, slice}; use dpi::{PhysicalPosition, PhysicalSize}; +use tracing::warn; use winit_common::xkb::{self, Context, XkbState}; use winit_core::application::ApplicationHandler; use winit_core::event::{ @@ -436,6 +437,7 @@ impl EventProcessor { let version = flags >> 24; dnd.version = Some(version); dnd.source_window = Some(source_window); + dnd.target_window = Some(window); let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; if !has_more_types { @@ -495,7 +497,13 @@ impl EventProcessor { // By our own state flow, `version` should never be `None` at this point. let version = dnd.version.unwrap_or(5); + if dnd.target_window != Some(window) { + warn!("Received `XdndPosition` without `XdndEnter`"); + dnd.target_window = Some(window); + } + dnd.source_window = Some(source_window); + let time = if version == 0 { // In version 0, time isn't specified x11rb::CURRENT_TIME diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 2eb512ba55..0277732110 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -5,14 +5,13 @@ use std::num::NonZeroU32; use std::ops::Deref; use std::os::raw::*; use std::path::Path; -use std::sync::{Arc, Mutex, MutexGuard, RwLock, Weak}; +use std::sync::{Arc, Mutex, MutexGuard}; use std::{cmp, env}; use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use tracing::{debug, info, warn}; use winit_core::application::ApplicationHandler; use winit_core::cursor::Cursor; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::AsyncRequestSerial; @@ -22,8 +21,8 @@ use winit_core::monitor::{ }; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest as CoreImeRequest, ImeRequestError, - ResizeDirection, Theme, UnknownDataTransfer, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, + WindowButtons, WindowId, WindowLevel, }; use x11rb::connection::{Connection, RequestConnection}; use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; @@ -33,7 +32,6 @@ use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle} use x11rb::protocol::{randr, xinput}; use crate::atoms::*; -use crate::dnd::{Dnd, DndState, Selection, SelectionFetchState}; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, WakeSender, X11Error, xinput_fp1616_to_float, @@ -42,7 +40,7 @@ use crate::ime::{ImeRequest, ImeSender}; use crate::monitor::MonitorHandle as X11MonitorHandle; use crate::util::{self, CustomCursor, SelectedCursor, rgba_to_cardinals}; use crate::xdisplay::XConnection; -use crate::{SelectionType, WindowAttributesX11, WindowType, ffi}; +use crate::{WindowAttributesX11, WindowType, ffi}; #[derive(Debug)] pub struct Window(Arc); @@ -304,163 +302,6 @@ impl CoreWindow for Window { fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { self } - - fn data_transfer( - &self, - id: DataTransferId, - ) -> Result, UnknownDataTransfer> { - let Some(dnd) = self.dnd.upgrade() else { - return Err(UnknownDataTransfer(id)); - }; - - if dnd.read().unwrap().transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - Ok(Box::new(Selection::new(dnd))) - } - - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let Some(dnd) = self.dnd.upgrade() else { - return Err(UnknownDataTransfer(id)); - }; - - let mut dnd = dnd.write().unwrap(); - if dnd.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - if dnd.accepted == Some(false) { - return Ok(()); - } - - dnd.accepted = Some(false); - - let Some(source_window) = dnd.source_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - let window = self.0.xwindow; - - unsafe { - dnd.send_status(window, source_window, DndState::Rejected) - .expect("Failed to send `XdndStatus` message."); - } - dnd.reset(); - - Ok(()) - } - - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let Some(dnd) = self.dnd.upgrade() else { - return Err(UnknownDataTransfer(id)); - }; - - let mut dnd = dnd.write().unwrap(); - if dnd.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - if dnd.accepted == Some(true) { - return Ok(()); - } - - dnd.accepted = Some(true); - - let Some(source_window) = dnd.source_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - let window = self.0.xwindow; - - unsafe { - dnd.send_status(window, source_window, DndState::Accepted) - .expect("Failed to send `XdndStatus` message."); - } - - Ok(()) - } - - fn fetch_data_transfer( - &self, - id: DataTransferId, - type_: &dyn TransferType, - ) -> Result { - let Some(dnd) = self.dnd.upgrade() else { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Drag-and-drop unavailable", - ))); - }; - - let mut dnd = dnd.write().unwrap(); - if dnd.transfer_id() != id { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Unknown data transfer", - ))); - } - - if dnd.source_window.is_none() { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Unknown source window", - ))); - } - - if let Some(state) = &dnd.last_fetched_selection - && state.value.is_some() - { - return Ok(state.serial); - } - - let window = self.0.xwindow; - - let type_ = type_ - .cast_ref::() - .or_else(|| dnd.find_type_by_hint(type_.hint()?)) - .cloned() - .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; - - let new_fetch_state = SelectionFetchState::new(type_.atom()); - let serial = new_fetch_state.serial; - - dnd.last_fetched_selection = Some(new_fetch_state); - - // This results in the `SelectionNotify` event below - unsafe { - // TODO: Handle this better - dnd.convert_selection(window, self.0.xconn.timestamp(), type_.atom()); - } - - Ok(serial) - } - - fn data_transfer_result( - &self, - serial: AsyncRequestSerial, - ) -> Result, RequestError> { - let Some(dnd) = self.dnd.upgrade() else { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Drag-and-drop unavailable", - ))); - }; - - let dnd = dnd.read().unwrap(); - - if dnd.source_window.is_none() { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Unknown source window", - ))); - } - - dnd.last_fetched_selection - .as_ref() - .filter(|state| state.serial == serial) - .and_then(|state| state.value.as_ref()) - // TODO: Actually return the error to the user somehow, maybe through a dummy - // `TypedData` impl? - .and_then(|res| res.as_ref().ok()) - .map(|reader| reader.clone() as _) - .ok_or(RequestError::Ignored) - } } impl rwh_06::HasDisplayHandle for Window { @@ -572,11 +413,10 @@ unsafe impl Sync for UnownedWindow {} #[derive(Debug)] pub struct UnownedWindow { pub(crate) xconn: Arc, // never changes - dnd: Weak>, - xwindow: xproto::Window, // never changes + xwindow: xproto::Window, // never changes #[allow(dead_code)] visual: u32, // never changes - root: xproto::Window, // never changes + root: xproto::Window, // never changes #[allow(dead_code)] screen_id: i32, // never changes sync_counter_id: Option, // never changes @@ -802,7 +642,6 @@ impl UnownedWindow { let mut window = UnownedWindow { xconn: Arc::clone(xconn), xwindow: xwindow as xproto::Window, - dnd: Arc::downgrade(&event_loop.dnd), visual, root, screen_id, diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index c276312243..aa1770fd99 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -21,11 +21,18 @@ fn main() -> Result<(), Box> { Ok(event_loop.run_app(app)?) } +#[derive(Debug)] +struct FetchState { + serial: AsyncRequestSerial, + type_: TypeHint, + received: bool, +} + /// Application state and event handling. #[derive(Debug, Default)] struct Application { window: Option>, - last_dnd_fetch: Option<(AsyncRequestSerial, bool)>, + last_dnd_fetch: Option, } impl Application { @@ -37,7 +44,7 @@ impl Application { impl ApplicationHandler for Application { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { let window_attributes = - WindowAttributes::default().with_title("Drag and drop files on me!"); + WindowAttributes::default().with_title("Drag and drop files, text or HTML onto me!"); self.window = Some(event_loop.create_window(window_attributes).unwrap()); } @@ -58,13 +65,23 @@ impl ApplicationHandler for Application { WindowEvent::DragDropped { .. } => { info!("{event:?}"); - if let Some((last_fetch_serial, received)) = &self.last_dnd_fetch - && let Some(window) = self.window.as_ref() - { - if *received { - let mut data = window.data_transfer_result(*last_fetch_serial).unwrap(); - let uris = data.try_as_uris().unwrap(); - info!("{uris:#?}"); + if let Some(state) = &self.last_dnd_fetch { + if state.received { + let mut data = event_loop.data_transfer_result(state.serial).unwrap(); + assert_eq!(data.type_().hint(), Some(state.type_)); + match state.type_ { + TypeHint::Plaintext | TypeHint::Html => { + let text = data.try_as_string().unwrap(); + info!("{text:?}"); + }, + TypeHint::UriList => { + let uris = data.try_as_uris().unwrap(); + info!("{uris:#?}"); + }, + _ => { + unreachable!("Received a type we didn't ask for!"); + }, + } } else { info!("Never received"); } @@ -75,48 +92,50 @@ impl ApplicationHandler for Application { WindowEvent::DataTransferResult { serial, .. } => { info!("{event:?}"); - if let Some((last_fetch_serial, received)) = &mut self.last_dnd_fetch - && serial == *last_fetch_serial + if let Some(state) = &mut self.last_dnd_fetch + && state.serial == serial { - *received = true; + state.received = true; } }, WindowEvent::DragEntered { id } => { info!("{event:?}"); - let type_ = TypeHint::UriList; - - if let Some(window) = self.window.as_ref() { - let data_transfer = match window.data_transfer(id) { - Ok(dt) => dt, - Err(e) => { - error!("{e}"); - return; - }, - }; - - info!( - "Types: {:#?}", - data_transfer - .available_types() - .into_iter() - .filter_map(|ty| ty.hint()) - .collect::>() - ); - - if !data_transfer.has_type(&type_) { - info!("Cannot drop (cannot interpret input as URI list)"); - window.reject_drag(id).unwrap(); + let data_transfer = match event_loop.data_transfer(id) { + Ok(dt) => dt, + Err(e) => { + error!("{e}"); return; - } - - window.accept_drag_type(id, &type_).unwrap(); - - self.last_dnd_fetch = - window.fetch_data_transfer(id, &type_).ok().map(|serial| (serial, false)); - } else { - error!("No window!"); - } + }, + }; + + info!( + "Types: {:#?}", + data_transfer + .available_types() + .into_iter() + .filter_map(|ty| ty.hint()) + .collect::>() + ); + + let wanted_types = [TypeHint::Html, TypeHint::UriList, TypeHint::Plaintext] + .into_iter() + .filter(|ty| data_transfer.has_type(ty)) + .collect::>(); + + info!("Supported types: {:#?}", wanted_types); + + let Some(type_) = wanted_types.first().copied() else { + event_loop.reject_drag(id).unwrap(); + return; + }; + + event_loop.accept_drag_type(id, &type_).unwrap(); + + self.last_dnd_fetch = event_loop + .fetch_data_transfer(id, &type_) + .ok() + .map(|serial| FetchState { serial, type_, received: false }); }, WindowEvent::RedrawRequested => { let window = self.window.as_ref().unwrap(); From fb7779e7f43de1897fbc959f90e040e3fab14667 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Fri, 22 May 2026 16:46:12 +0200 Subject: [PATCH 21/87] WIP: Implement drag-and-drop for AppKit using new API --- winit-appkit/Cargo.toml | 1 + winit-appkit/src/app_state.rs | 7 + winit-appkit/src/dnd.rs | 346 ++++++++++++++++++++++++++++ winit-appkit/src/event_loop.rs | 34 ++- winit-appkit/src/lib.rs | 1 + winit-appkit/src/window.rs | 21 +- winit-appkit/src/window_delegate.rs | 95 +++++--- winit-core/src/application/mod.rs | 20 +- winit-core/src/data_transfer.rs | 12 +- winit-core/src/event.rs | 33 ++- winit-core/src/event_loop/mod.rs | 77 ++----- winit-core/src/window.rs | 61 ++++- winit-x11/src/event_loop.rs | 61 ----- winit-x11/src/event_processor.rs | 5 +- winit-x11/src/window.rs | 69 +++++- winit/examples/application.rs | 1 - winit/examples/dnd.rs | 44 ++-- 17 files changed, 693 insertions(+), 195 deletions(-) create mode 100644 winit-appkit/src/dnd.rs diff --git a/winit-appkit/Cargo.toml b/winit-appkit/Cargo.toml index 1094510621..4172771f3b 100644 --- a/winit-appkit/Cargo.toml +++ b/winit-appkit/Cargo.toml @@ -46,6 +46,7 @@ objc2-app-kit = { workspace = true, features = [ "NSOpenGLView", "NSPanel", "NSPasteboard", + "NSPasteboardItem", "NSResponder", "NSRunningApplication", "NSScreen", diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index 5b057b79b3..8fa2d81f75 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -18,10 +18,12 @@ use winit_core::window::WindowId; use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately}; use super::menu; use super::observer::EventLoopWaker; +use crate::dnd::DndState; #[derive(Debug)] pub(super) struct AppState { mtm: MainThreadMarker, + dnd: DndState, activation_policy: Option, default_menu: bool, activate_ignoring_other_apps: bool, @@ -65,6 +67,7 @@ impl AppState { let this = Rc::new(Self { mtm, + dnd: Default::default(), activation_policy, default_menu, activate_ignoring_other_apps, @@ -371,6 +374,10 @@ impl AppState { }; self.waker.borrow_mut().start_at(min_timeout(wait_timeout, app_timeout)); } + + pub fn dnd(&self) -> &DndState { + &self.dnd + } } /// Returns the minimum `Option`, taking into account that `None` diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs new file mode 100644 index 0000000000..5268790695 --- /dev/null +++ b/winit-appkit/src/dnd.rs @@ -0,0 +1,346 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::io; +use std::ops::Deref; +use std::sync::atomic::{AtomicI64, Ordering}; + +use objc2::Message; +use objc2::rc::Retained; +use objc2_app_kit::{ + NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, + NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, +}; +use objc2_foundation::{NSArray, NSData, NSString}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use winit_core::event_loop::AsyncRequestSerial; + +#[derive(Debug, Clone)] +pub struct PasteboardType { + hint: Option, + // We need to convert `NSString` to `str` since `NSString` isn't `Send`/`Sync` + inner: Retained, +} + +impl PasteboardType { + fn from_hint(hint: TypeHint) -> Option { + let hint_to_pasteboard_type = unsafe { + [ + (TypeHint::UriList, NSPasteboardTypeFileURL), + (TypeHint::Plaintext, NSPasteboardTypeString), + (TypeHint::Html, NSPasteboardTypeHTML), + (TypeHint::Image { extension_hint: Some("png") }, NSPasteboardTypePNG), + (TypeHint::Image { extension_hint: Some("tiff") }, NSPasteboardTypeTIFF), + (TypeHint::Audio { extension_hint: None }, NSPasteboardTypeSound), + ] + }; + + hint_to_pasteboard_type.into_iter().find_map(|(haystack, inner)| { + (haystack == hint).then(|| Self { hint: Some(hint), inner: inner.retain() }) + }) + } +} + +impl Deref for PasteboardType { + type Target = Retained; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub struct InvalidPasteboardTypeError(pub TypeHint); + +impl From> for PasteboardType { + fn from(value: Retained) -> Self { + let pasteboard_type_to_hint = unsafe { + [ + #[expect(deprecated)] + (objc2_app_kit::NSFilenamesPboardType, TypeHint::UriList), + (NSPasteboardTypeFileURL, TypeHint::UriList), + (NSPasteboardTypeString, TypeHint::Plaintext), + (NSPasteboardTypeHTML, TypeHint::Html), + (NSPasteboardTypePNG, TypeHint::Image { extension_hint: Some("png") }), + (NSPasteboardTypeTIFF, TypeHint::Image { extension_hint: Some("tiff") }), + (NSPasteboardTypeSound, TypeHint::Audio { extension_hint: None }), + ] + }; + + let hint = pasteboard_type_to_hint + .iter() + .find_map(|(pb_type, hint)| (**pb_type == *value).then_some(hint)); + + Self { hint: hint.copied(), inner: value } + } +} + +impl TransferType for PasteboardType { + fn hint(&self) -> Option { + self.hint + } +} + +#[derive(Clone, Debug)] +pub struct Pasteboard { + transfer_id: DataTransferId, + inner: Retained, +} + +impl Deref for Pasteboard { + type Target = Retained; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +static TRANSFER_ID: AtomicI64 = AtomicI64::new(0); + +impl From> for Pasteboard { + fn from(value: Retained) -> Self { + Self::new(value) + } +} + +impl Pasteboard { + pub fn new(inner: Retained) -> Self { + Self { + transfer_id: DataTransferId::from_raw(TRANSFER_ID.fetch_and(1, Ordering::Relaxed)), + inner, + } + } + + pub(crate) fn set_pasteboard(&mut self, pasteboard: Retained) { + self.inner = pasteboard; + } + + pub fn id(&self) -> DataTransferId { + self.transfer_id + } + + pub fn data_by_type(&self, type_: &PasteboardType) -> Option<()> { + todo!() + } +} + +impl DataTransfer for Pasteboard { + fn available_types(&self) -> Vec> { + self.inner + .types() + .map(|types| { + types + .into_iter() + .map(|pb_type| Box::new(PasteboardType::from(pb_type)) as _) + .collect() + }) + .unwrap_or_default() + } + + fn has_type(&self, type_: &dyn TransferType) -> bool { + let Some(pb_types) = self.inner.types() else { + return false; + }; + + if let Some(needle) = type_.cast_ref::().cloned() { + pb_types.iter().any(|haystack| **needle == *haystack) + } else if let Some(needle) = type_.hint() { + pb_types.iter().any(|haystack| PasteboardType::from(haystack).hint() == Some(needle)) + } else { + false + } + } +} + +#[derive(Debug, Clone)] +enum PasteboardTypeSpec { + PasteboardType(PasteboardType), + TypeHint(TypeHint), +} + +impl PasteboardTypeSpec { + fn from_dyn(type_: &dyn TransferType) -> Option { + match type_.cast_ref::() { + Some(pb_type) => Some(Self::PasteboardType(pb_type.clone())), + None => type_.hint().map(Into::into), + } + } +} + +impl From for PasteboardTypeSpec { + fn from(value: TypeHint) -> Self { + match PasteboardType::from_hint(value) { + Some(pb_type) => Self::PasteboardType(pb_type), + None => Self::TypeHint(value), + } + } +} + +impl PasteboardTypeSpec { + fn pasteboard_type(&self) -> Option<&PasteboardType> { + match self { + PasteboardTypeSpec::PasteboardType(pasteboard_type) => Some(pasteboard_type), + PasteboardTypeSpec::TypeHint(_) => None, + } + } +} + +#[derive(Debug)] +pub struct PasteboardValue { + // The concept of "top-level" types for a pasteboard doesn't always make sense on macOS due to + // the use of `pasteboardItems`, so we allow using `TypeHint` instead to preserve the user's + // intention. + type_: PasteboardTypeSpec, + inner: Pasteboard, +} + +impl PasteboardValue { + fn single_file_url(&self) -> Option { + self.inner + .stringForType(unsafe { NSPasteboardTypeFileURL }) + .map(|ns_str| ns_str.to_string()) + } +} + +impl Deref for PasteboardValue { + type Target = Retained; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +static NO_TYPE_HINT: Option = None; + +impl TypedData for PasteboardValue { + fn type_(&self) -> &dyn TransferType { + match &self.type_ { + PasteboardTypeSpec::PasteboardType(pasteboard_type) => pasteboard_type, + PasteboardTypeSpec::TypeHint(type_hint) => type_hint, + } + } + + fn try_read(&mut self) -> Option> { + struct DataReader { + inner: Retained, + offset: usize, + } + + impl DataReader { + fn new(data: Retained) -> Self { + Self { inner: data, offset: 0 } + } + } + + impl io::Read for DataReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let end = (self.offset + buf.len()).min(self.inner.len()); + let range = self.offset..end; + self.offset = end; + let bytes = unsafe { self.inner.as_bytes_unchecked() }; + let src = &bytes[range]; + buf[..src.len()].copy_from_slice(src); + + Ok(src.len()) + } + } + + impl io::BufRead for DataReader { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + Ok(unsafe { self.inner.as_bytes_unchecked() } + .get(self.offset..) + .unwrap_or_default()) + } + + fn consume(&mut self, amount: usize) { + self.offset = (self.offset + amount).min(self.inner.len()) + } + } + + self.inner + .dataForType(self.type_.pasteboard_type()?) + .map(|data| Box::new(DataReader::new(data)) as _) + } + + fn try_as_uris(&mut self) -> Option> { + if self.type_().hint() != Some(TypeHint::UriList) { + return None; + } + + let Some(items) = self.inner.pasteboardItems() else { + // The pasteboard didn't expose any items, so we try with the deprecated method. + #[expect(deprecated)] + let property_list = match self + .inner + .propertyListForType(unsafe { objc2_app_kit::NSFilenamesPboardType }) + { + Some(property_list) => property_list, + None => return self.single_file_url().map(|str| vec![str]), + }; + + let paths = property_list + .downcast::() + .unwrap() + .into_iter() + .map(|file| file.downcast::().unwrap().to_string()) + .collect(); + + return Some(paths); + }; + + Some( + items + .into_iter() + .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL })) + .map(|ns_str| ns_str.to_string()) + .collect(), + ) + } + + fn try_as_string(&mut self) -> Option { + self.inner.stringForType(self.type_.pasteboard_type()?).map(|ns_str| ns_str.to_string()) + } +} + +#[derive(Debug, Default)] +pub struct DndState { + inner: RefCell>, + serial_to_id: RefCell>, +} + +impl DndState { + pub fn set_pasteboard(&self, id: DataTransferId, pb: Retained) { + let mut inner = self.inner.borrow_mut(); + if let Some(state) = inner.get_mut(&id) { + state.inner = pb; + } + } + + pub fn insert

(&self, pb: P) -> DataTransferId + where + P: Into, + { + let value = pb.into(); + let id = value.id(); + + self.inner.borrow_mut().insert(id, value); + + id + } + + pub fn remove(&self, id: DataTransferId) { + if self.inner.borrow_mut().remove(&id).is_none() { + return; + } + + self.serial_to_id.borrow_mut().retain(|_, value| value.inner.id() != id); + } + + pub fn get(&self, id: DataTransferId) -> Option { + self.inner.borrow().get(&id).cloned() + } + + pub fn fetch_type(&self, type_: &dyn TransferType) -> AsyncRequestSerial { + let out = AsyncRequestSerial::get(); + // TODO: Remove `DataTransferResult` + todo!() + } +} diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 881762075e..30544ca9cc 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -17,10 +17,11 @@ use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_ob use winit_common::foundation::create_observer; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; use winit_core::error::{EventLoopError, RequestError}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, EventLoopProxy as CoreEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; @@ -126,6 +127,37 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let Some(pb) = self.app_state.dnd().get(id) else { + return Err(RequestError::Ignored); + }; + + if !pb.has_type(type_) { + return Err(RequestError::Ignored); + } + + // TODO: We can get rid of `DataTransferResult` entirely + Ok(todo!()) + } + + fn data_transfer_result( + &self, + serial: AsyncRequestSerial, + ) -> Result, RequestError> { + // TODO: We can get rid of `DataTransferResult` entirely, so this API should be redesigned + // to return a `TypedData` immediately from `fetch_data_transfer`. + + todo!() + } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + todo!() + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { diff --git a/winit-appkit/src/lib.rs b/winit-appkit/src/lib.rs index bcec63f14b..96c1c4f57d 100644 --- a/winit-appkit/src/lib.rs +++ b/winit-appkit/src/lib.rs @@ -71,6 +71,7 @@ mod util; mod app; mod app_state; mod cursor; +mod dnd; mod event; mod event_loop; mod ffi; diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index bc6cde65df..e1bde30581 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -10,12 +10,13 @@ use objc2_app_kit::{NSPanel, NSResponder, NSWindow}; use objc2_foundation::NSObject; use tracing::trace_span; use winit_core::cursor::Cursor; +use winit_core::data_transfer::{DataTransferId, TransferType}; use winit_core::error::RequestError; use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ - ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + ImeCapabilities, ImeRequest, ImeRequestError, Theme, UnknownDataTransfer, UserAttentionType, + Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use super::event_loop::ActiveEventLoop; @@ -339,6 +340,22 @@ impl CoreWindow for Window { fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { self } + + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + todo!() + } + + fn accept_drag_type( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result<(), UnknownDataTransfer> { + todo!() + } + + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + todo!() + } } define_class!( diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index e316973e7c..2a291dd3be 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -44,6 +44,7 @@ use objc2_foundation::{ use tracing::{debug_span, trace, warn}; use winit_common::core_foundation::MainRunLoop; use winit_core::cursor::Cursor; +use winit_core::data_transfer::DataTransferId; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::icon::Icon; @@ -60,6 +61,7 @@ use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_di use super::util::cgerr; use super::view::WinitView; use super::window::{WinitPanel, WinitWindow, window_id}; +use crate::dnd::Pasteboard; use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS}; #[derive(Debug)] @@ -67,6 +69,8 @@ pub(crate) struct State { /// Strong reference to the global application state. app_state: Rc, + drag_state: Cell>, + window: Retained, // During `windowDidResize`, we use this to only send Moved if the position changed. @@ -364,30 +368,21 @@ define_class!( fn dragging_entered(&self, sender: &ProtocolObject) -> bool { let _entered = debug_span!("draggingEntered:").entered(); - use std::path::PathBuf; - let pb = sender.draggingPasteboard(); - - #[allow(deprecated)] - let property_list = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { - Some(property_list) => property_list, - None => return false.into(), - }; - - let paths = property_list - .downcast::() - .unwrap() - .into_iter() - .map(|file| PathBuf::from(file.downcast::().unwrap().to_string())) - .collect(); - let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); let position = LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragEntered { paths, position }); + let vars = self.ivars(); + + let transfer_id = vars.app_state.dnd().insert(pb); + self.queue_event(WindowEvent::DragEntered { + id: transfer_id, + position: Some(position), + }); + vars.drag_state.set(Some(transfer_id)); true } @@ -403,12 +398,22 @@ define_class!( fn dragging_updated(&self, sender: &ProtocolObject) -> bool { let _entered = debug_span!("draggingUpdated:").entered(); + let vars = self.ivars(); + + let Some(transfer_id) = vars.drag_state.get() else { + return false.into(); + }; + + let pb = sender.draggingPasteboard(); + + vars.app_state.dnd().set_pasteboard(transfer_id, pb); + let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); let position = LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragMoved { position }); + self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); true } @@ -425,29 +430,24 @@ define_class!( fn perform_drag_operation(&self, sender: &ProtocolObject) -> bool { let _entered = debug_span!("performDragOperation:").entered(); - use std::path::PathBuf; + let vars = self.ivars(); - let pb = sender.draggingPasteboard(); - - #[allow(deprecated)] - let property_list = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { - Some(property_list) => property_list, - None => return false.into(), + let Some(transfer_id) = vars.drag_state.get() else { + return false.into(); }; - let paths = property_list - .downcast::() - .unwrap() - .into_iter() - .map(|file| PathBuf::from(file.downcast::().unwrap().to_string())) - .collect(); + let pb = sender.draggingPasteboard(); + + let transfer_id = transfer_id; + vars.app_state.dnd().set_pasteboard(transfer_id, pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); let position = LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragDropped { paths, position }); + self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); + self.queue_event(WindowEvent::DragDropped { id: transfer_id }); true } @@ -456,6 +456,12 @@ define_class!( #[unsafe(method(concludeDragOperation:))] fn conclude_drag_operation(&self, _sender: Option<&NSObject>) { let _entered = debug_span!("concludeDragOperation:").entered(); + let vars = self.ivars(); + + if let Some(transfer_id) = vars.drag_state.get() { + vars.app_state.dnd().remove(transfer_id); + vars.drag_state.set(None); + } } /// Invoked when the dragging operation is cancelled @@ -463,13 +469,29 @@ define_class!( fn dragging_exited(&self, sender: Option<&ProtocolObject>) { let _entered = debug_span!("draggingExited:").entered(); - let position = sender.map(|sender| { + let vars = self.ivars(); + let Some(transfer_id) = vars.drag_state.get() else { + return; + }; + + if let Some(sender) = sender { + let pb = sender.draggingPasteboard(); + vars.app_state.dnd().set_pasteboard(transfer_id, pb); + let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); - LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()) - }); + let position = + LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragLeft { position }); + self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); + } + + self.queue_event(WindowEvent::DragLeft { id: transfer_id }); + + if let Some(transfer_id) = vars.drag_state.get() { + vars.app_state.dnd().remove(transfer_id); + vars.drag_state.set(None); + } } } @@ -816,6 +838,7 @@ impl WindowDelegate { let delegate = mtm.alloc().set_ivars(State { app_state: Rc::clone(app_state), + drag_state: Default::default(), window: window.retain(), previous_position: Cell::new(flip_window_screen_coordinates(window.frame())), previous_scale_factor: Cell::new(scale_factor), diff --git a/winit-core/src/application/mod.rs b/winit-core/src/application/mod.rs index b9fd34bff1..cf4aabfe25 100644 --- a/winit-core/src/application/mod.rs +++ b/winit-core/src/application/mod.rs @@ -1,6 +1,6 @@ //! End user application handling. -use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use crate::event::{DataTransferEvent, DeviceEvent, DeviceId, StartCause, WindowEvent}; use crate::event_loop::ActiveEventLoop; use crate::window::WindowId; @@ -199,6 +199,14 @@ pub trait ApplicationHandler { event: WindowEvent, ); + /// Emitted when the state of a [data transfer](crate::data_transfer) changes. + /// + /// Most applications do not need to handle this, see documentation for + /// [`ActiveEventLoop::fetch_data_transfer`]. + fn data_transfer_event(&mut self, event_loop: &dyn ActiveEventLoop, event: DataTransferEvent) { + let _ = (event_loop, event); + } + /// Emitted when the OS sends an event to a device. /// /// Whether device events are delivered depends on the backend in use. @@ -411,6 +419,11 @@ impl ApplicationHandler for &mut A { fn macos_handler(&mut self) -> Option<&mut dyn macos::ApplicationHandlerExtMacOS> { (**self).macos_handler() } + + #[inline] + fn data_transfer_event(&mut self, event_loop: &dyn ActiveEventLoop, event: DataTransferEvent) { + (**self).data_transfer_event(event_loop, event); + } } #[deny(clippy::missing_trait_methods)] @@ -479,4 +492,9 @@ impl ApplicationHandler for Box { fn macos_handler(&mut self) -> Option<&mut dyn macos::ApplicationHandlerExtMacOS> { (**self).macos_handler() } + + #[inline] + fn data_transfer_event(&mut self, event_loop: &dyn ActiveEventLoop, event: DataTransferEvent) { + (**self).data_transfer_event(event_loop, event); + } } diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 7f614f5930..96951687d1 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -106,7 +106,7 @@ pub enum TypeHint { /// /// [`hint`](TransferType::hint) can be called to get the type in /// a cross-platform format (see [`TypeHint`]) -pub trait TransferType: AsAny + Send + Sync + fmt::Debug { +pub trait TransferType: AsAny + fmt::Debug { /// Get the cross-platform representation of this type. /// /// If this returns `None`, then this is a platform-dependent type that has no cross-platform @@ -120,10 +120,16 @@ impl TransferType for TypeHint { } } +impl TransferType for Option { + fn hint(&self) -> Option { + *self + } +} + impl_dyn_casting!(TransferType); /// Data that has been fetched from a data transfer -pub trait TypedData: AsAny + Send + Sync + fmt::Debug { +pub trait TypedData: AsAny + fmt::Debug { /// The type of this `TypedData`. fn type_(&self) -> &dyn TransferType; @@ -150,7 +156,7 @@ impl_dyn_casting!(TypedData); /// asynchronous operation. To fetch the data from the source application, see /// [`Window::fetch_data_transfer`](crate::window::Window::fetch_data_transfer) /// and [`WindowEvent::DataTransferResult`](crate::event::WindowEvent::DataTransferResult). -pub trait DataTransfer: AsAny + Send + Sync + fmt::Debug { +pub trait DataTransfer: AsAny + fmt::Debug { /// Display the list of all available types. /// /// This is useful if more-complex type matching is required, but for most cases diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index ea2ea38c00..bd3974306c 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -79,14 +79,25 @@ pub enum WindowEvent { DragEntered { /// Data transfer object specifying the ID and available types. id: DataTransferId, + /// (x,y) coordinates in pixels relative to the top-left corner of the window. + /// + /// May be negative on some platforms if something is dragged over a window's decorations + /// (title bar, frame, etc). + /// + /// Some platforms will provide this on enter, others do not. On most platforms, the + /// receiving application is expected to either accept or reject a drag-and-drop operation + /// immediately upon receiving `DragEntered`, so when possible it is provided here to make + /// that behavior easier to implement correctly. + position: Option>, }, /// A file drag operation has moved over the window. DragPosition { /// Data transfer object specifying the ID and available types. id: DataTransferId, - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). + /// (x,y) coordinates in pixels relative to the top-left corner of the window. + /// + /// May be negative on some platforms if something is dragged over a window's decorations + /// (title bar, frame, etc). position: PhysicalPosition, }, /// The file drag operation has dropped file(s) on the window. @@ -100,9 +111,6 @@ pub enum WindowEvent { id: DataTransferId, }, - /// The result of a data transfer is available. - DataTransferResult { id: DataTransferId, serial: AsyncRequestSerial }, - /// The window gained or lost focus. /// /// The parameter is true if the window has gained focus, and false if it has lost focus. @@ -615,6 +623,17 @@ impl FingerId { } } +// TODO: Remove this, `DataTransferResult` is only necessary for X11 and there's a workaround +// implemented by Qt https://github.com/qt/qtbase/blob/dev/src/plugins/platforms/xcb/qxcbclipboard.cpp#L724 +pub enum DataTransferEvent { + // TODO: We should remove this event, but it would still be nice if we had a way to express + // this (probably in `ActiveEventLoop`). + Dropped { id: DataTransferId }, + + // TODO: Remove this + FetchResult { id: DataTransferId, serial: AsyncRequestSerial }, +} + /// Represents raw hardware events that are not associated with any particular window. /// /// Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera @@ -1560,7 +1579,7 @@ mod tests { with_window_event(Focused(true)); with_window_event(Moved((0, 0).into())); with_window_event(SurfaceResized((0, 0).into())); - with_window_event(DragEntered { id: dnd_data}); + with_window_event(DragEntered { id: dnd_data, position: None }); with_window_event(DragPosition { id: dnd_data, position: (0, 0).into() }); with_window_event(DragDropped { id: dnd_data }); with_window_event(DragLeft { id: dnd_data }); diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index 629cc96b40..c30720c052 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -3,7 +3,7 @@ pub mod pump_events; pub mod register; pub mod run_on_demand; -use std::fmt::{self, Debug, Display}; +use std::fmt::{self, Debug}; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; @@ -18,19 +18,6 @@ use crate::error::{NotSupportedError, RequestError}; use crate::monitor::MonitorHandle; use crate::window::{Theme, Window, WindowAttributes}; -/// An operation was attempted on a data transfer ID, but that ID was invalid. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct UnknownDataTransfer(pub DataTransferId); - -impl Display for UnknownDataTransfer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let id = self.0.into_raw(); - write!(f, "Unknown data transfer with ID {id}") - } -} - -impl std::error::Error for UnknownDataTransfer {} - pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Creates an [`EventLoopProxy`] that can be used to dispatch user events /// to the main event loop, possibly from another thread. @@ -132,6 +119,12 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer). /// /// This may be called multiple times on the same [`DataTransferId`] with different types. + /// + /// The result of the fetch is only guaranteed to be available after the + /// [`DataTransferEvent::FetchResult`](crate::event::DataTransferEvent::FetchResult) event is + /// emitted. However, on most platforms the result will be available immediately. The + /// user can attempt to access the result without waiting for `FetchResult`, and on platforms + /// where this does not . fn fetch_data_transfer( &self, id: DataTransferId, @@ -164,56 +157,12 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { /// /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will /// return an error. - fn data_transfer( - &self, - id: DataTransferId, - ) -> Result, UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } - - /// Mark a given data transfer ID as being accepted by the window. By default, a drag will be - /// rejected. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can be dropped. - /// - /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying - /// one or more accepted types. For platforms that require specifying a type, `accept_drag` will - /// mark all available types as accepted. - /// - /// For the most reliable cross-platform behaviour, - /// [`accept_drag_type`](Window::accept_drag_type) is preferred. - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } - - /// Mark a single type of a given data transfer ID as being accepted by the window. By default, - /// a drag will be rejected. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can be dropped. - /// - /// If the window may accept more than one of the advertised types, this method should be - /// called multiple times, once for each of the accepted types. - fn accept_drag_type( - &self, - id: DataTransferId, - type_: &dyn TransferType, - ) -> Result<(), UnknownDataTransfer> { - let _ = type_; - self.accept_drag(id) - } - - /// Mark a given data transfer ID as being rejected by the window. This is the default if - /// `accept_drag`/`accept_drag_type` is never called. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can _not_ be dropped. - /// - /// This will ensure that the OS/compositor indicates to the user that dropping the dragged data - /// is not possible. - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let _ = id; + Err(RequestError::NotSupported(NotSupportedError::new( + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ + this platform", + ))) } } diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 35e604ed2f..57b4cae985 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1,5 +1,5 @@ //! The [`Window`] trait and associated types. -use std::fmt; +use std::fmt::{self, Display}; use bitflags::bitflags; use cursor_icon::CursorIcon; @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::as_any::AsAny; use crate::cursor::Cursor; +use crate::data_transfer::{DataTransferId, TransferType}; use crate::error::RequestError; use crate::icon::Icon; use crate::monitor::{Fullscreen, MonitorHandle}; @@ -458,6 +459,19 @@ pub trait PlatformWindowAttributes: AsAny + std::fmt::Debug + Send + Sync { impl_dyn_casting!(PlatformWindowAttributes); +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + /// Represents a window. /// /// The window is closed when dropped. @@ -1436,6 +1450,51 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// Get the raw-window-handle v0.6 window handle. fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle; + + /// Mark a given data transfer ID as being accepted by the window. By default, a drag will be + /// rejected. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. + /// + /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying + /// one or more accepted types. For platforms that require specifying a type, `accept_drag` will + /// mark all available types as accepted. + /// + /// For the most reliable cross-platform behaviour, + /// [`accept_drag_type`](Window::accept_drag_type) is preferred. + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } + + /// Mark a single type of a given data transfer ID as being accepted by the window. By default, + /// a drag will be rejected. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. + /// + /// If the window may accept more than one of the advertised types, this method should be + /// called multiple times, once for each of the accepted types. + fn accept_drag_type( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result<(), UnknownDataTransfer> { + let _ = type_; + self.accept_drag(id) + } + + /// Mark a given data transfer ID as being rejected by the window. This is the default if + /// `accept_drag`/`accept_drag_type` is never called. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can _not_ be dropped. + /// + /// This will ensure that the OS/compositor indicates to the user that dropping the dragged data + /// is not possible. + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + Err(UnknownDataTransfer(id)) + } } impl_dyn_casting!(Window); diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 538c63a202..1c1192a7fc 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -773,67 +773,6 @@ impl RootActiveEventLoop for ActiveEventLoop { Ok(Box::new(Selection::new(self.dnd.clone()))) } - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let mut dnd = self.dnd.write().unwrap(); - if dnd.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - if dnd.accepted == Some(false) { - return Ok(()); - } - - dnd.accepted = Some(false); - - let Some(source_window) = dnd.source_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - let Some(window) = dnd.target_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - unsafe { - dnd.send_status(window, source_window, DndState::Rejected) - .expect("Failed to send `XdndStatus` message."); - } - dnd.reset(); - - Ok(()) - } - - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let mut dnd = self.dnd.write().unwrap(); - if dnd.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - if dnd.accepted == Some(true) { - return Ok(()); - } - - dnd.accepted = Some(true); - - let Some(source_window) = dnd.source_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - let Some(window) = dnd.target_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - unsafe { - dnd.send_status(window, source_window, DndState::Accepted) - .expect("Failed to send `XdndStatus` message."); - } - - Ok(()) - } - fn fetch_data_transfer( &self, id: DataTransferId, diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 7dd1af9039..902c5a6ec1 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -462,7 +462,10 @@ impl EventProcessor { dnd.transfer_id() }; - app.window_event(&self.target, window_id, WindowEvent::DragEntered { id: transfer_id }); + app.window_event(&self.target, window_id, WindowEvent::DragEntered { + id: transfer_id, + position: None, + }); return; } diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 0277732110..fb51748204 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -302,6 +302,67 @@ impl CoreWindow for Window { fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { self } + + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let mut dnd = self.dnd.write().unwrap(); + if dnd.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + if dnd.accepted == Some(false) { + return Ok(()); + } + + dnd.accepted = Some(false); + + let Some(source_window) = dnd.source_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + let Some(window) = dnd.target_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + unsafe { + dnd.send_status(window, source_window, DndState::Rejected) + .expect("Failed to send `XdndStatus` message."); + } + dnd.reset(); + + Ok(()) + } + + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let mut dnd = self.dnd.write().unwrap(); + if dnd.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + if dnd.accepted == Some(true) { + return Ok(()); + } + + dnd.accepted = Some(true); + + let Some(source_window) = dnd.source_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + let Some(window) = dnd.target_window else { + // TODO: Should have "other error" since this isn't an unknown data transfer. + return Err(UnknownDataTransfer(id)); + }; + + unsafe { + dnd.send_status(window, source_window, DndState::Accepted) + .expect("Failed to send `XdndStatus` message."); + } + + Ok(()) + } } impl rwh_06::HasDisplayHandle for Window { @@ -413,10 +474,11 @@ unsafe impl Sync for UnownedWindow {} #[derive(Debug)] pub struct UnownedWindow { pub(crate) xconn: Arc, // never changes - xwindow: xproto::Window, // never changes + dnd: Arc>, + xwindow: xproto::Window, // never changes #[allow(dead_code)] visual: u32, // never changes - root: xproto::Window, // never changes + root: xproto::Window, // never changes #[allow(dead_code)] screen_id: i32, // never changes sync_counter_id: Option, // never changes @@ -638,9 +700,12 @@ impl UnownedWindow { .visual; } + let dnd = event_loop.dnd.clone(); + #[allow(clippy::mutex_atomic)] let mut window = UnownedWindow { xconn: Arc::clone(xconn), + dnd, xwindow: xwindow as xproto::Window, visual, root, diff --git a/winit/examples/application.rs b/winit/examples/application.rs index ad005d23af..3d4e865ebf 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -548,7 +548,6 @@ impl ApplicationHandler for Application { | WindowEvent::DragEntered { .. } | WindowEvent::DragPosition { .. } | WindowEvent::DragDropped { .. } - | WindowEvent::DataTransferResult { .. } | WindowEvent::Destroyed | WindowEvent::Ime(_) | WindowEvent::Moved(_) => (), diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index aa1770fd99..5e66c08daa 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -2,8 +2,8 @@ use std::error::Error; use tracing::{error, info}; use winit::application::ApplicationHandler; -use winit::data_transfer::TypeHint; -use winit::event::WindowEvent; +use winit::data_transfer::{DataTransferId, TypeHint}; +use winit::event::{DataTransferEvent, WindowEvent}; use winit::event_loop::{ActiveEventLoop, AsyncRequestSerial, EventLoop}; use winit::window::{Window, WindowAttributes, WindowId}; @@ -23,6 +23,7 @@ fn main() -> Result<(), Box> { #[derive(Debug)] struct FetchState { + id: DataTransferId, serial: AsyncRequestSerial, type_: TypeHint, received: bool, @@ -54,6 +55,10 @@ impl ApplicationHandler for Application { _window_id: WindowId, event: WindowEvent, ) { + let Some(window) = self.window.as_ref() else { + return; + }; + match event { WindowEvent::DragLeft { .. } => { info!("{event:?}"); @@ -89,16 +94,7 @@ impl ApplicationHandler for Application { self.last_dnd_fetch = None; }, - WindowEvent::DataTransferResult { serial, .. } => { - info!("{event:?}"); - - if let Some(state) = &mut self.last_dnd_fetch - && state.serial == serial - { - state.received = true; - } - }, - WindowEvent::DragEntered { id } => { + WindowEvent::DragEntered { id, .. } => { info!("{event:?}"); let data_transfer = match event_loop.data_transfer(id) { @@ -126,16 +122,16 @@ impl ApplicationHandler for Application { info!("Supported types: {:#?}", wanted_types); let Some(type_) = wanted_types.first().copied() else { - event_loop.reject_drag(id).unwrap(); + window.reject_drag(id).unwrap(); return; }; - event_loop.accept_drag_type(id, &type_).unwrap(); + window.accept_drag_type(id, &type_).unwrap(); self.last_dnd_fetch = event_loop .fetch_data_transfer(id, &type_) .ok() - .map(|serial| FetchState { serial, type_, received: false }); + .map(|serial| FetchState { id, serial, type_, received: false }); }, WindowEvent::RedrawRequested => { let window = self.window.as_ref().unwrap(); @@ -148,4 +144,22 @@ impl ApplicationHandler for Application { _ => {}, } } + + fn data_transfer_event(&mut self, _: &dyn ActiveEventLoop, event: DataTransferEvent) { + match event { + DataTransferEvent::Dropped { id } => { + if self.last_dnd_fetch.as_ref().is_some_and(|state| state.id == id) { + self.last_dnd_fetch = None; + } + }, + DataTransferEvent::FetchResult { id, serial } => { + if let Some(state) = &mut self.last_dnd_fetch + && state.serial == serial + && id == state.id + { + state.received = true; + } + }, + } + } } From 98106b8defb2d3c7cbdbe827f14baa2921c3801a Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 12:03:19 +0200 Subject: [PATCH 22/87] Remove `DataTransferEvent` This commit also adds deadlock detection to X11, since on that platform the event loop needs to be polled in order for the selection to be received. --- winit-core/src/application/mod.rs | 20 +- winit-core/src/data_transfer.rs | 28 ++- winit-core/src/event.rs | 11 -- winit-core/src/event_loop/mod.rs | 23 +-- winit-x11/src/dnd.rs | 317 +++++++++++++++++++++++------- winit-x11/src/event_loop.rs | 86 +++----- winit-x11/src/event_processor.rs | 48 ++--- winit-x11/src/window.rs | 8 +- winit/examples/dnd.rs | 83 +++----- 9 files changed, 350 insertions(+), 274 deletions(-) diff --git a/winit-core/src/application/mod.rs b/winit-core/src/application/mod.rs index cf4aabfe25..b9fd34bff1 100644 --- a/winit-core/src/application/mod.rs +++ b/winit-core/src/application/mod.rs @@ -1,6 +1,6 @@ //! End user application handling. -use crate::event::{DataTransferEvent, DeviceEvent, DeviceId, StartCause, WindowEvent}; +use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; use crate::event_loop::ActiveEventLoop; use crate::window::WindowId; @@ -199,14 +199,6 @@ pub trait ApplicationHandler { event: WindowEvent, ); - /// Emitted when the state of a [data transfer](crate::data_transfer) changes. - /// - /// Most applications do not need to handle this, see documentation for - /// [`ActiveEventLoop::fetch_data_transfer`]. - fn data_transfer_event(&mut self, event_loop: &dyn ActiveEventLoop, event: DataTransferEvent) { - let _ = (event_loop, event); - } - /// Emitted when the OS sends an event to a device. /// /// Whether device events are delivered depends on the backend in use. @@ -419,11 +411,6 @@ impl ApplicationHandler for &mut A { fn macos_handler(&mut self) -> Option<&mut dyn macos::ApplicationHandlerExtMacOS> { (**self).macos_handler() } - - #[inline] - fn data_transfer_event(&mut self, event_loop: &dyn ActiveEventLoop, event: DataTransferEvent) { - (**self).data_transfer_event(event_loop, event); - } } #[deny(clippy::missing_trait_methods)] @@ -492,9 +479,4 @@ impl ApplicationHandler for Box { fn macos_handler(&mut self) -> Option<&mut dyn macos::ApplicationHandlerExtMacOS> { (**self).macos_handler() } - - #[inline] - fn data_transfer_event(&mut self, event_loop: &dyn ActiveEventLoop, event: DataTransferEvent) { - (**self).data_transfer_event(event_loop, event); - } } diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 96951687d1..d4c2bf139f 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -69,7 +69,7 @@ impl DataTransferId { } /// The set of types supported cross-platform. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum TypeHint { /// Plain UTF-8 text (see [`TypedData::try_as_plaintext`]). /// @@ -129,12 +129,22 @@ impl TransferType for Option { impl_dyn_casting!(TransferType); /// Data that has been fetched from a data transfer +/// +/// ### Blocking +/// +/// Note that, in general, this type provides a _non-blocking_ interface. This means that the reader +/// provided by [`try_read`](TypedData::try_read), as well other methods returning [`io::Result`], +/// may return an error with [`io::ErrorKind::WouldBlock`]. To ensure that the `TypedData` is ready +/// to read, the user may call [`wait_for_data`](TypedData::wait). This will block the current +/// thread until the data is ready to read without returning `WouldBlock`. **This should not be +/// called on the event handling thread**, as platforms may need to wait on OS events to populate +/// the data. pub trait TypedData: AsAny + fmt::Debug { /// The type of this `TypedData`. fn type_(&self) -> &dyn TransferType; /// If this value is readable as bytes, return a reader than can be used to read those bytes. - fn try_read(&mut self) -> Option>; + fn try_read(&mut self) -> Option>; /// Read this value as a list of URIs. /// @@ -142,12 +152,22 @@ pub trait TypedData: AsAny + fmt::Debug { /// /// The format of the returned URIs is simply a vector of strings. No validation is done /// to ensure that the URIs are valid or in the format - fn try_as_uris(&mut self) -> Option>; + fn try_as_uris(&mut self) -> io::Result>; /// Read this value as a plain text string. /// /// If this value is not readable as a string, return `None`. - fn try_as_string(&mut self) -> Option; + fn try_as_string(&mut self) -> io::Result; + + /// Block the current thread until the data is fully available, or until the data is + /// invalidated. + /// + /// Note that this doesn't mean that other methods will return `Ok`, simply that they won't + /// return `io::Error::WouldBlock`. + /// + /// If the data is ready to be read, return `Ok(())`. If this data has been invalidated (and + /// therefore this would wait forever), return `Err`. + fn wait_for_data(&self) -> io::Result<()>; } impl_dyn_casting!(TypedData); diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index bd3974306c..e1bbafa461 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -623,17 +623,6 @@ impl FingerId { } } -// TODO: Remove this, `DataTransferResult` is only necessary for X11 and there's a workaround -// implemented by Qt https://github.com/qt/qtbase/blob/dev/src/plugins/platforms/xcb/qxcbclipboard.cpp#L724 -pub enum DataTransferEvent { - // TODO: We should remove this event, but it would still be nice if we had a way to express - // this (probably in `ActiveEventLoop`). - Dropped { id: DataTransferId }, - - // TODO: Remove this - FetchResult { id: DataTransferId, serial: AsyncRequestSerial }, -} - /// Represents raw hardware events that are not associated with any particular window. /// /// Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index c30720c052..ffc4d2bfd1 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -119,17 +119,11 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer). /// /// This may be called multiple times on the same [`DataTransferId`] with different types. - /// - /// The result of the fetch is only guaranteed to be available after the - /// [`DataTransferEvent::FetchResult`](crate::event::DataTransferEvent::FetchResult) event is - /// emitted. However, on most platforms the result will be available immediately. The - /// user can attempt to access the result without waiting for `FetchResult`, and on platforms - /// where this does not . fn fetch_data_transfer( &self, id: DataTransferId, type_: &dyn TransferType, - ) -> Result { + ) -> Result, RequestError> { let _ = id; let _ = type_; Err(RequestError::NotSupported(NotSupportedError::new( @@ -138,21 +132,6 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { ))) } - /// Get the resolved data for a data transfer. - /// - /// Requires first calling [`fetch_data_transfer`](ActiveEventLoop::fetch_data_transfer) and - /// waiting for the corresponding `DataTransferResult` event. - fn data_transfer_result( - &self, - serial: AsyncRequestSerial, - ) -> Result, RequestError> { - let _ = serial; - Err(RequestError::NotSupported(NotSupportedError::new( - "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ - this platform", - ))) - } - /// Get a [data transfer](DataTransfer) by its ID. /// /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index e62a65c795..ae253c62ac 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -1,12 +1,13 @@ +use std::cell::Cell; use std::io; +use std::marker::PhantomData; use std::os::raw::*; -use std::path::{Path, PathBuf}; use std::str::Utf8Error; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, OnceLock, RwLock}; +use std::thread::ThreadId; use percent_encoding::percent_decode; use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; -use winit_core::event_loop::AsyncRequestSerial; use x11rb::protocol::xproto::{self, ConnectionExt}; use crate::atoms::AtomName::None as DndNone; @@ -28,6 +29,7 @@ pub enum UriListParseError { HostnameSpecified(#[allow(dead_code)] String), UnexpectedProtocol(#[allow(dead_code)] String), UnresolvablePath(#[allow(dead_code)] io::Error), + Io(#[allow(dead_code)] io::Error), } impl From for UriListParseError { @@ -42,58 +44,197 @@ impl From for UriListParseError { } } +// When `thread_id_value` is stabilized, this can become `AtomicU64`. +#[derive(Default, Debug)] +pub struct DeadlockSentinel(Arc>>); + +/// Read-side of `DeadlockSentinel` (to prevent accidentally guarding in a re-entrant way). +#[derive(Debug, Clone)] +pub struct DeadlockSentinelReader(Arc>>); + +impl DeadlockSentinelReader { + fn get(&self) -> Option { + *self.0.read().unwrap() + } +} + +#[must_use] +#[derive(Debug)] +pub struct DeadlockSentinelGuard(Arc>>); + +impl Drop for DeadlockSentinelGuard { + fn drop(&mut self) { + *self.0.write().unwrap() = None; + } +} + +impl DeadlockSentinel { + pub fn guard(&self) -> DeadlockSentinelGuard { + let mut writer = self.0.write().unwrap(); + assert!(writer.is_none(), "Internal error: re-entrant `DeadlockSentinelGuard`"); + *writer = Some(std::thread::current().id()); + DeadlockSentinelGuard(self.0.clone()) + } + + pub fn reader(&self) -> DeadlockSentinelReader { + DeadlockSentinelReader(self.0.clone()) + } +} + +#[derive(Debug, Default)] +struct SharedDataInnerState { + data: OnceLock, io::ErrorKind>>, +} + +impl SharedDataInnerState { + fn has_data(&self) -> bool { + self.data.get().is_some() + } + + fn try_data(&self) -> io::Result<&[u8]> { + self.data + .get() + .map(|data| data.as_ref().map(|data| &**data).map_err(|err| io::Error::from(*err))) + .ok_or_else(|| io::Error::from(io::ErrorKind::WouldBlock))? + } +} + +#[derive(Clone, Debug)] +pub(crate) struct SharedDataReader { + reader: Arc, + deadlock_sentinel: DeadlockSentinelReader, +} + +impl SharedDataReader { + fn try_data(&self) -> io::Result<&[u8]> { + self.reader.try_data() + } + + fn wait_for_data(&self) -> io::Result<()> { + if !self.reader.has_data() + && self.deadlock_sentinel.get() == Some(std::thread::current().id()) + { + return Err(io::ErrorKind::Deadlock.into()); + } + + let _ = self.reader.data.wait(); + + Ok(()) + } +} + +type NonSyncMarker = PhantomData>; + +#[derive(Debug, Default)] +pub(crate) struct SharedDataWriter { + writer: Arc, + _non_sync: NonSyncMarker, +} + +impl SharedDataWriter { + fn reader(&self, deadlock_sentinel: DeadlockSentinelReader) -> SharedDataReader { + SharedDataReader { reader: self.writer.clone(), deadlock_sentinel } + } + + pub(crate) fn write(&self, value: Box<[c_uchar]>) -> Result<(), Box<[c_uchar]>> { + // We know that we just passed `Ok`, so we can unwrap here. + self.writer.data.set(Ok(value)).map_err(|result| result.unwrap()) + } +} + +impl Drop for SharedDataWriter { + fn drop(&mut self) { + // Prevent `SelectionReader::wait_for_data` from deadlocking. + let _ = self.writer.data.set(Err(io::ErrorKind::BrokenPipe)); + } +} + #[derive(Clone, Debug)] pub struct SelectionReader { type_: SelectionType, - data: Arc<[c_uchar]>, + data: SharedDataReader, + pos: u64, +} + +impl io::Read for SelectionReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.with_cursor(|cursor| cursor.read(buf)) + } + + fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { + self.with_cursor(|cursor| cursor.read_to_end(buf)) + } + + fn read_to_string(&mut self, buf: &mut String) -> io::Result { + self.with_cursor(|cursor| cursor.read_to_string(buf)) + } + + fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { + self.with_cursor(|cursor| cursor.read_exact(buf)) + } +} + +impl io::BufRead for SelectionReader { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + // `io::Cursor::split` takes `&self` instead of `self`, so we need to reimplement it here. + let data = self.data.try_data()?; + + Ok(&data[self.pos.min(data.len() as u64) as usize..]) + } + + fn consume(&mut self, amount: usize) { + // `io::Cursor::consume` doesn't require a buffer, so we skip the `try_data` check implied + // by `with_cursor`. + self.pos += amount as u64; + } + + fn read_line(&mut self, buf: &mut String) -> io::Result { + self.with_cursor(|cursor| cursor.read_line(buf)) + } } impl SelectionReader { - pub(crate) fn new(type_: SelectionType, data: Arc<[c_uchar]>) -> Self { - Self { type_, data } - } - - pub fn parse_path_list(&self) -> Result, UriListParseError> { - if !self.data.is_empty() { - let mut path_list = Vec::new(); - let decoded = percent_decode(&self.data).decode_utf8()?.into_owned(); - for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { - // The format is specified as protocol://host/path - // However, it's typically simply protocol:///path - let path_str = if uri.starts_with("file://") { - let path_str = uri.replace("file://", ""); - if !path_str.starts_with('/') { - // A hostname is specified - // Supporting this case is beyond the scope of my mental health - return Err(UriListParseError::HostnameSpecified(path_str)); - } - path_str - } else { - // Only the file protocol is supported - return Err(UriListParseError::UnexpectedProtocol(uri.to_owned())); - }; - - let path = Path::new(&path_str).canonicalize()?; - path_list.push(path); - } - Ok(path_list) - } else { - Err(UriListParseError::EmptyData) - } + pub(crate) fn new(type_: SelectionType, data: SharedDataReader) -> Self { + Self { type_, data, pos: 0 } + } + + // Instead of reimplementing `io::Cursor`, we have a maximally-conservative + // implementation that just synchronizes state in order to prevent the chance + // of misimplementation. + fn with_cursor(&mut self, func: F) -> io::Result + where + F: FnOnce(&mut io::Cursor<&[u8]>) -> io::Result, + { + let data = self.data.try_data()?; + + let mut cursor = io::Cursor::new(&data[..]); + cursor.set_position(self.pos); + let result = func(&mut cursor)?; + let new_pos = cursor.position(); + self.pos = new_pos; + + Ok(result) } } impl TypedData for SelectionReader { - fn try_read(&mut self) -> Option> { - Some(Box::new(io::Cursor::new(&self.data))) + fn try_read(&mut self) -> Option> { + Some(Box::new(self.clone())) } fn type_(&self) -> &dyn TransferType { &self.type_ } - fn try_as_string(&mut self) -> Option { - fn decode_utf16_bytes(bytes: &[u8]) -> Option { + fn try_as_string(&mut self) -> io::Result { + fn invalid_data(err: E) -> io::Error + where + E: Into>, + { + io::Error::new(io::ErrorKind::InvalidData, err) + } + + fn decode_utf16_bytes(bytes: &[u8]) -> io::Result { let utf16 = bytes .chunks_exact(2) .into_iter() @@ -102,59 +243,75 @@ impl TypedData for SelectionReader { u16::from_ne_bytes(*bytes) }) .collect::>(); - String::from_utf16(&utf16).ok() + String::from_utf16(&utf16).map_err(invalid_data) } match self.type_.hint() { Some(TypeHint::Plaintext) | Some(TypeHint::Html) => { + let data = self.data.try_data()?; + // Bad way to detect UTF-16 - some applications (confirmed to at least happen with // Firefox) don't emit a BOM when passing HTML, so we need to check: // A) Does the string contain a null // B) Can the string be decoded as UTF-8 - if self.data.contains(&0) { - decode_utf16_bytes(&self.data) + if data.contains(&0) { + decode_utf16_bytes(data) // Even if we guess that it's utf-16, we'll still try utf-8 just in case - .or_else(|| str::from_utf8(&self.data).ok().map(|str| str.to_owned())) + .or_else(|_| { + str::from_utf8(data).map(|str| str.to_owned()).map_err(invalid_data) + }) } else { - str::from_utf8(&self.data) + str::from_utf8(data) .map(|str| str.to_owned()) - .ok() - .or_else(|| decode_utf16_bytes(&self.data)) + .map_err(invalid_data) + .or_else(|_| decode_utf16_bytes(data)) } }, Some(TypeHint::UriList) => { - percent_decode(&self.data).decode_utf8().ok().map(Into::into) + let data = self.data.try_data()?; + + percent_decode(data).decode_utf8().map(Into::into).map_err(invalid_data) }, - _ => None, + _ => Err(io::ErrorKind::InvalidData.into()), } } - fn try_as_uris(&mut self) -> Option> { + fn try_as_uris(&mut self) -> io::Result> { if self.type_().hint() != Some(TypeHint::UriList) { - return None; + return Err(io::ErrorKind::InvalidData.into()); } - Some( - self.try_as_string()? - .split(|c| c == '\n' || c == '\r') - .filter(|s| !s.is_empty()) - .map(Into::into) - .collect(), - ) + Ok(self + .try_as_string()? + .split(|c| c == '\n' || c == '\r') + .filter(|s| !s.is_empty()) + .map(Into::into) + .collect()) + } + + fn wait_for_data(&self) -> io::Result<()> { + self.data.wait_for_data() } } #[derive(Debug)] -pub struct SelectionFetchState { - pub serial: AsyncRequestSerial, - pub type_: xproto::Atom, +pub(crate) struct SelectionFetchState { + type_: SelectionType, // Populated by SelectionNotify event handler - pub value: Option>>, + value: SharedDataWriter, } impl SelectionFetchState { - pub fn new(type_: xproto::Atom) -> Self { - Self { serial: AsyncRequestSerial::get(), type_, value: None } + pub(crate) fn new(type_: SelectionType) -> Self { + Self { type_, value: Default::default() } + } + + pub(crate) fn type_(&self) -> &SelectionType { + &self.type_ + } + + pub(crate) fn as_reader(&self, sentinel: DeadlockSentinelReader) -> SelectionReader { + SelectionReader::new(self.type_().clone(), self.value.reader(sentinel)) } } @@ -175,6 +332,7 @@ pub struct Dnd { pub target_window: Option, // Populated by `fetch_data_transfer` pub last_fetched_selection: Option, + pub deadlock_sentinel: DeadlockSentinel, } #[derive(Debug)] @@ -188,7 +346,7 @@ impl Selection { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Hash)] pub struct SelectionType { hint: Option, atom: xproto::Atom, @@ -289,15 +447,19 @@ impl DataTransfer for Selection { } impl Dnd { - pub fn new(xconn: Arc) -> Self { - Self::with_id(xconn, DataTransferId::from_raw(0)) + pub fn new(xconn: Arc, sentinel: DeadlockSentinel) -> Self { + Self::with_id(xconn, sentinel, DataTransferId::from_raw(0)) } pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> { self.types.as_ref()?.iter().find(|haystack| haystack.hint() == Some(hint)) } - fn with_id(xconn: Arc, transfer_id: DataTransferId) -> Self { + fn with_id( + xconn: Arc, + deadlock_sentinel: DeadlockSentinel, + transfer_id: DataTransferId, + ) -> Self { Dnd { xconn, transfer_id, @@ -307,6 +469,7 @@ impl Dnd { source_window: None, target_window: None, last_fetched_selection: None, + deadlock_sentinel, } } @@ -316,8 +479,9 @@ impl Dnd { pub fn reset(&mut self) { let xconn = self.xconn.clone(); + let sentinel = std::mem::take(&mut self.deadlock_sentinel); let new_id = DataTransferId::from_raw(self.transfer_id.into_raw().wrapping_add(1)); - *self = Self::with_id(xconn, new_id); + *self = Self::with_id(xconn, sentinel, new_id); } pub unsafe fn send_status( @@ -395,16 +559,21 @@ impl Dnd { .expect_then_ignore_error("Failed to send XdndSelection event") } - pub unsafe fn read_data( - &self, - window: xproto::Window, - ) -> Result<(xproto::Atom, Vec), util::GetPropertyError> { + pub unsafe fn read_data(&self, window: xproto::Window) -> Result<(), util::GetPropertyError> { + // Never fetched + let data = + self.last_fetched_selection.as_ref().ok_or_else(|| util::GetPropertyError::Unknown)?; + let atoms = self.xconn.atoms(); let type_ = self .last_fetched_selection .as_ref() - .map(|state| state.type_) + .map(|state| state.type_.atom()) .ok_or(util::GetPropertyError::Unknown)?; - Ok((type_, self.xconn.get_property(window, atoms[XdndSelection], type_)?)) + let bytes = self.xconn.get_property(window, atoms[XdndSelection], type_)?; + + data.value.write(bytes.into()).map_err(|_| util::GetPropertyError::Unknown)?; + + Ok(()) } } diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 1c1192a7fc..6c5a276688 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -24,9 +24,9 @@ use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::event::{DeviceId, StartCause, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider, - OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, + OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::{Theme, Window as CoreWindow, WindowAttributes, WindowId}; @@ -38,7 +38,7 @@ use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; use crate::atoms::*; -use crate::dnd::{Dnd, DndState, SelectionFetchState}; +use crate::dnd::{DeadlockSentinelGuard, Dnd, SelectionFetchState}; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; use crate::util::{self, CustomCursor}; @@ -228,7 +228,7 @@ impl EventLoop { let net_wm_ping = atoms[_NET_WM_PING]; let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST]; - let dnd = Dnd::new(Arc::clone(&xconn)); + let dnd = Dnd::new(Arc::clone(&xconn), Default::default()); let dnd = Arc::new(RwLock::new(dnd)); let (ime_sender, ime_receiver) = mpsc::channel(); @@ -560,6 +560,8 @@ impl EventLoop { } fn single_iteration(&mut self, app: &mut A, cause: StartCause) { + let _guard = self.event_processor.target.selection_deadlock_guard(); + app.new_events(&self.event_processor.target, cause); // NB: For consistency all platforms must call `can_create_surfaces` even though X11 @@ -623,9 +625,8 @@ impl EventLoop { fn drain_events(&mut self, app: &mut A) { let mut xev = MaybeUninit::uninit(); - while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } { - let mut xev = unsafe { xev.assume_init() }; - self.event_processor.process_event(&mut xev, app); + while let Some(xev) = self.event_processor.poll_one_event(&mut xev) { + self.event_processor.process_event(xev, app); } } @@ -665,6 +666,10 @@ impl ActiveEventLoop { &self.xconn } + pub(crate) fn selection_deadlock_guard(&self) -> DeadlockSentinelGuard { + self.dnd.read().unwrap().deadlock_sentinel.guard() + } + /// Update the device event based on window focus. pub fn update_listen_device_events(&self, focus: bool) { let device_events = self.device_events.get() == DeviceEvents::Always @@ -762,12 +767,9 @@ impl RootActiveEventLoop for ActiveEventLoop { self } - fn data_transfer( - &self, - id: DataTransferId, - ) -> Result, UnknownDataTransfer> { + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { if self.dnd.read().unwrap().transfer_id() != id { - return Err(UnknownDataTransfer(id)); + return Err(RequestError::Ignored); } Ok(Box::new(Selection::new(self.dnd.clone()))) @@ -777,7 +779,7 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, id: DataTransferId, type_: &dyn TransferType, - ) -> Result { + ) -> Result, RequestError> { let mut dnd = self.dnd.write().unwrap(); if dnd.transfer_id() != id { return Err(RequestError::NotSupported(NotSupportedError::new( @@ -791,12 +793,6 @@ impl RootActiveEventLoop for ActiveEventLoop { ))); } - if let Some(state) = &dnd.last_fetched_selection - && state.value.is_some() - { - return Ok(state.serial); - } - let Some(window) = dnd.target_window else { return Err(RequestError::NotSupported(NotSupportedError::new( "Unknown target window", @@ -809,47 +805,25 @@ impl RootActiveEventLoop for ActiveEventLoop { .cloned() .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; - let new_fetch_state = SelectionFetchState::new(type_.atom()); - let serial = new_fetch_state.serial; - - dnd.last_fetched_selection = Some(new_fetch_state); - - // This results in the `SelectionNotify` event below - unsafe { - // TODO: Handle this better - dnd.convert_selection(window, self.xconn.timestamp(), type_.atom()); - } - - Ok(serial) - } + let mut new_state = + dnd.last_fetched_selection.take().filter(|state| state.type_() == &type_); - fn data_transfer_result( - &self, - serial: AsyncRequestSerial, - ) -> Result, RequestError> { - let dnd = self.dnd.read().unwrap(); + let deadlock_sentinel = dnd.deadlock_sentinel.reader(); + let reader = new_state + .get_or_insert_with(|| { + // This results in the `SelectionNotify` event + unsafe { + // TODO: Handle this better + dnd.convert_selection(window, self.xconn.timestamp(), type_.atom()); + } - if dnd.source_window.is_none() { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Unknown source window", - ))); - } + SelectionFetchState::new(type_) + }) + .as_reader(deadlock_sentinel); - if dnd.target_window.is_none() { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Unknown target window", - ))); - } + dnd.last_fetched_selection = new_state; - dnd.last_fetched_selection - .as_ref() - .filter(|state| state.serial == serial) - .and_then(|state| state.value.as_ref()) - // TODO: Actually return the error to the user somehow, maybe through a dummy - // `TypedData` impl? - .and_then(|res| res.as_ref().ok()) - .map(|reader| reader.clone() as _) - .ok_or(RequestError::Ignored) + Ok(Box::new(reader)) } } diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 902c5a6ec1..7902cb8d90 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -1,8 +1,9 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, VecDeque}; +use std::mem::MaybeUninit; use std::os::raw::{c_char, c_int, c_long, c_ulong}; +use std::slice; use std::sync::{Arc, Mutex}; -use std::{io, slice}; use dpi::{PhysicalPosition, PhysicalSize}; use tracing::warn; @@ -32,7 +33,7 @@ use x11rb::x11_utils::{ExtensionInformation, Serialize}; use xkbcommon_dl::xkb_mod_mask_t; use crate::atoms::*; -use crate::dnd::{DndState, SelectionReader, SelectionType}; +use crate::dnd::{DndState, SelectionType}; use crate::event_loop::{ ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType, ScrollOrientation, mkdid, mkwid, @@ -294,7 +295,10 @@ impl EventProcessor { unsafe { (self.target.xconn.xlib.XPending)(self.target.xconn.display) != 0 } } - pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool { + pub fn poll_one_event<'a>( + &mut self, + event_ptr: &'a mut MaybeUninit, + ) -> Option<&'a mut XEvent> { // This function is used to poll and remove a single event // from the Xlib event queue in a non-blocking, atomic way. // XCheckIfEvent is non-blocking and removes events from queue. @@ -304,20 +308,21 @@ impl EventProcessor { unsafe extern "C" fn predicate( _display: *mut XDisplay, _event: *mut XEvent, - _arg: *mut c_char, + _filter: *mut c_char, ) -> c_int { - // This predicate always returns "true" (1) to accept all events 1 } - unsafe { + let event_initialized = unsafe { (self.target.xconn.xlib.XCheckIfEvent)( self.target.xconn.display, - event_ptr, + event_ptr.as_mut_ptr(), Some(predicate), std::ptr::null_mut(), ) != 0 - } + }; + + event_initialized.then(|| unsafe { event_ptr.assume_init_mut() }) } pub fn init_device(&self, device: xinput::DeviceId) { @@ -570,11 +575,11 @@ impl EventProcessor { } } - fn selection_notify(&mut self, xev: &XSelectionEvent, app: &mut dyn ApplicationHandler) { + // TODO: Should we have an explicit notification for when the selection is ready? + fn selection_notify(&mut self, xev: &XSelectionEvent, _: &mut dyn ApplicationHandler) { let atoms = self.target.xconn.atoms(); let window = xev.requestor as xproto::Window; - let window_id = mkwid(window); // Set the timestamp. self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); @@ -585,28 +590,7 @@ impl EventProcessor { return; } - // This is where we receive data from drag and drop - let (serial, transfer_id) = { - let mut dnd = self.target.dnd.write().unwrap(); - let result = unsafe { dnd.read_data(window) }; - let Some(selection_fetch_state) = &mut dnd.last_fetched_selection else { - return; - }; - let new_value = result - .map(|(ty, data)| { - Box::new(SelectionReader::new(SelectionType::new(atoms, ty), data.into())) - }) - .map_err(io::Error::other); - - selection_fetch_state.value = Some(new_value); - - (selection_fetch_state.serial, dnd.transfer_id()) - }; - - app.window_event(&self.target, window_id, WindowEvent::DataTransferResult { - id: transfer_id, - serial, - }); + let _ = unsafe { self.target.dnd.read().unwrap().read_data(window) }; } fn configure_notify(&self, xev: &XConfigureEvent, app: &mut dyn ApplicationHandler) { diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index fb51748204..61a86f1e6a 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -5,13 +5,14 @@ use std::num::NonZeroU32; use std::ops::Deref; use std::os::raw::*; use std::path::Path; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard, RwLock}; use std::{cmp, env}; use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use tracing::{debug, info, warn}; use winit_core::application::ApplicationHandler; use winit_core::cursor::Cursor; +use winit_core::data_transfer::DataTransferId; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::AsyncRequestSerial; @@ -21,8 +22,8 @@ use winit_core::monitor::{ }; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest as CoreImeRequest, ImeRequestError, - ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, - WindowButtons, WindowId, WindowLevel, + ResizeDirection, Theme, UnknownDataTransfer, UserAttentionType, Window as CoreWindow, + WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use x11rb::connection::{Connection, RequestConnection}; use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; @@ -32,6 +33,7 @@ use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle} use x11rb::protocol::{randr, xinput}; use crate::atoms::*; +use crate::dnd::{Dnd, DndState}; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, WakeSender, X11Error, xinput_fp1616_to_float, diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 5e66c08daa..4ae70dca37 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -2,9 +2,9 @@ use std::error::Error; use tracing::{error, info}; use winit::application::ApplicationHandler; -use winit::data_transfer::{DataTransferId, TypeHint}; -use winit::event::{DataTransferEvent, WindowEvent}; -use winit::event_loop::{ActiveEventLoop, AsyncRequestSerial, EventLoop}; +use winit::data_transfer::{TypeHint, TypedData}; +use winit::event::WindowEvent; +use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::window::{Window, WindowAttributes, WindowId}; #[path = "util/fill.rs"] @@ -21,19 +21,11 @@ fn main() -> Result<(), Box> { Ok(event_loop.run_app(app)?) } -#[derive(Debug)] -struct FetchState { - id: DataTransferId, - serial: AsyncRequestSerial, - type_: TypeHint, - received: bool, -} - /// Application state and event handling. #[derive(Debug, Default)] struct Application { window: Option>, - last_dnd_fetch: Option, + last_dnd_fetch: Option>, } impl Application { @@ -70,25 +62,24 @@ impl ApplicationHandler for Application { WindowEvent::DragDropped { .. } => { info!("{event:?}"); - if let Some(state) = &self.last_dnd_fetch { - if state.received { - let mut data = event_loop.data_transfer_result(state.serial).unwrap(); - assert_eq!(data.type_().hint(), Some(state.type_)); - match state.type_ { - TypeHint::Plaintext | TypeHint::Html => { - let text = data.try_as_string().unwrap(); - info!("{text:?}"); - }, - TypeHint::UriList => { - let uris = data.try_as_uris().unwrap(); - info!("{uris:#?}"); - }, - _ => { - unreachable!("Received a type we didn't ask for!"); - }, - } - } else { - info!("Never received"); + if let Some(data) = &mut self.last_dnd_fetch { + // This may return an error with `io::ErrorKind::Deadlock` on X11 if + // this is called in the event loop thread while the application is + // still waiting for data. + data.wait_for_data().unwrap(); + + match data.type_().hint() { + Some(TypeHint::Plaintext | TypeHint::Html) => { + let text = data.try_as_string().unwrap(); + info!("{text:?}"); + }, + Some(TypeHint::UriList) => { + let uris = data.try_as_uris().unwrap(); + info!("{uris:#?}"); + }, + _ => { + unreachable!("Received a type we didn't ask for!"); + }, } } @@ -128,10 +119,14 @@ impl ApplicationHandler for Application { window.accept_drag_type(id, &type_).unwrap(); - self.last_dnd_fetch = event_loop - .fetch_data_transfer(id, &type_) - .ok() - .map(|serial| FetchState { id, serial, type_, received: false }); + self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok(); + + match self.last_dnd_fetch.as_ref().unwrap().wait_for_data() { + Err(e) if e.kind() == std::io::ErrorKind::Deadlock => { + eprintln!("Immediately waiting for a fetched data transfer may deadlock!"); + }, + _ => {}, + } }, WindowEvent::RedrawRequested => { let window = self.window.as_ref().unwrap(); @@ -144,22 +139,4 @@ impl ApplicationHandler for Application { _ => {}, } } - - fn data_transfer_event(&mut self, _: &dyn ActiveEventLoop, event: DataTransferEvent) { - match event { - DataTransferEvent::Dropped { id } => { - if self.last_dnd_fetch.as_ref().is_some_and(|state| state.id == id) { - self.last_dnd_fetch = None; - } - }, - DataTransferEvent::FetchResult { id, serial } => { - if let Some(state) = &mut self.last_dnd_fetch - && state.serial == serial - && id == state.id - { - state.received = true; - } - }, - } - } } From 2e99f3844062e35c61ba50375b496bb4bf02d287 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 14:36:24 +0200 Subject: [PATCH 23/87] Implement dnd for appkit --- winit-appkit/src/dnd.rs | 105 ++++++++++++---------------- winit-appkit/src/event_loop.rs | 15 +--- winit-appkit/src/window.rs | 14 ++-- winit-appkit/src/window_delegate.rs | 62 ++++++++++------ winit-core/src/data_transfer.rs | 2 +- winit-x11/src/dnd.rs | 2 +- 6 files changed, 94 insertions(+), 106 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 5268790695..44f9b0658e 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -5,7 +5,7 @@ use std::ops::Deref; use std::sync::atomic::{AtomicI64, Ordering}; use objc2::Message; -use objc2::rc::Retained; +use objc2::rc::{Retained, Weak}; use objc2_app_kit::{ NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, @@ -93,22 +93,7 @@ impl Deref for Pasteboard { } } -static TRANSFER_ID: AtomicI64 = AtomicI64::new(0); - -impl From> for Pasteboard { - fn from(value: Retained) -> Self { - Self::new(value) - } -} - impl Pasteboard { - pub fn new(inner: Retained) -> Self { - Self { - transfer_id: DataTransferId::from_raw(TRANSFER_ID.fetch_and(1, Ordering::Relaxed)), - inner, - } - } - pub(crate) fn set_pasteboard(&mut self, pasteboard: Retained) { self.inner = pasteboard; } @@ -116,10 +101,6 @@ impl Pasteboard { pub fn id(&self) -> DataTransferId { self.transfer_id } - - pub fn data_by_type(&self, type_: &PasteboardType) -> Option<()> { - todo!() - } } impl DataTransfer for Pasteboard { @@ -218,7 +199,7 @@ impl TypedData for PasteboardValue { } } - fn try_read(&mut self) -> Option> { + fn try_read(&mut self) -> Option> { struct DataReader { inner: Retained, offset: usize, @@ -260,9 +241,9 @@ impl TypedData for PasteboardValue { .map(|data| Box::new(DataReader::new(data)) as _) } - fn try_as_uris(&mut self) -> Option> { + fn try_as_uris(&mut self) -> io::Result> { if self.type_().hint() != Some(TypeHint::UriList) { - return None; + return Err(io::ErrorKind::InvalidData.into()); } let Some(items) = self.inner.pasteboardItems() else { @@ -273,7 +254,12 @@ impl TypedData for PasteboardValue { .propertyListForType(unsafe { objc2_app_kit::NSFilenamesPboardType }) { Some(property_list) => property_list, - None => return self.single_file_url().map(|str| vec![str]), + None => { + return self + .single_file_url() + .map(|str| vec![str]) + .ok_or_else(|| io::ErrorKind::InvalidData.into()); + }, }; let paths = property_list @@ -283,64 +269,63 @@ impl TypedData for PasteboardValue { .map(|file| file.downcast::().unwrap().to_string()) .collect(); - return Some(paths); + return Ok(paths); }; - Some( - items - .into_iter() - .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL })) - .map(|ns_str| ns_str.to_string()) - .collect(), - ) + Ok(items + .into_iter() + .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL })) + .map(|ns_str| ns_str.to_string()) + .collect()) } - fn try_as_string(&mut self) -> Option { - self.inner.stringForType(self.type_.pasteboard_type()?).map(|ns_str| ns_str.to_string()) + fn try_as_string(&mut self) -> io::Result { + self.inner + .stringForType(self.type_.pasteboard_type().ok_or(io::ErrorKind::InvalidData)?) + .map(|ns_str| ns_str.to_string()) + .ok_or_else(|| io::ErrorKind::InvalidData.into()) + } + + fn wait_for_data(&self) -> io::Result<()> { + // The methods on `NSPasteboard` already wait without danger of deadlock + Ok(()) } } #[derive(Debug, Default)] pub struct DndState { - inner: RefCell>, - serial_to_id: RefCell>, + inner: RefCell>>, } impl DndState { - pub fn set_pasteboard(&self, id: DataTransferId, pb: Retained) { + pub fn remove_deloaded_pasteboards(&self) { + self.inner.borrow_mut().retain(|_, v| v.load().is_some()); + } + + /// If the data transfer exists, update the pasteboard it points to. + pub fn set_pasteboard(&self, id: DataTransferId, pb: &Retained) { let mut inner = self.inner.borrow_mut(); if let Some(state) = inner.get_mut(&id) { - state.inner = pb; + *state = Weak::from_retained(pb); } } - pub fn insert

(&self, pb: P) -> DataTransferId - where - P: Into, - { - let value = pb.into(); - let id = value.id(); - - self.inner.borrow_mut().insert(id, value); + pub fn insert(&self, pb: &Retained) -> DataTransferId { + static TRANSFER_ID: AtomicI64 = AtomicI64::new(0); - id - } + let id = TRANSFER_ID.fetch_add(1, Ordering::Relaxed); + let id = DataTransferId::from_raw(id); - pub fn remove(&self, id: DataTransferId) { - if self.inner.borrow_mut().remove(&id).is_none() { - return; - } + self.inner.borrow_mut().insert(id, Weak::from_retained(pb)); - self.serial_to_id.borrow_mut().retain(|_, value| value.inner.id() != id); + id } pub fn get(&self, id: DataTransferId) -> Option { - self.inner.borrow().get(&id).cloned() - } - - pub fn fetch_type(&self, type_: &dyn TransferType) -> AsyncRequestSerial { - let out = AsyncRequestSerial::get(); - // TODO: Remove `DataTransferResult` - todo!() + self.inner + .borrow() + .get(&id) + .and_then(|weak| weak.load()) + .map(|pb| Pasteboard { transfer_id: id, inner: pb }) } } diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 30544ca9cc..a6536c166e 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -21,7 +21,7 @@ use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, Type use winit_core::error::{EventLoopError, RequestError}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, EventLoopProxy as CoreEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; @@ -132,7 +132,7 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, id: DataTransferId, type_: &dyn TransferType, - ) -> Result { + ) -> Result, RequestError> { let Some(pb) = self.app_state.dnd().get(id) else { return Err(RequestError::Ignored); }; @@ -141,20 +141,9 @@ impl RootActiveEventLoop for ActiveEventLoop { return Err(RequestError::Ignored); } - // TODO: We can get rid of `DataTransferResult` entirely Ok(todo!()) } - fn data_transfer_result( - &self, - serial: AsyncRequestSerial, - ) -> Result, RequestError> { - // TODO: We can get rid of `DataTransferResult` entirely, so this API should be redesigned - // to return a `TypedData` immediately from `fetch_data_transfer`. - - todo!() - } - fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { todo!() } diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index e1bde30581..40a362d83f 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -342,19 +342,13 @@ impl CoreWindow for Window { } fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - todo!() - } - - fn accept_drag_type( - &self, - id: DataTransferId, - type_: &dyn TransferType, - ) -> Result<(), UnknownDataTransfer> { - todo!() + self.maybe_wait_on_main(|delegate| delegate.set_drag_accepted(id, true)) + .map_err(|()| UnknownDataTransfer(id)) } fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - todo!() + self.maybe_wait_on_main(|delegate| delegate.set_drag_accepted(id, false)) + .map_err(|()| UnknownDataTransfer(id)) } } diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 2a291dd3be..0d924ad0e9 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -61,15 +61,20 @@ use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_di use super::util::cgerr; use super::view::WinitView; use super::window::{WinitPanel, WinitWindow, window_id}; -use crate::dnd::Pasteboard; use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS}; +#[derive(Copy, Clone, Debug)] +struct DragState { + id: DataTransferId, + accepted: bool, +} + #[derive(Debug)] pub(crate) struct State { /// Strong reference to the global application state. app_state: Rc, - drag_state: Cell>, + drag_state: Cell>, window: Retained, @@ -376,14 +381,14 @@ define_class!( let vars = self.ivars(); - let transfer_id = vars.app_state.dnd().insert(pb); + let transfer_id = vars.app_state.dnd().insert(&pb); self.queue_event(WindowEvent::DragEntered { id: transfer_id, position: Some(position), }); - vars.drag_state.set(Some(transfer_id)); - true + vars.drag_state.set(Some(DragState { id: transfer_id, accepted: false })); + false } #[unsafe(method(wantsPeriodicDraggingUpdates))] @@ -400,13 +405,13 @@ define_class!( let vars = self.ivars(); - let Some(transfer_id) = vars.drag_state.get() else { + let Some(DragState { id: transfer_id, accepted }) = vars.drag_state.get() else { return false.into(); }; let pb = sender.draggingPasteboard(); - vars.app_state.dnd().set_pasteboard(transfer_id, pb); + vars.app_state.dnd().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); @@ -415,7 +420,7 @@ define_class!( self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); - true + accepted } /// Invoked when the image is released @@ -432,14 +437,14 @@ define_class!( let vars = self.ivars(); - let Some(transfer_id) = vars.drag_state.get() else { + let Some(DragState { id: transfer_id, accepted }) = vars.drag_state.get() else { return false.into(); }; let pb = sender.draggingPasteboard(); let transfer_id = transfer_id; - vars.app_state.dnd().set_pasteboard(transfer_id, pb); + vars.app_state.dnd().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); @@ -449,7 +454,7 @@ define_class!( self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); self.queue_event(WindowEvent::DragDropped { id: transfer_id }); - true + accepted } /// Invoked when the dragging operation is complete @@ -458,10 +463,8 @@ define_class!( let _entered = debug_span!("concludeDragOperation:").entered(); let vars = self.ivars(); - if let Some(transfer_id) = vars.drag_state.get() { - vars.app_state.dnd().remove(transfer_id); - vars.drag_state.set(None); - } + vars.app_state.dnd().remove_deloaded_pasteboards(); + vars.drag_state.set(None); } /// Invoked when the dragging operation is cancelled @@ -470,13 +473,13 @@ define_class!( let _entered = debug_span!("draggingExited:").entered(); let vars = self.ivars(); - let Some(transfer_id) = vars.drag_state.get() else { + let Some(DragState { id: transfer_id, accepted: _ }) = vars.drag_state.get() else { return; }; if let Some(sender) = sender { let pb = sender.draggingPasteboard(); - vars.app_state.dnd().set_pasteboard(transfer_id, pb); + vars.app_state.dnd().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); @@ -488,10 +491,7 @@ define_class!( self.queue_event(WindowEvent::DragLeft { id: transfer_id }); - if let Some(transfer_id) = vars.drag_state.get() { - vars.app_state.dnd().remove(transfer_id); - vars.drag_state.set(None); - } + vars.drag_state.set(None); } } @@ -909,6 +909,26 @@ impl WindowDelegate { Ok(delegate) } + // TODO: Proper error + pub(super) fn set_drag_accepted( + &self, + transfer_id: DataTransferId, + accepted: bool, + ) -> Result<(), ()> { + let vars = self.ivars(); + let Some(DragState { id, accepted: _ }) = vars.drag_state.get() else { + return Err(()); + }; + + if id != transfer_id { + return Err(()); + } + + vars.drag_state.set(Some(DragState { id, accepted })); + + Ok(()) + } + #[track_caller] pub(super) fn view(&self) -> Retained { // The view inside WinitWindow should always be set and be `WinitView`. diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index d4c2bf139f..3ab2fb9c73 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -144,7 +144,7 @@ pub trait TypedData: AsAny + fmt::Debug { fn type_(&self) -> &dyn TransferType; /// If this value is readable as bytes, return a reader than can be used to read those bytes. - fn try_read(&mut self) -> Option>; + fn try_read(&mut self) -> Option>; /// Read this value as a list of URIs. /// diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index ae253c62ac..fa76a34f00 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -218,7 +218,7 @@ impl SelectionReader { } impl TypedData for SelectionReader { - fn try_read(&mut self) -> Option> { + fn try_read(&mut self) -> Option> { Some(Box::new(self.clone())) } From 1c5a76aec9c9838e36589bdec824b256e79ddec7 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 15:11:29 +0200 Subject: [PATCH 24/87] Fix `registerForDraggedTypes` on macOS --- winit-appkit/src/dnd.rs | 14 ++++++++++++-- winit-appkit/src/event_loop.rs | 12 ++++++------ winit-appkit/src/window_delegate.rs | 26 ++++++++++++++++++++------ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 44f9b0658e..e50e2fa587 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -101,6 +101,17 @@ impl Pasteboard { pub fn id(&self) -> DataTransferId { self.transfer_id } + + pub fn with_type(&self, dyn_type: &dyn TransferType) -> Option { + if self.has_type(dyn_type) { + Some(PasteboardValue { + type_: PasteboardTypeSpec::from_dyn(dyn_type)?, + inner: self.clone(), + }) + } else { + None + } + } } impl DataTransfer for Pasteboard { @@ -189,8 +200,6 @@ impl Deref for PasteboardValue { } } -static NO_TYPE_HINT: Option = None; - impl TypedData for PasteboardValue { fn type_(&self) -> &dyn TransferType { match &self.type_ { @@ -242,6 +251,7 @@ impl TypedData for PasteboardValue { } fn try_as_uris(&mut self) -> io::Result> { + // TODO: We should probably use `readObjects`, need to check how that works. if self.type_().hint() != Some(TypeHint::UriList) { return Err(io::ErrorKind::InvalidData.into()); } diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index a6536c166e..f66caf8541 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -137,15 +137,15 @@ impl RootActiveEventLoop for ActiveEventLoop { return Err(RequestError::Ignored); }; - if !pb.has_type(type_) { - return Err(RequestError::Ignored); - } - - Ok(todo!()) + pb.with_type(type_).map(|value| Box::new(value) as _).ok_or(RequestError::Ignored) } fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { - todo!() + let Some(pb) = self.app_state.dnd().get(id) else { + return Err(RequestError::Ignored); + }; + + Ok(Box::new(pb)) } } diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 0d924ad0e9..3403ebd263 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -19,11 +19,12 @@ use objc2::{ use objc2_app_kit::{ NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, - NSColor, NSDraggingDestination, NSDraggingInfo, NSRequestUserAttentionType, NSScreen, - NSToolbar, NSView, NSViewFrameDidChangeNotification, NSWindow, NSWindowButton, - NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode, - NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility, - NSWindowToolbarStyle, + NSColor, NSDraggingDestination, NSDraggingInfo, NSPasteboardTypeColor, NSPasteboardTypeFileURL, + NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, + NSPasteboardTypeTIFF, NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, + NSViewFrameDidChangeNotification, NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, + NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, + NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle, }; #[allow(deprecated)] use objc2_app_kit::{NSFilenamesPboardType, NSWindowFullScreenButton}; @@ -382,12 +383,13 @@ define_class!( let vars = self.ivars(); let transfer_id = vars.app_state.dnd().insert(&pb); + vars.drag_state.set(Some(DragState { id: transfer_id, accepted: false })); + self.queue_event(WindowEvent::DragEntered { id: transfer_id, position: Some(position), }); - vars.drag_state.set(Some(DragState { id: transfer_id, accepted: false })); false } @@ -860,6 +862,18 @@ impl WindowDelegate { window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + let drag_types = unsafe { + NSArray::from_slice(&[ + NSPasteboardTypeFileURL, + NSPasteboardTypeHTML, + NSPasteboardTypePNG, + NSPasteboardTypeSound, + NSPasteboardTypeString, + NSPasteboardTypeTIFF, + ]) + }; + window.registerForDraggedTypes(&drag_types); + // Listen for theme change event. // // SAFETY: The observer is un-registered in the `Drop` of the delegate. From 0e6bbf641cf067b7c7975700e4319ec2f48b58f0 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 15:19:53 +0200 Subject: [PATCH 25/87] Fix clippy on macOS --- winit-appkit/src/dnd.rs | 16 ++++++++-------- winit-appkit/src/lib.rs | 1 + winit-appkit/src/window.rs | 2 +- winit-appkit/src/window_delegate.rs | 13 ++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index e50e2fa587..4997daf89c 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -12,8 +12,8 @@ use objc2_app_kit::{ }; use objc2_foundation::{NSArray, NSData, NSString}; use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; -use winit_core::event_loop::AsyncRequestSerial; +/// A thin wrapper around [`NSPasteboardType`], implementing [`TransferType`]. #[derive(Debug, Clone)] pub struct PasteboardType { hint: Option, @@ -48,8 +48,6 @@ impl Deref for PasteboardType { } } -pub struct InvalidPasteboardTypeError(pub TypeHint); - impl From> for PasteboardType { fn from(value: Retained) -> Self { let pasteboard_type_to_hint = unsafe { @@ -79,6 +77,7 @@ impl TransferType for PasteboardType { } } +/// A thin wrapper around [`NSPasteboard`], implementing [`DataTransfer`]. #[derive(Clone, Debug)] pub struct Pasteboard { transfer_id: DataTransferId, @@ -94,15 +93,15 @@ impl Deref for Pasteboard { } impl Pasteboard { - pub(crate) fn set_pasteboard(&mut self, pasteboard: Retained) { - self.inner = pasteboard; - } - + /// Get the `DataTransferId` of this pasteboard. pub fn id(&self) -> DataTransferId { self.transfer_id } - pub fn with_type(&self, dyn_type: &dyn TransferType) -> Option { + /// Get a typed reader for this pasteboard. This is only necessary in the cross-platform case, + /// as a user downcasting to the platform-specific type can just access the `NSPasteboard` + /// directly. + pub(crate) fn with_type(&self, dyn_type: &dyn TransferType) -> Option { if self.has_type(dyn_type) { Some(PasteboardValue { type_: PasteboardTypeSpec::from_dyn(dyn_type)?, @@ -175,6 +174,7 @@ impl PasteboardTypeSpec { } } +/// A thin wrapper around [`NSPasteboard`], implementing [`TypedValue`]. #[derive(Debug)] pub struct PasteboardValue { // The concept of "top-level" types for a pasteboard doesn't always make sense on macOS due to diff --git a/winit-appkit/src/lib.rs b/winit-appkit/src/lib.rs index 96c1c4f57d..8548f8f738 100644 --- a/winit-appkit/src/lib.rs +++ b/winit-appkit/src/lib.rs @@ -92,6 +92,7 @@ use winit_core::event_loop::ActiveEventLoop; use winit_core::monitor::MonitorHandle; use winit_core::window::{PlatformWindowAttributes, Window}; +pub use self::dnd::{Pasteboard, PasteboardType, PasteboardValue}; pub use self::event::{physicalkey_to_scancode, scancode_to_physicalkey}; use self::event_loop::ActiveEventLoop as AppKitActiveEventLoop; pub use self::event_loop::{EventLoop, PlatformSpecificEventLoopAttributes}; diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index 40a362d83f..dcaed949ca 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -10,7 +10,7 @@ use objc2_app_kit::{NSPanel, NSResponder, NSWindow}; use objc2_foundation::NSObject; use tracing::trace_span; use winit_core::cursor::Cursor; -use winit_core::data_transfer::{DataTransferId, TransferType}; +use winit_core::data_transfer::DataTransferId; use winit_core::error::RequestError; use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 3403ebd263..0da107a69c 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -19,12 +19,12 @@ use objc2::{ use objc2_app_kit::{ NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, - NSColor, NSDraggingDestination, NSDraggingInfo, NSPasteboardTypeColor, NSPasteboardTypeFileURL, - NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, - NSPasteboardTypeTIFF, NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, - NSViewFrameDidChangeNotification, NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, - NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, - NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle, + NSColor, NSDraggingDestination, NSDraggingInfo, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, + NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, + NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification, + NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, + NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, + NSWindowTitleVisibility, NSWindowToolbarStyle, }; #[allow(deprecated)] use objc2_app_kit::{NSFilenamesPboardType, NSWindowFullScreenButton}; @@ -445,7 +445,6 @@ define_class!( let pb = sender.draggingPasteboard(); - let transfer_id = transfer_id; vars.app_state.dnd().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); From 9a058cbba97dd76565028961c85f3948dd4a0cdd Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 15:29:21 +0200 Subject: [PATCH 26/87] Fix clippy --- winit/examples/child_window.rs | 2 +- winit/examples/dnd.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/winit/examples/child_window.rs b/winit/examples/child_window.rs index 4077bca36b..d1427c2c62 100644 --- a/winit/examples/child_window.rs +++ b/winit/examples/child_window.rs @@ -56,7 +56,7 @@ fn main() -> Result<(), impl std::error::Error> { self.windows.clear(); event_loop.exit(); }, - WindowEvent::PointerEntered { device_id: _, .. } => { + WindowEvent::PointerEntered { .. } => { // On x11, log when the cursor entered in a window even if the child window // is created by some key inputs. // the child windows are always placed at (0, 0) with size (200, 200) in the diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 4ae70dca37..5fb4889a38 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -1,6 +1,6 @@ use std::error::Error; -use tracing::{error, info}; +use tracing::{error, info, warn}; use winit::application::ApplicationHandler; use winit::data_transfer::{TypeHint, TypedData}; use winit::event::WindowEvent; @@ -123,7 +123,7 @@ impl ApplicationHandler for Application { match self.last_dnd_fetch.as_ref().unwrap().wait_for_data() { Err(e) if e.kind() == std::io::ErrorKind::Deadlock => { - eprintln!("Immediately waiting for a fetched data transfer may deadlock!"); + warn!("Immediately waiting for a fetched data transfer may deadlock!"); }, _ => {}, } From fbccd661b0033403dfb9a1b578e9c4b2a3568676 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 15:40:10 +0200 Subject: [PATCH 27/87] Fix 1.85 compat --- Cargo.toml | 1 + winit-x11/Cargo.toml | 1 + winit-x11/src/dnd.rs | 22 +++++++++++++++++----- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3321df958d..e8e3547077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"] } serde = { version = "1", features = ["serde_derive"] } smol_str = "0.3" tracing = { version = "0.1.40", default-features = false } +rustversion = "1.0" # Dev dependencies. image = { version = "0.25.0", default-features = false } diff --git a/winit-x11/Cargo.toml b/winit-x11/Cargo.toml index 519904a268..2492476a2a 100644 --- a/winit-x11/Cargo.toml +++ b/winit-x11/Cargo.toml @@ -20,6 +20,7 @@ serde = { workspace = true, optional = true } smol_str.workspace = true tracing.workspace = true winit-core.workspace = true +rustversion.workspace = true # Platform-specific bytemuck.workspace = true diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index fa76a34f00..e88dfe9c00 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -110,6 +110,18 @@ impl SharedDataReader { self.reader.try_data() } + #[rustversion::since(1.86)] + fn wait_internal(&self) -> io::Result<()> { + let _ = self.reader.data.wait(); + + Ok(()) + } + + #[rustversion::before(1.86)] + fn wait_internal(&self) -> io::Result<()> { + if self.reader.has_data() { Ok(()) } else { Err(io::ErrorKind::WouldBlock.into()) } + } + fn wait_for_data(&self) -> io::Result<()> { if !self.reader.has_data() && self.deadlock_sentinel.get() == Some(std::thread::current().id()) @@ -117,9 +129,7 @@ impl SharedDataReader { return Err(io::ErrorKind::Deadlock.into()); } - let _ = self.reader.data.wait(); - - Ok(()) + self.wait_internal() } } @@ -258,10 +268,12 @@ impl TypedData for SelectionReader { decode_utf16_bytes(data) // Even if we guess that it's utf-16, we'll still try utf-8 just in case .or_else(|_| { - str::from_utf8(data).map(|str| str.to_owned()).map_err(invalid_data) + std::str::from_utf8(data) + .map(|str| str.to_owned()) + .map_err(invalid_data) }) } else { - str::from_utf8(data) + std::str::from_utf8(data) .map(|str| str.to_owned()) .map_err(invalid_data) .or_else(|_| decode_utf16_bytes(data)) From d4b9c49f04524f15121e7e03b6d472b3d3643c0e Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 15:42:51 +0200 Subject: [PATCH 28/87] Taplo fmt --- Cargo.toml | 2 +- winit-x11/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e8e3547077..eee26d23bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,11 +32,11 @@ cursor-icon = "1.1.0" dpi = { version = "0.1.2", path = "dpi" } keyboard-types = "0.8.0" mint = "0.5.6" +rustversion = "1.0" rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"] } serde = { version = "1", features = ["serde_derive"] } smol_str = "0.3" tracing = { version = "0.1.40", default-features = false } -rustversion = "1.0" # Dev dependencies. image = { version = "0.25.0", default-features = false } diff --git a/winit-x11/Cargo.toml b/winit-x11/Cargo.toml index 2492476a2a..9f8fa096fd 100644 --- a/winit-x11/Cargo.toml +++ b/winit-x11/Cargo.toml @@ -15,12 +15,12 @@ serde = ["dep:serde", "bitflags/serde", "smol_str/serde", "dpi/serde"] bitflags.workspace = true cursor-icon.workspace = true dpi.workspace = true +rustversion.workspace = true rwh_06.workspace = true serde = { workspace = true, optional = true } smol_str.workspace = true tracing.workspace = true winit-core.workspace = true -rustversion.workspace = true # Platform-specific bytemuck.workspace = true From d00d415f7b6da06ebf102ff6ad360f88c3739fa0 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 15:49:12 +0200 Subject: [PATCH 29/87] X11 clippy --- winit-x11/src/atoms.rs | 2 +- winit-x11/src/dnd.rs | 15 ++++----------- winit-x11/src/event_processor.rs | 4 ++-- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/winit-x11/src/atoms.rs b/winit-x11/src/atoms.rs index 52972bf4da..f9876cf96f 100644 --- a/winit-x11/src/atoms.rs +++ b/winit-x11/src/atoms.rs @@ -13,7 +13,7 @@ macro_rules! atom_manager { /// Indices into the `Atoms` struct. #[derive(Copy, Clone, Debug)] - #[allow(non_camel_case_types)] + #[allow(non_camel_case_types, clippy::upper_case_acronyms)] pub enum AtomName { $($name,)* } diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index e88dfe9c00..5f2fa6a300 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -217,7 +217,7 @@ impl SelectionReader { { let data = self.data.try_data()?; - let mut cursor = io::Cursor::new(&data[..]); + let mut cursor = io::Cursor::new(data); cursor.set_position(self.pos); let result = func(&mut cursor)?; let new_pos = cursor.position(); @@ -247,7 +247,6 @@ impl TypedData for SelectionReader { fn decode_utf16_bytes(bytes: &[u8]) -> io::Result { let utf16 = bytes .chunks_exact(2) - .into_iter() .map(|chunk| { let bytes: &[u8; 2] = chunk.try_into().unwrap(); u16::from_ne_bytes(*bytes) @@ -295,7 +294,7 @@ impl TypedData for SelectionReader { Ok(self .try_as_string()? - .split(|c| c == '\n' || c == '\r') + .split(['\n', '\r']) .filter(|s| !s.is_empty()) .map(Into::into) .collect()) @@ -358,7 +357,7 @@ impl Selection { } } -#[derive(Clone, Debug, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SelectionType { hint: Option, atom: xproto::Atom, @@ -415,15 +414,9 @@ impl SelectionType { } } -impl PartialEq for SelectionType { - fn eq(&self, other: &Self) -> bool { - self.atom == other.atom - } -} - impl TransferType for SelectionType { fn hint(&self) -> Option { - self.hint.clone() + self.hint } } diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 7902cb8d90..1a5b47ea28 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -451,7 +451,7 @@ impl EventProcessor { xev.data.get_long(3) as xproto::Atom, xev.data.get_long(4) as xproto::Atom, ] - .map(|ty_atom| SelectionType::new(atoms, ty_atom).into()) + .map(|ty_atom| SelectionType::new(atoms, ty_atom)) .into_iter() .collect(); dnd.types = Some(type_list); @@ -459,7 +459,7 @@ impl EventProcessor { dnd.types = Some( more_types .into_iter() - .map(|ty_atom| SelectionType::new(atoms, ty_atom).into()) + .map(|ty_atom| SelectionType::new(atoms, ty_atom)) .collect(), ); } From e52cae1a1c41eb13d98ebbd4031f2b2208e5fad7 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 26 May 2026 15:52:21 +0200 Subject: [PATCH 30/87] Explain `regsisterForDraggedTypes` usage --- winit-appkit/src/window_delegate.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 0da107a69c..a2a076723a 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -779,10 +779,6 @@ fn new_window( window.setBackgroundColor(Some(&NSColor::clearColor())); } - // register for drag and drop operations. - #[allow(deprecated)] - window.registerForDraggedTypes(&NSArray::from_slice(&[unsafe { NSFilenamesPboardType }])); - Some(window) }) } @@ -862,6 +858,10 @@ impl WindowDelegate { window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); let drag_types = unsafe { + // Advertize support for the set of types which correspond to variants of `TypeHint`. + // If the user wants to support other pasteboard types which don't have a cross-platform + // equivalent, they can downcast the window and manually call `registerForDraggedTypes` + // themselves. NSArray::from_slice(&[ NSPasteboardTypeFileURL, NSPasteboardTypeHTML, From 47dd3cf4c1d316ece653155850f4e66b178debbe Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 09:43:17 +0200 Subject: [PATCH 31/87] Fix clippy issue --- winit-appkit/src/dnd.rs | 1 + winit-appkit/src/window_delegate.rs | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 4997daf89c..9e0d841fbe 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -52,6 +52,7 @@ impl From> for PasteboardType { fn from(value: Retained) -> Self { let pasteboard_type_to_hint = unsafe { [ + // Just in case the source application uses the deprecated method, we handle it here #[expect(deprecated)] (objc2_app_kit::NSFilenamesPboardType, TypeHint::UriList), (NSPasteboardTypeFileURL, TypeHint::UriList), diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index a2a076723a..9c06670684 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -26,8 +26,6 @@ use objc2_app_kit::{ NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle, }; -#[allow(deprecated)] -use objc2_app_kit::{NSFilenamesPboardType, NSWindowFullScreenButton}; use objc2_core_foundation::{CGFloat, CGPoint}; use objc2_core_graphics::{ CGAcquireDisplayFadeReservation, CGAssociateMouseAndMouseCursorPosition, CGDisplayCapture, @@ -697,7 +695,7 @@ fn new_window( if macos_attrs.titlebar_buttons_hidden { for titlebar_button in &[ #[allow(deprecated)] - NSWindowFullScreenButton, + objc2_app_kit::NSWindowFullScreenButton, NSWindowButton::MiniaturizeButton, NSWindowButton::CloseButton, NSWindowButton::ZoomButton, From 03eb54a7e23e908fd34a6362ef9c77da916c087a Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 10:00:58 +0200 Subject: [PATCH 32/87] Small changes --- winit-appkit/src/dnd.rs | 5 +++-- winit-core/src/data_transfer.rs | 30 ++++++++++++++++++++++++++++++ winit-x11/src/dnd.rs | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 9e0d841fbe..31cdd4ead5 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -35,7 +35,7 @@ impl PasteboardType { }; hint_to_pasteboard_type.into_iter().find_map(|(haystack, inner)| { - (haystack == hint).then(|| Self { hint: Some(hint), inner: inner.retain() }) + (haystack.matches(hint)).then(|| Self { hint: Some(hint), inner: inner.retain() }) }) } } @@ -52,7 +52,8 @@ impl From> for PasteboardType { fn from(value: Retained) -> Self { let pasteboard_type_to_hint = unsafe { [ - // Just in case the source application uses the deprecated method, we handle it here + // Just in case the source application uses the deprecated method, we handle it + // here #[expect(deprecated)] (objc2_app_kit::NSFilenamesPboardType, TypeHint::UriList), (NSPasteboardTypeFileURL, TypeHint::UriList), diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 3ab2fb9c73..1bc48862d7 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -102,6 +102,36 @@ pub enum TypeHint { }, } +impl TypeHint { + /// Check whether the two type hints "match". + /// + /// This is subtly different to direct equality. If one of the types is an image or audio with a + /// `None` extension hint, then the other type just needs to match variant (i.e. image/audio), + /// the extension does not also have to be `None`. + pub fn matches(&self, other: &Self) -> bool { + match (self, other) { + (Self::Plaintext, Self::Plaintext) + | (Self::UriList, Self::UriList) + | (Self::Html, Self::Html) + | (Self::Rtf, Self::Rtf) => true, + + ( + Self::Audio { extension_hint: this_ext }, + Self::Audio { extension_hint: other_ext }, + ) + | ( + Self::Image { extension_hint: this_ext }, + Self::Image { extension_hint: other_ext }, + ) => match (this_ext, other_ext) { + (Some(this_ext), Some(other_ext)) => this_ext == other_ext, + (None, _) | (_, None) => true, + }, + + _ => false, + } + } +} + /// The type of a data transfer. /// /// [`hint`](TransferType::hint) can be called to get the type in diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 5f2fa6a300..c56aeb7214 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -446,7 +446,7 @@ impl DataTransfer for Selection { return false; }; - types.iter().any(|haystack| haystack.hint() == Some(hint)) + types.iter().any(|haystack| haystack.hint().is_some_and(|hs| hs.matches(&hint))) } } } From a4394364b9bd27476b331fd71de9b61d80b45bd9 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 10:05:49 +0200 Subject: [PATCH 33/87] Fix compilation issue --- winit-appkit/src/dnd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 31cdd4ead5..629fa5d29e 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -35,7 +35,7 @@ impl PasteboardType { }; hint_to_pasteboard_type.into_iter().find_map(|(haystack, inner)| { - (haystack.matches(hint)).then(|| Self { hint: Some(hint), inner: inner.retain() }) + (haystack.matches(&hint)).then(|| Self { hint: Some(hint), inner: inner.retain() }) }) } } From 0c946500a2738242485fb2b67ffb070650ad3df3 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 11:40:15 +0200 Subject: [PATCH 34/87] WIP: Implement for Windows --- winit-win32/src/definitions.rs | 4 ++ winit-win32/src/drop_handler.rs | 110 ++++++++++++++++++++------------ 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index c5225cfee9..b9b4dae73f 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -8,9 +8,12 @@ use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM}; use windows_sys::core::{BOOL, GUID, HRESULT}; pub type IUnknown = *mut c_void; +#[expect(dead_code, reason = "TODO")] pub type IAdviseSink = *mut c_void; pub type IDataObject = *mut c_void; +#[expect(dead_code, reason = "TODO")] pub type IEnumFORMATETC = *mut c_void; +#[expect(dead_code, reason = "TODO")] pub type IEnumSTATDATA = *mut c_void; #[repr(C)] @@ -24,6 +27,7 @@ pub struct IUnknownVtbl { pub Release: unsafe extern "system" fn(This: *mut IUnknown) -> u32, } +#[expect(dead_code, reason = "TODO")] #[repr(C)] pub struct IDataObjectVtbl { pub parent: IUnknownVtbl, diff --git a/winit-win32/src/drop_handler.rs b/winit-win32/src/drop_handler.rs index 19a23c9520..fbb121bcad 100644 --- a/winit-win32/src/drop_handler.rs +++ b/winit-win32/src/drop_handler.rs @@ -2,16 +2,17 @@ use std::ffi::{OsString, c_void}; use std::os::windows::ffi::OsStringExt; use std::path::PathBuf; use std::ptr; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; use dpi::PhysicalPosition; use tracing::debug; -use windows_sys::Win32::Foundation::{DV_E_FORMATETC, HWND, POINT, POINTL, S_OK}; +use windows_sys::Win32::Foundation::{DV_E_FORMATETC, E_ABORT, HWND, POINT, POINTL, S_OK}; use windows_sys::Win32::Graphics::Gdi::ScreenToClient; use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}; use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE}; -use windows_sys::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP}; +use windows_sys::Win32::UI::Shell::{DragQueryFileW, HDROP}; use windows_sys::core::{GUID, HRESULT}; +use winit_core::data_transfer::DataTransferId; use winit_core::event::WindowEvent; use crate::definitions::{ @@ -24,9 +25,19 @@ pub struct FileDropHandlerData { refcount: AtomicUsize, window: HWND, send_event: Box, - cursor_effect: u32, - valid: bool, /* If the currently hovered item is not valid there must not be any - * `DragLeft` emitted */ + accepted: bool, + active_data_transfer_id: Option, +} + +impl FileDropHandlerData { + fn cursor_effect(&self) -> u32 { + if self.accepted { + // TODO: Handle other kinds of drop effect + DROPEFFECT_COPY + } else { + DROPEFFECT_NONE + } + } } pub struct FileDropHandler { @@ -41,12 +52,18 @@ impl FileDropHandler { refcount: AtomicUsize::new(1), window, send_event, - cursor_effect: DROPEFFECT_NONE, - valid: false, + active_data_transfer_id: None, + accepted: false, }); FileDropHandler { data: Box::into_raw(data) } } + #[expect(dead_code, reason = "TODO")] + pub(crate) fn set_accepted(&mut self, accepted: bool) { + let drop_handler_data = unsafe { Self::from_interface(self.data) }; + drop_handler_data.accepted = accepted; + } + // Implement IUnknown pub unsafe extern "system" fn QueryInterface( _this: *mut IUnknown, @@ -76,27 +93,28 @@ impl FileDropHandler { pub unsafe extern "system" fn DragEnter( this: *mut IDropTarget, - pDataObj: *const IDataObject, + _pDataObj: *const IDataObject, _grfKeyState: u32, pt: POINTL, pdwEffect: *mut u32, ) -> HRESULT { + static DATA_TRANSFER_ID: AtomicI64 = AtomicI64::new(0); + let drop_handler = unsafe { Self::from_interface(this) }; + let data_transfer_id = + DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)); + drop_handler.active_data_transfer_id = Some(data_transfer_id); let mut pt = POINT { x: pt.x, y: pt.y }; unsafe { ScreenToClient(drop_handler.window, &mut pt); } let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); - let mut paths = Vec::new(); - let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) }; - drop_handler.valid = hdrop.is_some(); - if drop_handler.valid { - (drop_handler.send_event)(WindowEvent::DragEntered { paths, position }); - } - drop_handler.cursor_effect = - if drop_handler.valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE }; + (drop_handler.send_event)(WindowEvent::DragEntered { + id: data_transfer_id, + position: Some(position), + }); unsafe { - *pdwEffect = drop_handler.cursor_effect; + *pdwEffect = DROPEFFECT_NONE; } S_OK @@ -109,16 +127,22 @@ impl FileDropHandler { pdwEffect: *mut u32, ) -> HRESULT { let drop_handler = unsafe { Self::from_interface(this) }; - if drop_handler.valid { - let mut pt = POINT { x: pt.x, y: pt.y }; + let Some(data_transfer_id) = drop_handler.active_data_transfer_id else { unsafe { - ScreenToClient(drop_handler.window, &mut pt); + *pdwEffect = DROPEFFECT_NONE; } - let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); - (drop_handler.send_event)(WindowEvent::DragMoved { position }); + + return E_ABORT; + }; + + let mut pt = POINT { x: pt.x, y: pt.y }; + unsafe { + ScreenToClient(drop_handler.window, &mut pt); } + let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); + (drop_handler.send_event)(WindowEvent::DragPosition { id: data_transfer_id, position }); unsafe { - *pdwEffect = drop_handler.cursor_effect; + *pdwEffect = drop_handler.cursor_effect(); } S_OK @@ -126,38 +150,41 @@ impl FileDropHandler { pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { let drop_handler = unsafe { Self::from_interface(this) }; - if drop_handler.valid { - (drop_handler.send_event)(WindowEvent::DragLeft { position: None }); - } + let Some(data_transfer_id) = drop_handler.active_data_transfer_id else { + return E_ABORT; + }; + + (drop_handler.send_event)(WindowEvent::DragLeft { id: data_transfer_id }); S_OK } pub unsafe extern "system" fn Drop( this: *mut IDropTarget, - pDataObj: *const IDataObject, + _pDataObj: *const IDataObject, _grfKeyState: u32, pt: POINTL, pdwEffect: *mut u32, ) -> HRESULT { let drop_handler = unsafe { Self::from_interface(this) }; - if drop_handler.valid { - let mut pt = POINT { x: pt.x, y: pt.y }; + let Some(data_transfer_id) = drop_handler.active_data_transfer_id else { unsafe { - ScreenToClient(drop_handler.window, &mut pt); - } - let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); - let mut paths = Vec::new(); - let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) }; - (drop_handler.send_event)(WindowEvent::DragDropped { paths, position }); - if let Some(hdrop) = hdrop { - unsafe { - DragFinish(hdrop); - } + *pdwEffect = DROPEFFECT_NONE; } + + return E_ABORT; + }; + + let mut pt = POINT { x: pt.x, y: pt.y }; + unsafe { + ScreenToClient(drop_handler.window, &mut pt); } + + let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); + (drop_handler.send_event)(WindowEvent::DragPosition { id: data_transfer_id, position }); + (drop_handler.send_event)(WindowEvent::DragDropped { id: data_transfer_id }); unsafe { - *pdwEffect = drop_handler.cursor_effect; + *pdwEffect = drop_handler.cursor_effect(); } S_OK @@ -167,6 +194,7 @@ impl FileDropHandler { unsafe { &mut *(this as *mut _) } } + #[expect(dead_code, reason = "TODO")] unsafe fn iterate_filenames(data_obj: *const IDataObject, mut callback: F) -> Option where F: FnMut(PathBuf), From fb92e3642a0cecacf29813ad6bbc11b34250c1be Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 16:33:42 +0200 Subject: [PATCH 35/87] X11 cleanup --- winit-x11/src/dnd.rs | 179 ++++++++++++++++--------------- winit-x11/src/event_loop.rs | 57 +++++----- winit-x11/src/event_processor.rs | 90 ++++++++-------- winit-x11/src/window.rs | 69 ++++-------- 4 files changed, 189 insertions(+), 206 deletions(-) diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index c56aeb7214..bb0be69214 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -3,6 +3,7 @@ use std::io; use std::marker::PhantomData; use std::os::raw::*; use std::str::Utf8Error; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::{Arc, OnceLock, RwLock}; use std::thread::ThreadId; @@ -326,34 +327,56 @@ impl SelectionFetchState { } } -#[derive(Debug)] -pub struct Dnd { - xconn: Arc, - transfer_id: DataTransferId, +#[derive(Default, Debug)] +pub struct DndSharedState { + transfer_id: AtomicI64, /// Whether the drag operation is accepted (or `None` if the user never indicated that it's /// accepted or rejected) // Populated by `Window::accept_drag`/`Window::reject_drag`. - pub accepted: Option, + pub accepted: AtomicBool, +} + +impl DndSharedState { + fn reset(&self) { + self.transfer_id.fetch_add(1, Ordering::Relaxed); + self.accepted.store(false, Ordering::Relaxed); + } + + pub fn transfer_id(&self) -> DataTransferId { + DataTransferId::from_raw(self.transfer_id.load(Ordering::Relaxed)) + } +} + +#[derive(Default, Debug)] +pub struct DragState { // Populated by XdndEnter event handler - pub version: Option, - pub types: Option>, + pub version: c_long, + pub types: Arc<[SelectionType]>, // Populated by Xdnd* event handlers - pub source_window: Option, + pub source_window: xproto::Window, // Populated by Xdnd* event handlers - pub target_window: Option, + pub target_window: xproto::Window, // Populated by `fetch_data_transfer` pub last_fetched_selection: Option, +} + +#[derive(Debug)] +pub struct Dnd { + xconn: Arc, + pub shared: Arc, pub deadlock_sentinel: DeadlockSentinel, + // If `None`, no drag operation is in progress. + pub state: Option, } #[derive(Debug)] pub struct Selection { - dnd: Arc>, + types: Arc<[SelectionType]>, } impl Selection { - pub(crate) fn new(dnd: Arc>) -> Selection { - Selection { dnd } + pub(crate) fn new(types: Arc<[SelectionType]>) -> Selection { + Selection { types } } } @@ -422,107 +445,68 @@ impl TransferType for SelectionType { impl DataTransfer for Selection { fn available_types(&self) -> Vec> { - self.dnd - .read() - .unwrap() - .types - .as_ref() - .into_iter() - .flat_map(|types| types.iter().map(|val| Box::new(val.clone()) as _)) - .collect() + self.types.iter().cloned().map(|val| Box::new(val) as _).collect() } fn has_type(&self, type_: &dyn TransferType) -> bool { - let dnd = self.dnd.read().unwrap(); - - let Some(types) = dnd.types.as_ref() else { - return false; - }; - if let Some(x11_type) = type_.cast_ref() { - types.iter().any(|haystack| haystack == x11_type) + self.types.iter().any(|haystack| haystack == x11_type) } else { let Some(hint) = type_.hint() else { return false; }; - types.iter().any(|haystack| haystack.hint().is_some_and(|hs| hs.matches(&hint))) + self.types.iter().any(|haystack| haystack.hint().is_some_and(|hs| hs.matches(&hint))) } } } impl Dnd { - pub fn new(xconn: Arc, sentinel: DeadlockSentinel) -> Self { - Self::with_id(xconn, sentinel, DataTransferId::from_raw(0)) + pub fn new(xconn: Arc, deadlock_sentinel: DeadlockSentinel) -> Self { + let shared = Arc::new(Default::default()); + + Dnd { xconn, shared, state: None, deadlock_sentinel } } pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> { - self.types.as_ref()?.iter().find(|haystack| haystack.hint() == Some(hint)) - } - - fn with_id( - xconn: Arc, - deadlock_sentinel: DeadlockSentinel, - transfer_id: DataTransferId, - ) -> Self { - Dnd { - xconn, - transfer_id, - accepted: None, - version: None, - types: None, - source_window: None, - target_window: None, - last_fetched_selection: None, - deadlock_sentinel, - } + self.state.as_ref()?.types.iter().find(|haystack| haystack.hint() == Some(hint)) } pub fn transfer_id(&self) -> DataTransferId { - self.transfer_id + DataTransferId::from_raw(self.shared.transfer_id.load(Ordering::Relaxed)) } pub fn reset(&mut self) { - let xconn = self.xconn.clone(); - let sentinel = std::mem::take(&mut self.deadlock_sentinel); - let new_id = DataTransferId::from_raw(self.transfer_id.into_raw().wrapping_add(1)); - *self = Self::with_id(xconn, sentinel, new_id); + self.shared.reset(); + self.state = None; } - pub unsafe fn send_status( - &self, - this_window: xproto::Window, + pub fn init_state( + &mut self, + version: c_long, + source_window: xproto::Window, target_window: xproto::Window, - state: DndState, - ) -> Result<(), X11Error> { - let atoms = self.xconn.atoms(); - let (accepted, action) = match state { - DndState::Accepted => (1, atoms[XdndActionPrivate]), - DndState::Rejected => (0, atoms[DndNone]), - }; - self.xconn - .send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [ - this_window, - accepted, - 0, - 0, - action as _, - ])? - .ignore_error(); - - Ok(()) + types: Arc<[SelectionType]>, + ) -> &DragState { + self.state.get_or_insert(DragState { + version, + types, + source_window, + target_window, + last_fetched_selection: None, + }) } pub unsafe fn send_finished( &self, this_window: xproto::Window, target_window: xproto::Window, - state: DndState, ) -> Result<(), X11Error> { let atoms = self.xconn.atoms(); - let (accepted, action) = match state { - DndState::Accepted => (1, atoms[XdndActionPrivate]), - DndState::Rejected => (0, atoms[DndNone]), + let (accepted, action) = if self.shared.accepted.load(Ordering::Relaxed) { + (1, atoms[XdndActionPrivate]) + } else { + (0, atoms[DndNone]) }; self.xconn .send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [ @@ -564,20 +548,41 @@ impl Dnd { .expect_then_ignore_error("Failed to send XdndSelection event") } + pub unsafe fn send_status( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + status: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match status { + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [ + this_window, + accepted, + 0, + 0, + action as _, + ])? + .ignore_error(); + + Ok(()) + } pub unsafe fn read_data(&self, window: xproto::Window) -> Result<(), util::GetPropertyError> { + let state = self.state.as_ref().ok_or(util::GetPropertyError::Unknown)?; + // Never fetched - let data = - self.last_fetched_selection.as_ref().ok_or_else(|| util::GetPropertyError::Unknown)?; + let last_fetch = + state.last_fetched_selection.as_ref().ok_or(util::GetPropertyError::Unknown)?; let atoms = self.xconn.atoms(); - let type_ = self - .last_fetched_selection - .as_ref() - .map(|state| state.type_.atom()) - .ok_or(util::GetPropertyError::Unknown)?; + let type_ = last_fetch.type_.atom(); let bytes = self.xconn.get_property(window, atoms[XdndSelection], type_)?; - data.value.write(bytes.into()).map_err(|_| util::GetPropertyError::Unknown)?; + last_fetch.value.write(bytes.into()).map_err(|_| util::GetPropertyError::Unknown)?; Ok(()) } diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 6c5a276688..4538b6c3f8 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -7,7 +7,7 @@ use std::os::raw::*; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; -use std::sync::{Arc, LazyLock, Mutex, RwLock, Weak}; +use std::sync::{Arc, LazyLock, Mutex, Weak}; use std::time::{Duration, Instant}; use std::{fmt, mem, ptr, slice, str}; @@ -169,7 +169,7 @@ impl PeekableReceiver { #[derive(Debug)] pub struct ActiveEventLoop { pub(crate) xconn: Arc, - pub(crate) dnd: Arc>, + pub(crate) dnd: RefCell, pub(crate) wm_delete_window: xproto::Atom, pub(crate) net_wm_ping: xproto::Atom, pub(crate) net_wm_sync_request: xproto::Atom, @@ -228,8 +228,7 @@ impl EventLoop { let net_wm_ping = atoms[_NET_WM_PING]; let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST]; - let dnd = Dnd::new(Arc::clone(&xconn), Default::default()); - let dnd = Arc::new(RwLock::new(dnd)); + let dnd = Dnd::new(Arc::clone(&xconn), Default::default()).into(); let (ime_sender, ime_receiver) = mpsc::channel(); let (ime_event_sender, ime_event_receiver) = mpsc::channel(); @@ -667,7 +666,7 @@ impl ActiveEventLoop { } pub(crate) fn selection_deadlock_guard(&self) -> DeadlockSentinelGuard { - self.dnd.read().unwrap().deadlock_sentinel.guard() + self.dnd.borrow().deadlock_sentinel.guard() } /// Update the device event based on window focus. @@ -768,11 +767,17 @@ impl RootActiveEventLoop for ActiveEventLoop { } fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { - if self.dnd.read().unwrap().transfer_id() != id { + let dnd = self.dnd.borrow(); + + if dnd.transfer_id() != id { return Err(RequestError::Ignored); } - Ok(Box::new(Selection::new(self.dnd.clone()))) + let Some(state) = dnd.state.as_ref() else { + return Err(RequestError::Ignored); + }; + + Ok(Box::new(Selection::new(state.types.clone()))) } fn fetch_data_transfer( @@ -780,48 +785,50 @@ impl RootActiveEventLoop for ActiveEventLoop { id: DataTransferId, type_: &dyn TransferType, ) -> Result, RequestError> { - let mut dnd = self.dnd.write().unwrap(); + let mut dnd = self.dnd.borrow_mut(); + if dnd.transfer_id() != id { return Err(RequestError::NotSupported(NotSupportedError::new( "Unknown data transfer", ))); } - if dnd.source_window.is_none() { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Unknown source window", - ))); - } - - let Some(window) = dnd.target_window else { - return Err(RequestError::NotSupported(NotSupportedError::new( - "Unknown target window", - ))); - }; - let type_ = type_ .cast_ref::() .or_else(|| dnd.find_type_by_hint(type_.hint()?)) .cloned() .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; + let deadlock_sentinel = dnd.deadlock_sentinel.reader(); - let mut new_state = - dnd.last_fetched_selection.take().filter(|state| state.type_() == &type_); + let mut new_state = dnd + .state + .as_mut() + .ok_or(RequestError::Ignored)? + .last_fetched_selection + .take() + .filter(|state| state.type_() == &type_); + + let Some(target_window) = dnd.state.as_ref().map(|s| s.target_window) else { + return Err(RequestError::Ignored); + }; - let deadlock_sentinel = dnd.deadlock_sentinel.reader(); let reader = new_state .get_or_insert_with(|| { // This results in the `SelectionNotify` event unsafe { // TODO: Handle this better - dnd.convert_selection(window, self.xconn.timestamp(), type_.atom()); + dnd.convert_selection(target_window, self.xconn.timestamp(), type_.atom()); } SelectionFetchState::new(type_) }) .as_reader(deadlock_sentinel); - dnd.last_fetched_selection = new_state; + let Some(state) = dnd.state.as_mut() else { + return Err(RequestError::Ignored); + }; + + state.last_fetched_selection = new_state; Ok(Box::new(reader)) } diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 1a5b47ea28..62d5bfb157 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, VecDeque}; use std::mem::MaybeUninit; use std::os::raw::{c_char, c_int, c_long, c_ulong}; use std::slice; +use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; use dpi::{PhysicalPosition, PhysicalSize}; @@ -431,7 +432,7 @@ impl EventProcessor { // Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event` // never contending the lock. let transfer_id = { - let mut dnd = self.target.dnd.write().unwrap(); + let mut dnd = self.target.dnd.borrow_mut(); // We only reset when a new drag-and-drop enters, to maximize the amount of time // that the drag data can be accessed. dnd.reset(); @@ -440,30 +441,27 @@ impl EventProcessor { let flags = xev.data.get_long(1); let version = flags >> 24; - dnd.version = Some(version); - dnd.source_window = Some(source_window); - dnd.target_window = Some(window); let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; - if !has_more_types { - let type_list = [ + let types: Vec<_> = if !has_more_types { + [ xev.data.get_long(2) as xproto::Atom, xev.data.get_long(3) as xproto::Atom, xev.data.get_long(4) as xproto::Atom, ] .map(|ty_atom| SelectionType::new(atoms, ty_atom)) .into_iter() - .collect(); - dnd.types = Some(type_list); + .collect() } else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } { - dnd.types = Some( - more_types - .into_iter() - .map(|ty_atom| SelectionType::new(atoms, ty_atom)) - .collect(), - ); - } + more_types + .into_iter() + .map(|ty_atom| SelectionType::new(atoms, ty_atom)) + .collect() + } else { + Default::default() + }; + dnd.init_state(version, source_window, window, types.into()); dnd.transfer_id() }; @@ -501,16 +499,12 @@ impl EventProcessor { // Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event` // never contending the lock. let transfer_id = { - let mut dnd = self.target.dnd.write().unwrap(); - // By our own state flow, `version` should never be `None` at this point. - let version = dnd.version.unwrap_or(5); - - if dnd.target_window != Some(window) { + let dnd = self.target.dnd.borrow(); + // By our own state flow, `state` should never be `None` at this point. + let version = dnd.state.as_ref().map(|s| s.version).unwrap_or_else(|| { warn!("Received `XdndPosition` without `XdndEnter`"); - dnd.target_window = Some(window); - } - - dnd.source_window = Some(source_window); + 5 + }); let time = if version == 0 { // In version 0, time isn't specified @@ -522,15 +516,17 @@ impl EventProcessor { // Log this timestamp. self.target.xconn.set_timestamp(time); - let status = if dnd.accepted.unwrap_or_default() { - DndState::Accepted - } else { - DndState::Rejected - }; - unsafe { - dnd.send_status(window, source_window, status) - .expect("Failed to send `XdndStatus` message."); + dnd.send_status( + window, + source_window, + if dnd.shared.accepted.load(Ordering::Relaxed) { + DndState::Accepted + } else { + DndState::Rejected + }, + ) + .expect("Failed to send `XdndStatus` message."); } dnd.transfer_id() @@ -545,16 +541,24 @@ impl EventProcessor { } if xev.message_type == atoms[XdndDrop] as c_ulong { - let dnd = self.target.dnd.read().unwrap(); - let (source_window, state) = if let Some(source_window) = dnd.source_window { - (source_window, DndState::Accepted) - } else { - // `source_window` won't be part of our DND state if we already rejected the drop in - // our `XdndPosition` handler. - let source_window = xev.data.get_long(0) as xproto::Window; - (source_window, DndState::Rejected) + let dnd = self.target.dnd.borrow(); + let Some(source_window) = dnd.state.as_ref().map(|s| s.source_window) else { + warn!("Received `XdndDrop` without `XdndEnter`"); + return; }; + unsafe { + dnd.send_status( + window, + source_window, + if dnd.shared.accepted.load(Ordering::Relaxed) { + DndState::Accepted + } else { + DndState::Rejected + }, + ) + .expect("Failed to send `XdndStatus` message."); + } // TODO: Ensure that this is sent after `dnd` lock is released, to prevent // accidentally introducing a deadlock later down the line. app.window_event(&self.target, window_id, WindowEvent::DragDropped { @@ -562,7 +566,7 @@ impl EventProcessor { }); unsafe { - dnd.send_finished(window, source_window, state) + dnd.send_finished(window, source_window) .expect("Failed to send `XdndFinished` message."); } @@ -570,7 +574,7 @@ impl EventProcessor { } if xev.message_type == atoms[XdndLeave] as c_ulong { - let transfer_id = self.target.dnd.read().unwrap().transfer_id(); + let transfer_id = self.target.dnd.borrow().transfer_id(); app.window_event(&self.target, window_id, WindowEvent::DragLeft { id: transfer_id }); } } @@ -590,7 +594,7 @@ impl EventProcessor { return; } - let _ = unsafe { self.target.dnd.read().unwrap().read_data(window) }; + let _ = unsafe { self.target.dnd.borrow().read_data(window) }; } fn configure_notify(&self, xev: &XConfigureEvent, app: &mut dyn ApplicationHandler) { diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 61a86f1e6a..ba81850f69 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -5,7 +5,8 @@ use std::num::NonZeroU32; use std::ops::Deref; use std::os::raw::*; use std::path::Path; -use std::sync::{Arc, Mutex, MutexGuard, RwLock}; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex, MutexGuard}; use std::{cmp, env}; use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; @@ -32,8 +33,15 @@ use x11rb::protocol::sync::{ConnectionExt as _, Int64}; use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle}; use x11rb::protocol::{randr, xinput}; -use crate::atoms::*; -use crate::dnd::{Dnd, DndState}; +use crate::atoms::{ + _GTK_THEME_VARIANT, _NET_ACTIVE_WINDOW, _NET_WM_ICON, _NET_WM_MOVERESIZE, _NET_WM_NAME, + _NET_WM_PID, _NET_WM_PING, _NET_WM_STATE, _NET_WM_STATE_ABOVE, _NET_WM_STATE_BELOW, + _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_HIDDEN, _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_SYNC_REQUEST, _NET_WM_SYNC_REQUEST_COUNTER, + _NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE, + WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware, +}; +use crate::dnd::DndSharedState; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, WakeSender, X11Error, xinput_fp1616_to_float, @@ -306,62 +314,21 @@ impl CoreWindow for Window { } fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let mut dnd = self.dnd.write().unwrap(); - if dnd.transfer_id() != id { + if self.dnd_shared.transfer_id() != id { return Err(UnknownDataTransfer(id)); } - if dnd.accepted == Some(false) { - return Ok(()); - } - - dnd.accepted = Some(false); - - let Some(source_window) = dnd.source_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - let Some(window) = dnd.target_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - unsafe { - dnd.send_status(window, source_window, DndState::Rejected) - .expect("Failed to send `XdndStatus` message."); - } - dnd.reset(); + self.dnd_shared.accepted.store(false, Ordering::Relaxed); Ok(()) } fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let mut dnd = self.dnd.write().unwrap(); - if dnd.transfer_id() != id { + if self.dnd_shared.transfer_id() != id { return Err(UnknownDataTransfer(id)); } - if dnd.accepted == Some(true) { - return Ok(()); - } - - dnd.accepted = Some(true); - - let Some(source_window) = dnd.source_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - let Some(window) = dnd.target_window else { - // TODO: Should have "other error" since this isn't an unknown data transfer. - return Err(UnknownDataTransfer(id)); - }; - - unsafe { - dnd.send_status(window, source_window, DndState::Accepted) - .expect("Failed to send `XdndStatus` message."); - } + self.dnd_shared.accepted.store(true, Ordering::Relaxed); Ok(()) } @@ -476,7 +443,7 @@ unsafe impl Sync for UnownedWindow {} #[derive(Debug)] pub struct UnownedWindow { pub(crate) xconn: Arc, // never changes - dnd: Arc>, + dnd_shared: Arc, xwindow: xproto::Window, // never changes #[allow(dead_code)] visual: u32, // never changes @@ -702,12 +669,12 @@ impl UnownedWindow { .visual; } - let dnd = event_loop.dnd.clone(); + let dnd_shared = event_loop.dnd.borrow().shared.clone(); #[allow(clippy::mutex_atomic)] let mut window = UnownedWindow { xconn: Arc::clone(xconn), - dnd, + dnd_shared, xwindow: xwindow as xproto::Window, visual, root, From aaa006a06c7ef0c4a09f87a22fd1af943cb74012 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 16:43:37 +0200 Subject: [PATCH 36/87] X11 cleanup --- winit-x11/src/event_processor.rs | 38 +++++++++++--------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 62d5bfb157..6596c2d72e 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -433,10 +433,6 @@ impl EventProcessor { // never contending the lock. let transfer_id = { let mut dnd = self.target.dnd.borrow_mut(); - // We only reset when a new drag-and-drop enters, to maximize the amount of time - // that the drag data can be accessed. - dnd.reset(); - let source_window = xev.data.get_long(0) as xproto::Window; let flags = xev.data.get_long(1); @@ -541,35 +537,27 @@ impl EventProcessor { } if xev.message_type == atoms[XdndDrop] as c_ulong { - let dnd = self.target.dnd.borrow(); - let Some(source_window) = dnd.state.as_ref().map(|s| s.source_window) else { - warn!("Received `XdndDrop` without `XdndEnter`"); - return; + let (source_window, transfer_id) = { + let dnd = self.target.dnd.borrow(); + let Some(source_window) = dnd.state.as_ref().map(|s| s.source_window) else { + warn!("Received `XdndDrop` without `XdndEnter`"); + return; + }; + + (source_window, dnd.transfer_id()) }; - unsafe { - dnd.send_status( - window, - source_window, - if dnd.shared.accepted.load(Ordering::Relaxed) { - DndState::Accepted - } else { - DndState::Rejected - }, - ) - .expect("Failed to send `XdndStatus` message."); - } - // TODO: Ensure that this is sent after `dnd` lock is released, to prevent - // accidentally introducing a deadlock later down the line. - app.window_event(&self.target, window_id, WindowEvent::DragDropped { - id: dnd.transfer_id(), - }); + app.window_event(&self.target, window_id, WindowEvent::DragDropped { id: transfer_id }); + + let mut dnd = self.target.dnd.borrow_mut(); unsafe { dnd.send_finished(window, source_window) .expect("Failed to send `XdndFinished` message."); } + dnd.reset(); + return; } From 72ed878130a16dfc1df55d61a9322fac1a1d7d3a Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 16:49:36 +0200 Subject: [PATCH 37/87] Fix clippy warnings, make `wait_for_data` X11-internal --- winit-core/src/data_transfer.rs | 10 ---------- winit-x11/src/dnd.rs | 5 +---- winit-x11/src/event_processor.rs | 2 +- winit/examples/dnd.rs | 12 +++++------- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 1bc48862d7..d5f6edaba3 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -188,16 +188,6 @@ pub trait TypedData: AsAny + fmt::Debug { /// /// If this value is not readable as a string, return `None`. fn try_as_string(&mut self) -> io::Result; - - /// Block the current thread until the data is fully available, or until the data is - /// invalidated. - /// - /// Note that this doesn't mean that other methods will return `Ok`, simply that they won't - /// return `io::Error::WouldBlock`. - /// - /// If the data is ready to be read, return `Ok(())`. If this data has been invalidated (and - /// therefore this would wait forever), return `Err`. - fn wait_for_data(&self) -> io::Result<()>; } impl_dyn_casting!(TypedData); diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index bb0be69214..082a503c83 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -108,6 +108,7 @@ pub(crate) struct SharedDataReader { impl SharedDataReader { fn try_data(&self) -> io::Result<&[u8]> { + self.wait_for_data()?; self.reader.try_data() } @@ -300,10 +301,6 @@ impl TypedData for SelectionReader { .map(Into::into) .collect()) } - - fn wait_for_data(&self) -> io::Result<()> { - self.data.wait_for_data() - } } #[derive(Debug)] diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 6596c2d72e..906de52830 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -1760,7 +1760,7 @@ impl EventProcessor { .find(|prev_monitor| prev_monitor.name == new_monitor.name) .map(|prev_monitor| prev_monitor.scale_factor); if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { - for window in self.target.windows.borrow().iter().filter_map(|(_, w)| w.upgrade()) { + for window in self.target.windows.borrow().values().filter_map(|w| w.upgrade()) { window.refresh_dpi_for_monitor( &new_monitor, maybe_prev_scale_factor, diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 5fb4889a38..f7aecee1fb 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -63,11 +63,6 @@ impl ApplicationHandler for Application { info!("{event:?}"); if let Some(data) = &mut self.last_dnd_fetch { - // This may return an error with `io::ErrorKind::Deadlock` on X11 if - // this is called in the event loop thread while the application is - // still waiting for data. - data.wait_for_data().unwrap(); - match data.type_().hint() { Some(TypeHint::Plaintext | TypeHint::Html) => { let text = data.try_as_string().unwrap(); @@ -121,9 +116,12 @@ impl ApplicationHandler for Application { self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok(); - match self.last_dnd_fetch.as_ref().unwrap().wait_for_data() { + match self.last_dnd_fetch.as_mut().unwrap().try_as_string() { Err(e) if e.kind() == std::io::ErrorKind::Deadlock => { - warn!("Immediately waiting for a fetched data transfer may deadlock!"); + warn!( + "Immediately waiting for a fetched data transfer may deadlock on some \ + platforms!" + ); }, _ => {}, } From 1a6b0dd2942cd4080571ab162f78d0fffa4729ca Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 17:18:33 +0200 Subject: [PATCH 38/87] Remove `wait_for_data` from appkit --- winit-appkit/src/dnd.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 629fa5d29e..208515e007 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -297,11 +297,6 @@ impl TypedData for PasteboardValue { .map(|ns_str| ns_str.to_string()) .ok_or_else(|| io::ErrorKind::InvalidData.into()) } - - fn wait_for_data(&self) -> io::Result<()> { - // The methods on `NSPasteboard` already wait without danger of deadlock - Ok(()) - } } #[derive(Debug, Default)] From fc554065b6992e941e3e8420f5c42f7306a91de4 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 27 May 2026 17:41:56 +0200 Subject: [PATCH 39/87] WIP: win32 --- winit-win32/src/drop_handler.rs | 65 ++++++++++++++++++++-------- winit-win32/src/event_loop.rs | 16 +++++++ winit-win32/src/event_loop/runner.rs | 2 + winit-win32/src/lib.rs | 7 +-- winit-win32/src/window.rs | 44 ++++++++++++++++--- winit-win32/src/window_state.rs | 10 ++++- 6 files changed, 115 insertions(+), 29 deletions(-) diff --git a/winit-win32/src/drop_handler.rs b/winit-win32/src/drop_handler.rs index fbb121bcad..e30b3ab46e 100644 --- a/winit-win32/src/drop_handler.rs +++ b/winit-win32/src/drop_handler.rs @@ -2,7 +2,8 @@ use std::ffi::{OsString, c_void}; use std::os::windows::ffi::OsStringExt; use std::path::PathBuf; use std::ptr; -use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; use dpi::PhysicalPosition; use tracing::debug; @@ -19,19 +20,43 @@ use crate::definitions::{ IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl, }; +#[derive(Default, Debug)] +pub struct FileDropDataShared { + transfer_id: AtomicI64, + pub accepted: AtomicBool, +} + +impl FileDropDataShared { + pub fn transfer_id(&self) -> DataTransferId { + DataTransferId::from_raw(self.transfer_id.load(Ordering::Relaxed)) + } +} + +#[allow(dead_code, reason = "TODO")] +#[repr(C)] +pub struct DataObjectData { + pub interface: IDataObject, + refcount: AtomicUsize, +} + +#[allow(dead_code, reason = "TODO")] +pub struct DataObject { + data: *mut DataObjectData, +} + #[repr(C)] pub struct FileDropHandlerData { - pub interface: IDropTarget, + interface: IDropTarget, refcount: AtomicUsize, window: HWND, send_event: Box, - accepted: bool, + shared: Arc, active_data_transfer_id: Option, } impl FileDropHandlerData { fn cursor_effect(&self) -> u32 { - if self.accepted { + if self.shared.accepted.load(Ordering::Relaxed) { // TODO: Handle other kinds of drop effect DROPEFFECT_COPY } else { @@ -41,31 +66,33 @@ impl FileDropHandlerData { } pub struct FileDropHandler { - pub data: *mut FileDropHandlerData, + data: *mut FileDropHandlerData, } #[allow(non_snake_case)] impl FileDropHandler { - pub(crate) fn new(window: HWND, send_event: Box) -> FileDropHandler { + pub(crate) fn new( + window: HWND, + shared: Arc, + send_event: Box, + ) -> FileDropHandler { let data = Box::new(FileDropHandlerData { interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl }, refcount: AtomicUsize::new(1), window, send_event, active_data_transfer_id: None, - accepted: false, + shared, }); FileDropHandler { data: Box::into_raw(data) } } - #[expect(dead_code, reason = "TODO")] - pub(crate) fn set_accepted(&mut self, accepted: bool) { - let drop_handler_data = unsafe { Self::from_interface(self.data) }; - drop_handler_data.accepted = accepted; + pub(crate) unsafe fn interface_unchecked_mut(&mut self) -> &mut IDropTarget { + unsafe { &mut (*self.data).interface } } // Implement IUnknown - pub unsafe extern "system" fn QueryInterface( + unsafe extern "system" fn QueryInterface( _this: *mut IUnknown, _riid: *const GUID, _ppvObject: *mut *mut c_void, @@ -75,13 +102,13 @@ impl FileDropHandler { unimplemented!(); } - pub unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { let drop_handler_data = unsafe { Self::from_interface(this) }; let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; count as u32 } - pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { let drop_handler = unsafe { Self::from_interface(this) }; let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; if count == 0 { @@ -91,7 +118,7 @@ impl FileDropHandler { count as u32 } - pub unsafe extern "system" fn DragEnter( + unsafe extern "system" fn DragEnter( this: *mut IDropTarget, _pDataObj: *const IDataObject, _grfKeyState: u32, @@ -120,7 +147,7 @@ impl FileDropHandler { S_OK } - pub unsafe extern "system" fn DragOver( + unsafe extern "system" fn DragOver( this: *mut IDropTarget, _grfKeyState: u32, pt: POINTL, @@ -148,7 +175,7 @@ impl FileDropHandler { S_OK } - pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { + unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { let drop_handler = unsafe { Self::from_interface(this) }; let Some(data_transfer_id) = drop_handler.active_data_transfer_id else { return E_ABORT; @@ -159,7 +186,7 @@ impl FileDropHandler { S_OK } - pub unsafe extern "system" fn Drop( + unsafe extern "system" fn Drop( this: *mut IDropTarget, _pDataObj: *const IDataObject, _grfKeyState: u32, @@ -194,7 +221,7 @@ impl FileDropHandler { unsafe { &mut *(this as *mut _) } } - #[expect(dead_code, reason = "TODO")] + #[allow(dead_code, reason = "TODO")] unsafe fn iterate_filenames(data_obj: *const IDataObject, mut callback: F) -> Option where F: FnMut(PathBuf), diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 8f1008f98d..9c9388e1d2 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -63,6 +63,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ }; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor, CustomCursorSource}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::event::{ DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton, @@ -478,6 +479,21 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result, RequestError> { + let _ = id; + let _ = type_; + todo!() + } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let _ = id; + todo!() + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { diff --git a/winit-win32/src/event_loop/runner.rs b/winit-win32/src/event_loop/runner.rs index 65e6082761..f4bc643598 100644 --- a/winit-win32/src/event_loop/runner.rs +++ b/winit-win32/src/event_loop/runner.rs @@ -37,6 +37,8 @@ pub(crate) struct EventLoopRunner { event_handler: Rc, event_buffer: RefCell>, + // TODO + // data_transfers_per_window: RefCell>>, panic_error: Cell>, } diff --git a/winit-win32/src/lib.rs b/winit-win32/src/lib.rs index 569d1f32c1..1db6953502 100644 --- a/winit-win32/src/lib.rs +++ b/winit-win32/src/lib.rs @@ -39,6 +39,7 @@ use self::icon::{RaiiIcon, SelectedCursor}; pub use self::keyboard::{physicalkey_to_scancode, scancode_to_physicalkey}; pub use self::monitor::{MonitorHandle, VideoModeHandle}; pub use self::window::Window; +use crate::drop_handler::FileDropDataShared; /// Window Handle type used by Win32 API pub type HWND = *mut c_void; @@ -463,7 +464,7 @@ pub struct WindowAttributesWindows { pub(crate) menu: Option, pub(crate) taskbar_icon: Option, pub(crate) no_redirection_bitmap: bool, - pub(crate) drag_and_drop: bool, + pub(crate) drag_and_drop: Option>, pub(crate) skip_taskbar: bool, pub(crate) class_name: String, pub(crate) decoration_shadow: bool, @@ -483,7 +484,7 @@ impl Default for WindowAttributesWindows { menu: None, taskbar_icon: None, no_redirection_bitmap: false, - drag_and_drop: true, + drag_and_drop: Some(Default::default()), skip_taskbar: false, class_name: "Window Class".to_string(), decoration_shadow: false, @@ -556,7 +557,7 @@ impl WindowAttributesWindows { /// does that, but there may be more in the future. If you need COM API with /// `COINIT_MULTITHREADED` you must initialize it before calling any winit functions. See for more information. pub fn with_drag_and_drop(mut self, flag: bool) -> Self { - self.drag_and_drop = flag; + self.drag_and_drop = flag.then(|| Default::default()); self } diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index dba95e9bd8..4c5fdf5892 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -3,6 +3,7 @@ use std::cell::Cell; use std::ffi::c_void; use std::mem::{self, MaybeUninit}; use std::rc::Rc; +use std::sync::atomic::Ordering; use std::sync::mpsc::channel; use std::sync::{Arc, Mutex, MutexGuard}; use std::{io, panic, ptr}; @@ -48,13 +49,14 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ WM_SYSCOMMAND, WNDCLASSEXW, }; use winit_core::cursor::Cursor; +use winit_core::data_transfer::DataTransferId; use winit_core::error::RequestError; use winit_core::icon::{Icon, RgbaIcon}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, - UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, - WindowLevel, + UnknownDataTransfer, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, + WindowId, WindowLevel, }; use crate::dark_mode::try_theme; @@ -1158,6 +1160,36 @@ impl CoreWindow for Window { fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let state = self.window_state.lock().unwrap(); + if let Some(shared) = &state.drop_data_shared { + if shared.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + shared.accepted.store(true, Ordering::Relaxed); + + Ok(()) + } else { + return Err(UnknownDataTransfer(id)); + } + } + + fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { + let state = self.window_state.lock().unwrap(); + if let Some(shared) = &state.drop_data_shared { + if shared.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + shared.accepted.store(true, Ordering::Relaxed); + + Ok(()) + } else { + return Err(UnknownDataTransfer(id)); + } + } } pub(super) struct InitData<'a> { @@ -1191,6 +1223,7 @@ impl InitData<'_> { let window_state = { let window_state = WindowState::new( &self.attributes, + &self.win_attributes, scale_factor, current_theme, self.attributes.preferred_theme, @@ -1214,7 +1247,7 @@ impl InitData<'_> { } unsafe fn create_window_data(&self, win: &Window) -> event_loop::WindowData { - let file_drop_handler = if self.win_attributes.drag_and_drop { + let file_drop_handler = if let Some(shared) = self.win_attributes.drag_and_drop.clone() { let ole_init_result = unsafe { OleInitialize(ptr::null_mut()) }; // It is ok if the initialize result is `S_FALSE` because it might happen that // multiple windows are created on the same thread. @@ -1230,15 +1263,16 @@ impl InitData<'_> { let file_drop_runner = self.runner.clone(); let window_id = win.id(); - let file_drop_handler = FileDropHandler::new( + let mut file_drop_handler = FileDropHandler::new( win.window.hwnd(), + shared, Box::new(move |event| { file_drop_runner.send_event(Event::Window { window_id, event }) }), ); let handler_interface_ptr = - unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void }; + unsafe { file_drop_handler.interface_unchecked_mut() as *mut _ as *mut c_void }; assert_eq!(unsafe { RegisterDragDrop(win.window.hwnd(), handler_interface_ptr) }, S_OK); Some(file_drop_handler) diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index ed06d76042..e5151f09af 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -1,4 +1,4 @@ -use std::sync::MutexGuard; +use std::sync::{Arc, MutexGuard}; use std::{fmt, io, ptr}; use bitflags::bitflags; @@ -22,7 +22,8 @@ use winit_core::keyboard::ModifiersState; use winit_core::monitor::Fullscreen; use winit_core::window::{ImeCapabilities, Theme, WindowAttributes}; -use crate::{SelectedCursor, event_loop, util}; +use crate::drop_handler::FileDropDataShared; +use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util}; /// Contains information about states and the window that the callback is going to use. #[derive(Debug)] @@ -63,6 +64,8 @@ pub(crate) struct WindowState { pub dragging: bool, + pub drop_data_shared: Option>, + pub skip_taskbar: bool, pub use_system_wheel_speed: bool, @@ -154,6 +157,7 @@ pub enum ImeState { impl WindowState { pub(crate) fn new( attributes: &WindowAttributes, + win_attributes: &WindowAttributesWindows, scale_factor: f64, current_theme: Theme, preferred_theme: Option, @@ -194,6 +198,8 @@ impl WindowState { dragging: false, + drop_data_shared: win_attributes.drag_and_drop.clone(), + skip_taskbar: false, use_system_wheel_speed: true, From 4f22d27dcf6ccc3ed034bda0f606d4aa093785c5 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 28 May 2026 10:51:02 +0200 Subject: [PATCH 40/87] Simpler API Since we don't need internal mutability on `Dnd` any more. --- winit-appkit/src/dnd.rs | 61 +++++++++++++++++++++------------ winit-core/src/data_transfer.rs | 48 +++++++++++++++++++------- winit-win32/src/drop_handler.rs | 19 +++++----- winit-x11/src/dnd.rs | 12 +++++-- 4 files changed, 96 insertions(+), 44 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 208515e007..06646c0b5c 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -1,7 +1,8 @@ -use std::cell::RefCell; +use std::cell::{OnceCell, RefCell}; use std::collections::HashMap; use std::io; -use std::ops::Deref; +use std::ops::{ControlFlow, Deref}; +use std::rc::Rc; use std::sync::atomic::{AtomicI64, Ordering}; use objc2::Message; @@ -84,6 +85,7 @@ impl TransferType for PasteboardType { pub struct Pasteboard { transfer_id: DataTransferId, inner: Retained, + types: OnceCell>, } impl Deref for Pasteboard { @@ -95,6 +97,26 @@ impl Deref for Pasteboard { } impl Pasteboard { + fn new(transfer_id: DataTransferId, pasteboard: Retained) -> Self { + Self { transfer_id, inner: pasteboard, types: Default::default() } + } + + /// Get the array of [`PasteboardType`]s advertized by this [`Pasteboard`]. + pub fn types(&self) -> &[PasteboardType] { + self.types.get_or_init(|| { + self.inner + .types() + .map(|types| { + types + .into_iter() + .map(|pb_type| PasteboardType::from(pb_type)) + .collect::>() + }) + .unwrap_or_default() + .into() + }) + } + /// Get the `DataTransferId` of this pasteboard. pub fn id(&self) -> DataTransferId { self.transfer_id @@ -116,27 +138,26 @@ impl Pasteboard { } impl DataTransfer for Pasteboard { - fn available_types(&self) -> Vec> { - self.inner - .types() - .map(|types| { - types - .into_iter() - .map(|pb_type| Box::new(PasteboardType::from(pb_type)) as _) - .collect() - }) - .unwrap_or_default() + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, + ) { + for ty in self.types() { + if let ControlFlow::Break(()) = func(ty) { + break; + } + } } fn has_type(&self, type_: &dyn TransferType) -> bool { - let Some(pb_types) = self.inner.types() else { - return false; - }; - if let Some(needle) = type_.cast_ref::().cloned() { + let Some(pb_types) = self.inner.types() else { + return false; + }; + pb_types.iter().any(|haystack| **needle == *haystack) } else if let Some(needle) = type_.hint() { - pb_types.iter().any(|haystack| PasteboardType::from(haystack).hint() == Some(needle)) + self.types().iter().any(|haystack| haystack.hint() == Some(needle)) } else { false } @@ -329,10 +350,6 @@ impl DndState { } pub fn get(&self, id: DataTransferId) -> Option { - self.inner - .borrow() - .get(&id) - .and_then(|weak| weak.load()) - .map(|pb| Pasteboard { transfer_id: id, inner: pb }) + self.inner.borrow().get(&id).and_then(|weak| weak.load()).map(|pb| Pasteboard::new(id, pb)) } } diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index d5f6edaba3..df1f456afa 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -43,8 +43,10 @@ //! implementing the traits in this module, which can then be accessed in an application //! using the methods defined on [`dyn AsAny`]. See each platform's documentation for details. +use std::borrow::Cow; use std::fmt::{self, Debug}; use std::io; +use std::ops::ControlFlow; use crate::as_any::AsAny; @@ -162,13 +164,11 @@ impl_dyn_casting!(TransferType); /// /// ### Blocking /// -/// Note that, in general, this type provides a _non-blocking_ interface. This means that the reader -/// provided by [`try_read`](TypedData::try_read), as well other methods returning [`io::Result`], -/// may return an error with [`io::ErrorKind::WouldBlock`]. To ensure that the `TypedData` is ready -/// to read, the user may call [`wait_for_data`](TypedData::wait). This will block the current -/// thread until the data is ready to read without returning `WouldBlock`. **This should not be -/// called on the event handling thread**, as platforms may need to wait on OS events to populate -/// the data. +/// Note that this type provides a blocking interface. In cases where reading this type directly on +/// the event loop would cause a deadlock, the backend will make a best-effort attempt to return an +/// error with [`io::ErrorKind::Deadlock`]. For now, the only way to access the data is via blocking +/// on the event loop, so simply retrying the next time an event is received that references the +/// data transfer should be enough to ensure that the data is accessible. pub trait TypedData: AsAny + fmt::Debug { /// The type of this `TypedData`. fn type_(&self) -> &dyn TransferType; @@ -197,13 +197,28 @@ impl_dyn_casting!(TypedData); /// [`Window::fetch_data_transfer`](crate::window::Window::fetch_data_transfer) /// and [`WindowEvent::DataTransferResult`](crate::event::WindowEvent::DataTransferResult). pub trait DataTransfer: AsAny + fmt::Debug { + /// Iterate over each type advertized by this `DataTransfer`. This is just a minor optimization, + /// in most cases you should probably use [`has_type`](DataTransfer::has_type) or + /// [`available_types`](DataTransfer::available_types). + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>, + ); + /// Display the list of all available types. /// /// This is useful if more-complex type matching is required, but for most cases /// [`has_type`](DataTransfer::has_type) should be used. - // TODO: We should be able to do `&dyn TransferType`, but some implementation details in - // the platforms make that unnecessarily difficult right now. Specifically, use of `RwLock`. - fn available_types(&self) -> Vec>; + fn available_types(&self) -> Vec<&'_ dyn TransferType> { + let mut out = Vec::new(); + + self.for_each_available_type(&mut |ty| { + out.push(ty); + ControlFlow::Continue(()) + }); + + out + } /// Check if the supplied type is provided by this [`DataTransfer`]. /// @@ -211,9 +226,18 @@ pub trait DataTransfer: AsAny + fmt::Debug { /// platform-specific type is required then that platform's implementation of `TransferType` can /// be used. fn has_type(&self, type_: &dyn TransferType) -> bool { - let available_types = self.available_types(); type_.hint().is_some_and(|hint| { - available_types.iter().any(|haystack| haystack.hint() == Some(hint)) + let mut found = false; + self.for_each_available_type(&mut |haystack| { + if haystack.hint() == Some(hint) { + found = true; + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }); + + found }) } } diff --git a/winit-win32/src/drop_handler.rs b/winit-win32/src/drop_handler.rs index e30b3ab46e..8af754f741 100644 --- a/winit-win32/src/drop_handler.rs +++ b/winit-win32/src/drop_handler.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::ffi::{OsString, c_void}; use std::os::windows::ffi::OsStringExt; use std::path::PathBuf; @@ -32,16 +33,18 @@ impl FileDropDataShared { } } -#[allow(dead_code, reason = "TODO")] -#[repr(C)] -pub struct DataObjectData { - pub interface: IDataObject, - refcount: AtomicUsize, +enum DataKind { + Uris(Vec), + String(String), + Bytes(Vec), } -#[allow(dead_code, reason = "TODO")] -pub struct DataObject { - data: *mut DataObjectData, +struct DataObject { + // TODO: Exposing the full native API to client applications is too error-prone so long as + // winit is still manually implementing refcounting and using the win32 APIs. For now, we + // just eagerly read all the data supported by cross-platform type hints on Windows. This + // would be resolved by migrating to `windows-rs`. + data: HashMap, } #[repr(C)] diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 082a503c83..8195d29e7b 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -1,6 +1,7 @@ use std::cell::Cell; use std::io; use std::marker::PhantomData; +use std::ops::ControlFlow; use std::os::raw::*; use std::str::Utf8Error; use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; @@ -441,8 +442,15 @@ impl TransferType for SelectionType { } impl DataTransfer for Selection { - fn available_types(&self) -> Vec> { - self.types.iter().cloned().map(|val| Box::new(val) as _).collect() + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, + ) { + for ty in &self.types[..] { + if let ControlFlow::Break(()) = func(ty) { + break; + } + } } fn has_type(&self, type_: &dyn TransferType) -> bool { From ea2451430325aa656d2d364cf8359f7e6e0a4132 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 28 May 2026 10:57:32 +0200 Subject: [PATCH 41/87] `drop_handler`->`dnd` For consistency with other platforms --- winit-win32/src/{drop_handler.rs => dnd.rs} | 2 +- winit-win32/src/event_loop.rs | 2 +- winit-win32/src/lib.rs | 4 ++-- winit-win32/src/window.rs | 2 +- winit-win32/src/window_state.rs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename winit-win32/src/{drop_handler.rs => dnd.rs} (99%) diff --git a/winit-win32/src/drop_handler.rs b/winit-win32/src/dnd.rs similarity index 99% rename from winit-win32/src/drop_handler.rs rename to winit-win32/src/dnd.rs index 8af754f741..3eabea1d09 100644 --- a/winit-win32/src/drop_handler.rs +++ b/winit-win32/src/dnd.rs @@ -14,7 +14,7 @@ use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE}; use windows_sys::Win32::UI::Shell::{DragQueryFileW, HDROP}; use windows_sys::core::{GUID, HRESULT}; -use winit_core::data_transfer::DataTransferId; +use winit_core::data_transfer::{DataTransferId, TypeHint}; use winit_core::event::WindowEvent; use crate::definitions::{ diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 9c9388e1d2..593a49d7fa 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -83,8 +83,8 @@ pub(super) use self::runner::{Event, EventLoopRunner}; use super::SelectedCursor; use super::window::set_skip_taskbar; use crate::dark_mode::try_theme; +use crate::dnd::FileDropHandler; use crate::dpi::{become_dpi_aware, dpi_to_scale_factor}; -use crate::drop_handler::FileDropHandler; use crate::icon::WinCursor; use crate::ime::ImeContext; use crate::keyboard::KeyEventBuilder; diff --git a/winit-win32/src/lib.rs b/winit-win32/src/lib.rs index 1db6953502..4f922831a3 100644 --- a/winit-win32/src/lib.rs +++ b/winit-win32/src/lib.rs @@ -8,8 +8,8 @@ mod util; mod dark_mode; mod definitions; +mod dnd; mod dpi; -mod drop_handler; mod event_loop; mod icon; mod ime; @@ -39,7 +39,7 @@ use self::icon::{RaiiIcon, SelectedCursor}; pub use self::keyboard::{physicalkey_to_scancode, scancode_to_physicalkey}; pub use self::monitor::{MonitorHandle, VideoModeHandle}; pub use self::window::Window; -use crate::drop_handler::FileDropDataShared; +use crate::dnd::FileDropDataShared; /// Window Handle type used by Win32 API pub type HWND = *mut c_void; diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index 4c5fdf5892..0f6097949e 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -63,8 +63,8 @@ use crate::dark_mode::try_theme; use crate::definitions::{ CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2, }; +use crate::dnd::FileDropHandler; use crate::dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi}; -use crate::drop_handler::FileDropHandler; use crate::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID, Event, EventLoopRunner}; use crate::icon::{IconType, WinCursor}; use crate::ime::ImeContext; diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index e5151f09af..a09e027072 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -22,7 +22,7 @@ use winit_core::keyboard::ModifiersState; use winit_core::monitor::Fullscreen; use winit_core::window::{ImeCapabilities, Theme, WindowAttributes}; -use crate::drop_handler::FileDropDataShared; +use crate::dnd::FileDropDataShared; use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util}; /// Contains information about states and the window that the callback is going to use. From 8028fc8784bc1dafd0b0eb74f53869ca2180ee68 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 28 May 2026 12:08:47 +0200 Subject: [PATCH 42/87] Fix unused Cow import --- winit-core/src/data_transfer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index df1f456afa..b7c52f5be7 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -43,7 +43,7 @@ //! implementing the traits in this module, which can then be accessed in an application //! using the methods defined on [`dyn AsAny`]. See each platform's documentation for details. -use std::borrow::Cow; +use std::ffi::OsString; use std::fmt::{self, Debug}; use std::io; use std::ops::ControlFlow; From 42c4ccf5ce7d9e0a091e118f8886c7617ea4981f Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 28 May 2026 12:14:57 +0200 Subject: [PATCH 43/87] Use `OsString` for paths --- winit-appkit/src/dnd.rs | 5 +++-- winit-core/src/data_transfer.rs | 2 +- winit-x11/src/dnd.rs | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 06646c0b5c..b66001c868 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -1,5 +1,6 @@ use std::cell::{OnceCell, RefCell}; use std::collections::HashMap; +use std::ffi::OsString; use std::io; use std::ops::{ControlFlow, Deref}; use std::rc::Rc; @@ -273,7 +274,7 @@ impl TypedData for PasteboardValue { .map(|data| Box::new(DataReader::new(data)) as _) } - fn try_as_uris(&mut self) -> io::Result> { + fn try_as_uris(&mut self) -> io::Result> { // TODO: We should probably use `readObjects`, need to check how that works. if self.type_().hint() != Some(TypeHint::UriList) { return Err(io::ErrorKind::InvalidData.into()); @@ -299,7 +300,7 @@ impl TypedData for PasteboardValue { .downcast::() .unwrap() .into_iter() - .map(|file| file.downcast::().unwrap().to_string()) + .map(|file| file.downcast::().unwrap().to_string().into()) .collect(); return Ok(paths); diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index b7c52f5be7..6b970bbffb 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -182,7 +182,7 @@ pub trait TypedData: AsAny + fmt::Debug { /// /// The format of the returned URIs is simply a vector of strings. No validation is done /// to ensure that the URIs are valid or in the format - fn try_as_uris(&mut self) -> io::Result>; + fn try_as_uris(&mut self) -> io::Result>; /// Read this value as a plain text string. /// diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 8195d29e7b..1cb34807fc 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -1,4 +1,5 @@ use std::cell::Cell; +use std::ffi::OsString; use std::io; use std::marker::PhantomData; use std::ops::ControlFlow; @@ -290,7 +291,7 @@ impl TypedData for SelectionReader { } } - fn try_as_uris(&mut self) -> io::Result> { + fn try_as_uris(&mut self) -> io::Result> { if self.type_().hint() != Some(TypeHint::UriList) { return Err(io::ErrorKind::InvalidData.into()); } From 798c79b4755b6248df2261fb5833738bd8db6248 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Mon, 1 Jun 2026 13:14:44 +0200 Subject: [PATCH 44/87] WIP: Implement for Wayland --- winit-appkit/src/dnd.rs | 29 +- winit-appkit/src/window.rs | 10 +- winit-core/src/data_transfer.rs | 34 +- winit-core/src/event_loop/mod.rs | 126 ++++++ winit-core/src/window.rs | 61 +-- winit-wayland/src/data_device.rs | 46 ++ winit-wayland/src/dnd.rs | 653 ++++++++++++++++++++++++++++ winit-wayland/src/event_loop/mod.rs | 72 ++- winit-wayland/src/lib.rs | 1 + winit-wayland/src/seat/dnd/mod.rs | 182 -------- winit-wayland/src/seat/mod.rs | 1 - winit-wayland/src/state.rs | 7 +- winit-wayland/src/window/state.rs | 1 + winit-win32/src/window.rs | 30 +- winit-x11/src/atoms.rs | 4 +- winit-x11/src/dnd.rs | 189 ++++---- winit-x11/src/event_loop.rs | 28 +- winit-x11/src/event_processor.rs | 33 +- winit-x11/src/window.rs | 35 +- winit/examples/dnd.rs | 18 +- 20 files changed, 1076 insertions(+), 484 deletions(-) create mode 100644 winit-wayland/src/data_device.rs create mode 100644 winit-wayland/src/dnd.rs delete mode 100644 winit-wayland/src/seat/dnd/mod.rs diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index b66001c868..5bfafe155f 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -79,6 +79,15 @@ impl TransferType for PasteboardType { fn hint(&self) -> Option { self.hint } + + fn matches(&self, other: &dyn TransferType) -> bool { + if let Some(other_mime) = other.cast_ref::() { + *self == *other_mime + } else { + // If either hint is `None`, return false + self.hint().is_some_and(|hint| other.hint() == Some(hint)) + } + } } /// A thin wrapper around [`NSPasteboard`], implementing [`DataTransfer`]. @@ -143,25 +152,7 @@ impl DataTransfer for Pasteboard { &'this self, func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, ) { - for ty in self.types() { - if let ControlFlow::Break(()) = func(ty) { - break; - } - } - } - - fn has_type(&self, type_: &dyn TransferType) -> bool { - if let Some(needle) = type_.cast_ref::().cloned() { - let Some(pb_types) = self.inner.types() else { - return false; - }; - - pb_types.iter().any(|haystack| **needle == *haystack) - } else if let Some(needle) = type_.hint() { - self.types().iter().any(|haystack| haystack.hint() == Some(needle)) - } else { - false - } + let _ = self.types().iter().map(|mime| mime as &dyn TransferType).try_for_each(func); } } diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index dcaed949ca..bbd1a392c7 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -341,15 +341,7 @@ impl CoreWindow for Window { self } - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - self.maybe_wait_on_main(|delegate| delegate.set_drag_accepted(id, true)) - .map_err(|()| UnknownDataTransfer(id)) - } - - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - self.maybe_wait_on_main(|delegate| delegate.set_drag_accepted(id, false)) - .map_err(|()| UnknownDataTransfer(id)) - } + // TODO: `set_valid_actions` } define_class!( diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 6b970bbffb..9f9c1e0e11 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -144,17 +144,19 @@ pub trait TransferType: AsAny + fmt::Debug { /// If this returns `None`, then this is a platform-dependent type that has no cross-platform /// equivalent. fn hint(&self) -> Option; + + /// Check whether two dynamically-typed transfer types are equivalent. + // Can't use a `PartialEq` bound because it causes a dependency cycle. + fn matches(&self, other: &dyn TransferType) -> bool; } impl TransferType for TypeHint { fn hint(&self) -> Option { Some(*self) } -} -impl TransferType for Option { - fn hint(&self) -> Option { - *self + fn matches(&self, other: &dyn TransferType) -> bool { + other.hint() == Some(*self) } } @@ -226,19 +228,17 @@ pub trait DataTransfer: AsAny + fmt::Debug { /// platform-specific type is required then that platform's implementation of `TransferType` can /// be used. fn has_type(&self, type_: &dyn TransferType) -> bool { - type_.hint().is_some_and(|hint| { - let mut found = false; - self.for_each_available_type(&mut |haystack| { - if haystack.hint() == Some(hint) { - found = true; - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - }); - - found - }) + let mut found = false; + self.for_each_available_type(&mut |haystack| { + if haystack.matches(type_) { + found = true; + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }); + + found } } diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index ffc4d2bfd1..74236a9b41 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -18,6 +18,19 @@ use crate::error::{NotSupportedError, RequestError}; use crate::monitor::MonitorHandle; use crate::window::{Theme, Window, WindowAttributes}; +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Creates an [`EventLoopProxy`] that can be used to dispatch user events /// to the main event loop, possibly from another thread. @@ -143,6 +156,20 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { this platform", ))) } + + /// Set a given `DndActionMask` as the valid actions for the given [`DataTransferId`], + /// presuming that the transfer ID is from a drag-and-drop operation. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. + fn set_valid_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> Result<(), UnknownDataTransfer> { + let _ = actions; + Err(UnknownDataTransfer(id)) + } } impl HasDisplayHandle for dyn ActiveEventLoop + '_ { @@ -153,6 +180,105 @@ impl HasDisplayHandle for dyn ActiveEventLoop + '_ { impl_dyn_casting!(ActiveEventLoop); +// Inspired by https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum DndActions { + /// A specific set of operations. + Flags { move_: bool, copy: bool, link: bool }, + /// All actions, including platform-specific ones not represented in `Self::Flags`. + All, +} + +impl DndActions { + pub const fn copy(&self) -> bool { + match *self { + DndActions::Flags { copy, .. } => copy, + DndActions::All => true, + } + } + + pub const fn move_(&self) -> bool { + match *self { + DndActions::Flags { move_, .. } => move_, + DndActions::All => true, + } + } + + pub const fn link(&self) -> bool { + match *self { + DndActions::Flags { link, .. } => link, + DndActions::All => true, + } + } + + pub const fn all() -> Self { + Self::All + } + + pub const fn none() -> Self { + Self::Flags { move_: false, copy: false, link: false } + } + + pub const fn any(&self) -> bool { + match *self { + Self::All => true, + Self::Flags { move_, copy, link } => move_ || copy || link, + } + } + + pub const fn is_empty(&self) -> bool { + !self.any() + } + + pub const fn intersects(&self, other: &Self) -> bool { + self.intersection(other).any() + } + + pub const fn intersection(&self, other: &Self) -> Self { + match (*self, *other) { + (Self::All, other) | (other, Self::All) => other, + ( + Self::Flags { move_: this_move, copy: this_copy, link: this_link }, + Self::Flags { move_: other_move, copy: other_copy, link: other_link }, + ) => Self::Flags { + move_: this_move && other_move, + copy: this_copy && other_copy, + link: this_link && other_link, + }, + } + } +} + +pub trait DndActionMask: AsAny + fmt::Debug { + fn hint(&self) -> DndActions; + fn intersection(&self, other: &dyn DndActionMask) -> Box; + fn is_empty(&self) -> bool; + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + !self.intersection(other).is_empty() + } +} + +impl_dyn_casting!(DndActionMask); + +impl DndActionMask for DndActions { + fn hint(&self) -> DndActions { + *self + } + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + self.intersects(&other.hint()) + } + + fn intersection(&self, other: &dyn DndActionMask) -> Box { + Box::new(self.intersection(&other.hint())) + } + + fn is_empty(&self) -> bool { + self.is_empty() + } +} + /// Control the [`ActiveEventLoop`], possibly from a different thread, without referencing it /// directly. #[derive(Clone, Debug)] diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 57b4cae985..35e604ed2f 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1,5 +1,5 @@ //! The [`Window`] trait and associated types. -use std::fmt::{self, Display}; +use std::fmt; use bitflags::bitflags; use cursor_icon::CursorIcon; @@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize}; use crate::as_any::AsAny; use crate::cursor::Cursor; -use crate::data_transfer::{DataTransferId, TransferType}; use crate::error::RequestError; use crate::icon::Icon; use crate::monitor::{Fullscreen, MonitorHandle}; @@ -459,19 +458,6 @@ pub trait PlatformWindowAttributes: AsAny + std::fmt::Debug + Send + Sync { impl_dyn_casting!(PlatformWindowAttributes); -/// An operation was attempted on a data transfer ID, but that ID was invalid. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct UnknownDataTransfer(pub DataTransferId); - -impl Display for UnknownDataTransfer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let id = self.0.into_raw(); - write!(f, "Unknown data transfer with ID {id}") - } -} - -impl std::error::Error for UnknownDataTransfer {} - /// Represents a window. /// /// The window is closed when dropped. @@ -1450,51 +1436,6 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// Get the raw-window-handle v0.6 window handle. fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle; - - /// Mark a given data transfer ID as being accepted by the window. By default, a drag will be - /// rejected. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can be dropped. - /// - /// Note that on some platforms (e.g. Wayland), accepting a data transfer requires specifying - /// one or more accepted types. For platforms that require specifying a type, `accept_drag` will - /// mark all available types as accepted. - /// - /// For the most reliable cross-platform behaviour, - /// [`accept_drag_type`](Window::accept_drag_type) is preferred. - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } - - /// Mark a single type of a given data transfer ID as being accepted by the window. By default, - /// a drag will be rejected. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can be dropped. - /// - /// If the window may accept more than one of the advertised types, this method should be - /// called multiple times, once for each of the accepted types. - fn accept_drag_type( - &self, - id: DataTransferId, - type_: &dyn TransferType, - ) -> Result<(), UnknownDataTransfer> { - let _ = type_; - self.accept_drag(id) - } - - /// Mark a given data transfer ID as being rejected by the window. This is the default if - /// `accept_drag`/`accept_drag_type` is never called. - /// - /// This allows the OS/compositor to display the correct UI, indicating that the dragged data - /// can _not_ be dropped. - /// - /// This will ensure that the OS/compositor indicates to the user that dropping the dragged data - /// is not possible. - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - Err(UnknownDataTransfer(id)) - } } impl_dyn_casting!(Window); diff --git a/winit-wayland/src/data_device.rs b/winit-wayland/src/data_device.rs new file mode 100644 index 0000000000..becfda56ea --- /dev/null +++ b/winit-wayland/src/data_device.rs @@ -0,0 +1,46 @@ +impl DataOfferHandler for WinitState { + fn source_actions( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + let seat = offer.winit_data().seat(); + let seat_state = match self.seats.get(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received pointer event without seat"); + return; + }, + }; + + let themed_pointer = match seat_state.pointer.as_ref() { + Some(pointer) => pointer, + None => { + warn!("Received pointer event without pointer"); + return; + }, + }; + + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + todo!() + } + + fn selected_action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut sctk::data_device_manager::data_offer::DragOffer, + actions: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + todo!() + } +} diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs new file mode 100644 index 0000000000..8c02825c03 --- /dev/null +++ b/winit-wayland/src/dnd.rs @@ -0,0 +1,653 @@ +use std::ffi::OsString; +use std::fmt; +use std::fs::File; +use std::io::{self, BufRead, BufReader, Read}; +use std::ops::Deref; +use std::os::fd::OwnedFd; +use std::sync::Arc; + +use dpi::{LogicalPosition, PhysicalPosition}; +use sctk::data_device_manager::data_device::{DataDeviceData, DataDeviceHandler}; +use sctk::data_device_manager::data_offer::{DataOfferHandler, DragOffer}; +use sctk::data_device_manager::data_source::DataSourceHandler; +use wayland_client::protocol::wl_data_device::WlDataDevice; +use wayland_client::protocol::wl_data_device_manager::DndAction; +use wayland_client::protocol::wl_data_offer::WlDataOffer; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::{Connection, Proxy, QueueHandle}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use winit_core::event::WindowEvent; +use winit_core::event_loop::{DndActionMask, DndActions}; + +use crate::state::WinitState; + +impl DataSourceHandler for WinitState { + fn accept_mime( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + mime: Option, + ) { + let _ = mime; + let _ = source; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } + + fn send_request( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + mime: String, + fd: sctk::data_device_manager::WritePipe, + ) { + let _ = fd; + let _ = mime; + let _ = source; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } + + fn cancelled( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } + + fn dnd_dropped( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } + + fn dnd_finished( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } + + fn action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + action: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + let _ = action; + let _ = source; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +enum Charset { + Utf8, + Utf16, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct MimeType { + mime: Arc, + hint: Option, +} + +// MIME types +// Files +const TEXT_URI_LIST: &str = "text/uri-list"; +// Plaintext +const TEXT_PLAIN: &str = "text/plain"; +const TEXT_PLAIN_CHARSET_UTF8: &str = "text/plain;charset=utf-8"; +// HTML +const TEXT_HTML: &str = "text/html"; +const TEXT_HTML_CHARSET_UTF8: &str = "text/html;charset=utf-8"; +// RTF +const APPLICATION_RTF: &str = "application/rtf"; +// Audio +const AUDIO_AAC: &str = "audio/aac"; +const AUDIO_AIFF: &str = "audio/aiff"; +const AUDIO_FLAC: &str = "audio/flac"; +const AUDIO_WAV: &str = "audio/wav"; +const AUDIO_WAVE: &str = "audio/wave"; +const AUDIO_X_WAV: &str = "audio/x-wav"; +const AUDIO_VND_WAV: &str = "audio/vnd.wav"; +const AUDIO_VND_WAVE: &str = "audio/vnd.wave"; +const AUDIO_MPEG: &str = "audio/mpeg"; +const AUDIO_OGG: &str = "audio/ogg"; +// Image +const IMAGE_BMP: &str = "image/bmp"; +const IMAGE_GIF: &str = "image/gif"; +const IMAGE_JPEG: &str = "image/jpeg"; +const IMAGE_PJPEG: &str = "image/pjpeg"; +const IMAGE_PNG: &str = "image/png"; +const IMAGE_SVG: &str = "image/svg+xml"; +const IMAGE_TIFF: &str = "image/tiff"; +const IMAGE_WEBP: &str = "image/webp"; +const IMAGE_X_ICON: &str = "image/x-icon"; +const IMAGE_RAW: &str = "image/x-panasonic-raw"; + +impl MimeType { + const MIME_HINT_MAP: &[(&str, TypeHint)] = &[ + // Files + (TEXT_URI_LIST, TypeHint::UriList), + // Plaintext + (TEXT_PLAIN, TypeHint::Plaintext), + (TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext), + // HTML + (TEXT_HTML, TypeHint::Html), + (TEXT_HTML_CHARSET_UTF8, TypeHint::Html), + // RTF + (APPLICATION_RTF, TypeHint::Rtf), + // Audio + (AUDIO_AAC, TypeHint::Audio { extension_hint: Some("aac") }), + (AUDIO_AIFF, TypeHint::Audio { extension_hint: Some("aif") }), + (AUDIO_FLAC, TypeHint::Audio { extension_hint: Some("flac") }), + (AUDIO_VND_WAV, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_VND_WAVE, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_WAV, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_WAVE, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_X_WAV, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_OGG, TypeHint::Audio { extension_hint: Some("ogg") }), + (AUDIO_MPEG, TypeHint::Audio { extension_hint: Some("mp3") }), + // Image + (IMAGE_BMP, TypeHint::Image { extension_hint: Some("bmp") }), + (IMAGE_GIF, TypeHint::Image { extension_hint: Some("gif") }), + (IMAGE_JPEG, TypeHint::Image { extension_hint: Some("jpg") }), + (IMAGE_PJPEG, TypeHint::Image { extension_hint: Some("jpg") }), + (IMAGE_PNG, TypeHint::Image { extension_hint: Some("png") }), + (IMAGE_RAW, TypeHint::Image { extension_hint: Some("raw") }), + (IMAGE_SVG, TypeHint::Image { extension_hint: Some("svg") }), + (IMAGE_TIFF, TypeHint::Image { extension_hint: Some("tiff") }), + (IMAGE_WEBP, TypeHint::Image { extension_hint: Some("webp") }), + (IMAGE_X_ICON, TypeHint::Image { extension_hint: Some("ico") }), + ]; + + fn charset(&self) -> Option { + let (_essence, options) = self.mime.split_once(';')?; + + let (_, charset) = options.split_once("charset=")?; + + if charset.starts_with("utf-8") { + Some(Charset::Utf8) + } else if charset.starts_with("utf-16") { + Some(Charset::Utf16) + } else { + None + } + } + + fn parse(mime: String) -> Self { + let hint = Self::MIME_HINT_MAP + .iter() + .find_map(|(haystack, hint)| (*haystack == &*mime).then_some(*hint)) + .or_else(|| { + if mime.starts_with("image/") { + Some(TypeHint::Image { extension_hint: None }) + } else if mime.starts_with("audio/") { + Some(TypeHint::Audio { extension_hint: None }) + } else { + None + } + }); + + Self { mime: mime.into(), hint } + } +} + +impl fmt::Display for MimeType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.mime.fmt(f) + } +} + +pub struct UnknownTypeHint(pub TypeHint); + +impl TryFrom for MimeType { + type Error = UnknownTypeHint; + + fn try_from(hint: TypeHint) -> Result { + let mime = Self::MIME_HINT_MAP + .iter() + .find_map(|(mime, haystack)| (*haystack == hint).then_some(*mime)) + .ok_or(UnknownTypeHint(hint))?; + + Ok(Self { mime: mime.to_owned().into(), hint: Some(hint) }) + } +} + +impl TransferType for MimeType { + fn hint(&self) -> Option { + self.hint + } + + fn matches(&self, other: &dyn TransferType) -> bool { + if let Some(other_mime) = other.cast_ref::() { + *self == *other_mime + } else { + // If either hint is `None`, return false + self.hint().is_some_and(|hint| other.hint() == Some(hint)) + } + } +} + +#[derive(Debug)] +pub struct MimeData { + mime_type: MimeType, + fd: Option, +} + +impl MimeData { + pub(crate) fn new(fd: OwnedFd, mime_type: MimeType) -> Self { + Self { mime_type, fd: Some(fd) } + } + + fn try_as_file(&mut self) -> Option { + let fd_clone = + // TODO: Is it ok that this may only work once, depending on what the fd points to? + if let Ok(cloned) = self.fd.as_ref()?.try_clone() { cloned } else { self.fd.take()? }; + self.fd.take().map(Into::into) + } +} + +impl TypedData for MimeData { + fn type_(&self) -> &dyn TransferType { + &self.mime_type + } + + fn try_read(&mut self) -> Option> { + dbg!(&self.mime_type); + Some(Box::new(BufReader::new(self.try_as_file()?))) + } + + fn try_as_uris(&mut self) -> io::Result> { + dbg!(&self.mime_type); + let Some(file) = self.try_as_file() else { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "This `MimeData` was already read, and the underlying file descriptor does not \ + support cloning", + )); + }; + + BufReader::new(file).lines().map(|res| res.map(OsString::from)).collect() + } + + fn try_as_string(&mut self) -> io::Result { + let Some(mut file) = self.try_as_file() else { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "This `MimeData` was already read, and the underlying file descriptor does not \ + support cloning", + )); + }; + + // Default charset is UTF-16 for some reason + let charset = self.mime_type.charset().unwrap_or(Charset::Utf16); + + match charset { + Charset::Utf8 => { + let mut out = String::new(); + file.read_to_string(&mut out)?; + + Ok(out) + }, + Charset::Utf16 => { + let mut bytes = Vec::::new(); + file.read_to_end(&mut bytes)?; + + // TODO: `from_utf16le` once it's stable + let utf_16 = bytes + .chunks_exact(2) + .map(|chunk| { + let arr: [u8; 2] = chunk.try_into().unwrap(); + u16::from_le_bytes(arr) + }) + .collect::>(); + + String::from_utf16(&utf_16) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + }, + } + } +} + +#[derive(Debug, Clone)] +pub struct CurrentDrag { + mime_types: Arc<[MimeType]>, + accepted_type: Option, + data: WlDataOffer, + transfer_id: DataTransferId, +} + +impl CurrentDrag { + pub(crate) fn transfer_id(&self) -> DataTransferId { + self.transfer_id + } + + pub(crate) fn set_actions(&self, action_set: &DndActionSet) { + self.data.set_actions(action_set.dnd_actions, action_set.preferred_action()); + } + + pub(crate) fn find_type_dyn<'a>(&'a self, type_: &'a dyn TransferType) -> Option<&'a MimeType> { + match type_.cast_ref::() { + Some(mime_type) => Some(mime_type), + None => { + let hint = type_.hint()?; + self.mime_types.iter().find(|mime_type| { + mime_type.hint().is_some_and(|haystack| haystack.matches(&hint)) + }) + }, + } + } +} + +impl Deref for CurrentDrag { + type Target = WlDataOffer; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DataTransfer for CurrentDrag { + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, + ) { + let _ = self.mime_types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func); + } +} + +#[derive(Debug, Default)] +pub struct DndState { + current_drag: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct DndActionSet { + /// The set of available actions. + pub dnd_actions: DndAction, + /// If set, the preferred action. + preferred_action: Option, +} + +fn guess_preferred_action(action: DndAction) -> DndAction { + [DndAction::Move, DndAction::Copy, DndAction::Ask] + .into_iter() + .find(|preferred| preferred.intersects(action)) + .unwrap_or(DndAction::empty()) +} + +impl DndActionSet { + pub fn all() -> Self { + Self { dnd_actions: DndAction::all(), preferred_action: None } + } + + pub(crate) fn from_dyn(mask: &dyn DndActionMask) -> Self { + mask.cast_ref::().copied().unwrap_or_else(|| mask.hint().into()) + } + + pub fn preferred_action(&self) -> DndAction { + self.preferred_action.unwrap_or_else(|| guess_preferred_action(self.dnd_actions)) + } + + pub fn intersection(&self, other: &Self) -> Self { + let preferred_action = match (self.preferred_action, other.preferred_action) { + (Some(this_pref), Some(other_pref)) if this_pref.intersects(other_pref) => { + Some(this_pref.intersection(other_pref)) + }, + (Some(pref), None) | (None, Some(pref)) => Some(pref), + // If the preferences do not intersect, calculate it from the actions. + _ => None, + }; + + let dnd_actions = self.dnd_actions.intersection(other.dnd_actions); + + Self { dnd_actions, preferred_action } + } +} + +impl DndActionMask for DndActionSet { + fn hint(&self) -> DndActions { + if self.dnd_actions.is_all() { + DndActions::All + } else { + DndActions::Flags { + move_: self.dnd_actions.contains(DndAction::Move), + copy: self.dnd_actions.contains(DndAction::Copy), + link: false, + } + } + } + + fn intersection(&self, other: &dyn DndActionMask) -> Box { + Box::new(self.intersection(&Self::from_dyn(other))) + } + + fn is_empty(&self) -> bool { + self.dnd_actions.is_empty() + } + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + !self.intersection(&Self::from_dyn(other)).is_empty() + } +} + +impl From for DndActionSet { + fn from(value: DndActions) -> Self { + let copy_flag = if value.copy() { DndAction::Copy } else { DndAction::empty() }; + let move_flag = if value.move_() { DndAction::Move } else { DndAction::empty() }; + + DndActionSet { dnd_actions: copy_flag | move_flag, preferred_action: None } + } +} + +impl DndState { + pub(crate) fn current_drag(&self) -> Option<&CurrentDrag> { + self.current_drag.as_ref() + } + + pub(crate) fn accept_type(&mut self, mime_type: MimeType) { + if let Some(cur) = self.current_drag.as_mut() { + cur.accepted_type = Some(mime_type); + } + } +} + +impl DataOfferHandler for WinitState { + fn source_actions( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } + + fn selected_action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } +} + +impl DataDeviceHandler for WinitState { + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + data_device: &WlDataDevice, + x: f64, + y: f64, + wl_surface: &WlSurface, + ) { + let Some(data) = data_device.data::() else { + return; + }; + let Some(drag) = data.drag_offer() else { + // Selections are not yet implemented + return; + }; + + let window_id = crate::make_wid(wl_surface); + + let current_drag = drag.with_mime_types(|types| CurrentDrag { + mime_types: types + .iter() + .map(|str| MimeType::parse(str.clone())) + .collect::>() + .into(), + transfer_id: DataTransferId::from_raw(drag.serial as i64), + data: drag.inner().clone(), + accepted_type: None, + }); + + current_drag.set_actions(&DndActionSet::all()); + + self.dnd_state.current_drag = Some(current_drag); + + let scale_factor = self + .windows + .borrow() + .get(&window_id) + .map(|window| window.lock().unwrap().scale_factor()) + .unwrap_or(1.); + let position: PhysicalPosition = LogicalPosition::new(x, y).to_physical(scale_factor); + + self.events_sink.push_window_event( + WindowEvent::DragEntered { + id: DataTransferId::from_raw(drag.serial.into()), + position: Some(position), + }, + window_id, + ); + } + + fn leave(&mut self, _: &Connection, _: &QueueHandle, data_device: &WlDataDevice) { + let Some(data) = data_device.data::() else { + return; + }; + let Some(drag) = data.drag_offer() else { + // Selections are not yet implemented + return; + }; + + let window_id = crate::make_wid(&drag.surface); + + self.events_sink.push_window_event( + WindowEvent::DragLeft { id: DataTransferId::from_raw(drag.serial.into()) }, + window_id, + ); + + self.dnd_state.current_drag = None; + } + + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + data_device: &WlDataDevice, + x: f64, + y: f64, + ) { + let Some(data) = data_device.data::() else { + return; + }; + let Some(drag) = data.drag_offer() else { + // Selections are not yet implemented + return; + }; + + let window_id = crate::make_wid(&drag.surface); + + let scale_factor = self + .windows + .borrow() + .get(&window_id) + .map(|window| window.lock().unwrap().scale_factor()) + .unwrap_or(1.); + let position: PhysicalPosition = LogicalPosition::new(x, y).to_physical(scale_factor); + + self.events_sink.push_window_event( + WindowEvent::DragPosition { + id: DataTransferId::from_raw(drag.serial.into()), + position, + }, + window_id, + ); + + self.dnd_state.current_drag = None; + } + + fn selection(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) { + // We don't handle selections right now. + } + + fn drop_performed( + &mut self, + _: &Connection, + _: &QueueHandle, + data_device: &WlDataDevice, + ) { + let Some(data) = data_device.data::() else { + return; + }; + let Some(drag) = data.drag_offer() else { + // Selections (copy/paste) are not yet implemented + return; + }; + + let window_id = crate::make_wid(&drag.surface); + + self.events_sink.push_window_event( + WindowEvent::DragDropped { id: DataTransferId::from_raw(drag.serial.into()) }, + window_id, + ); + + self.dnd_state.current_drag().inspect(|current_drag| { + current_drag + .accept(drag.serial, current_drag.accepted_type.as_ref().map(ToString::to_string)); + current_drag.destroy(); + }); + + self.dnd_state.current_drag = None; + } +} + +sctk::delegate_data_device!(WinitState); diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 3a6ca6982b..2c50eb33e3 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -14,21 +14,24 @@ use calloop::ping::Ping; use dpi::LogicalSize; use rustix::event::{PollFd, PollFlags}; use rustix::pipe::{self, PipeFlags}; +use sctk::data_device_manager::data_offer; use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::{Connection, QueueHandle, globals}; use tracing::warn; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError}; use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, - OwnedDisplayHandle as CoreOwnedDisplayHandle, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, + OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::Theme; +use crate::dnd::{DndActionSet, MimeData, MimeType}; use crate::types::cursor::WaylandCustomCursor; mod proxy; @@ -678,6 +681,71 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result, RequestError> { + let mut state = self.state.borrow_mut(); + let Some(current_drag) = state.dnd_state.current_drag() else { + return Err(RequestError::Ignored); + }; + + if current_drag.transfer_id() != id { + return Err(RequestError::Ignored); + } + + let Some(mime_type) = current_drag.find_type_dyn(type_) else { + return Err(RequestError::Ignored); + }; + + let mime_type_str = mime_type.to_string(); + + // create a pipe + let (readfd, writefd) = + pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK).map_err(|e| os_error!(e))?; + + data_offer::receive_to_fd(current_drag, mime_type_str, writefd); + + let mime_type = mime_type.clone(); + + state.dnd_state.accept_type(mime_type.clone()); + + Ok(Box::new(MimeData::new(readfd, mime_type))) + } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let state = self.state.borrow(); + let Some(state) = state.dnd_state.current_drag() else { + return Err(RequestError::Ignored); + }; + + if state.transfer_id() != id { + return Err(RequestError::Ignored); + } + + Ok(Box::new(state.clone())) + } + + fn set_valid_actions( + &self, + id: DataTransferId, + mask: &dyn DndActionMask, + ) -> Result<(), UnknownDataTransfer> { + let state = self.state.borrow(); + let Some(state) = state.dnd_state.current_drag() else { + return Err(UnknownDataTransfer(id)); + }; + + if state.transfer_id() != id { + return Err(UnknownDataTransfer(id)); + } + + state.set_actions(&DndActionSet::from_dyn(mask)); + + Ok(()) + } } impl ActiveEventLoop { diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 930562b380..e294222b47 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -34,6 +34,7 @@ macro_rules! os_error { ($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }}; } +mod dnd; mod event_loop; mod output; mod seat; diff --git a/winit-wayland/src/seat/dnd/mod.rs b/winit-wayland/src/seat/dnd/mod.rs deleted file mode 100644 index 406c0c4224..0000000000 --- a/winit-wayland/src/seat/dnd/mod.rs +++ /dev/null @@ -1,182 +0,0 @@ -use sctk::data_device_manager::data_device::DataDeviceHandler; -use sctk::data_device_manager::data_offer::DataOfferHandler; -use sctk::data_device_manager::data_source::DataSourceHandler; -use wayland_client::protocol::wl_data_device::WlDataDevice; -use wayland_client::protocol::wl_surface::WlSurface; -use wayland_client::{Connection, QueueHandle}; - -use crate::state::WinitState; - -impl DataSourceHandler for WinitState { - fn accept_mime( - &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - mime: Option, - ) { - let _ = mime; - let _ = source; - let _ = qh; - let _ = conn; - todo!() - } - - fn send_request( - &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - mime: String, - fd: sctk::data_device_manager::WritePipe, - ) { - let _ = fd; - let _ = mime; - let _ = source; - let _ = qh; - let _ = conn; - todo!() - } - - fn cancelled( - &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - ) { - let _ = source; - let _ = qh; - let _ = conn; - todo!() - } - - fn dnd_dropped( - &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - ) { - let _ = source; - let _ = qh; - let _ = conn; - todo!() - } - - fn dnd_finished( - &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - ) { - let _ = source; - let _ = qh; - let _ = conn; - todo!() - } - - fn action( - &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - action: wayland_client::protocol::wl_data_device_manager::DndAction, - ) { - let _ = action; - let _ = source; - let _ = qh; - let _ = conn; - todo!() - } -} - -impl DataOfferHandler for WinitState { - fn source_actions( - &mut self, - conn: &Connection, - qh: &QueueHandle, - offer: &mut sctk::data_device_manager::data_offer::DragOffer, - actions: wayland_client::protocol::wl_data_device_manager::DndAction, - ) { - let _ = actions; - let _ = offer; - let _ = qh; - let _ = conn; - todo!() - } - - fn selected_action( - &mut self, - conn: &Connection, - qh: &QueueHandle, - offer: &mut sctk::data_device_manager::data_offer::DragOffer, - actions: wayland_client::protocol::wl_data_device_manager::DndAction, - ) { - let _ = actions; - let _ = offer; - let _ = qh; - let _ = conn; - todo!() - } -} - -impl DataDeviceHandler for WinitState { - fn enter( - &mut self, - conn: &Connection, - qh: &QueueHandle, - data_device: &WlDataDevice, - x: f64, - y: f64, - wl_surface: &WlSurface, - ) { - let _ = wl_surface; - let _ = y; - let _ = x; - let _ = data_device; - let _ = qh; - let _ = conn; - todo!() - } - - fn leave(&mut self, conn: &Connection, qh: &QueueHandle, data_device: &WlDataDevice) { - let _ = data_device; - let _ = qh; - let _ = conn; - todo!() - } - - fn motion( - &mut self, - conn: &Connection, - qh: &QueueHandle, - data_device: &WlDataDevice, - x: f64, - y: f64, - ) { - let _ = y; - let _ = x; - let _ = data_device; - let _ = qh; - let _ = conn; - todo!() - } - - fn selection(&mut self, conn: &Connection, qh: &QueueHandle, data_device: &WlDataDevice) { - let _ = data_device; - let _ = qh; - let _ = conn; - todo!() - } - - fn drop_performed( - &mut self, - conn: &Connection, - qh: &QueueHandle, - data_device: &WlDataDevice, - ) { - let _ = data_device; - let _ = qh; - let _ = conn; - todo!() - } -} diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index 31c6c9fa45..20139c4726 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -20,7 +20,6 @@ use winit_core::keyboard::ModifiersState; use crate::state::WinitState; -mod dnd; mod keyboard; mod pointer; mod text_input; diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index 82aba10b60..8c06edc2f8 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -24,6 +24,7 @@ use sctk::subcompositor::SubcompositorState; use winit_core::error::OsError; use crate::WindowId; +use crate::dnd::DndState; use crate::event_loop::sink::EventSink; use crate::output::MonitorHandle; use crate::seat::{ @@ -123,6 +124,9 @@ pub struct WinitState { /// Blur manager. pub blur_manager: Option, + /// Drag-and-drop state. + pub dnd_state: DndState, + /// Loop handle to re-register event sources, such as keyboard repeat. pub loop_handle: LoopHandle<'static, Self>, @@ -210,6 +214,8 @@ impl WinitState { fractional_scaling_manager, blur_manager: BgrEffectManager::new(globals, queue_handle).ok(), + dnd_state: Default::default(), + seats, text_input_state: TextInputState::new(globals, queue_handle).ok(), @@ -468,4 +474,3 @@ sctk::delegate_registry!(WinitState); sctk::delegate_shm!(WinitState); sctk::delegate_xdg_shell!(WinitState); sctk::delegate_xdg_window!(WinitState); -sctk::delegate_data_device!(WinitState); diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index dcac4b69d1..85238177b9 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -35,6 +35,7 @@ use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId, }; +use crate::dnd::DndState; use crate::event_loop::OwnedDisplayHandle; use crate::logical_to_physical_rounded; use crate::seat::{ diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index 0f6097949e..5a09b35101 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -1161,35 +1161,7 @@ impl CoreWindow for Window { self } - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let state = self.window_state.lock().unwrap(); - if let Some(shared) = &state.drop_data_shared { - if shared.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - shared.accepted.store(true, Ordering::Relaxed); - - Ok(()) - } else { - return Err(UnknownDataTransfer(id)); - } - } - - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - let state = self.window_state.lock().unwrap(); - if let Some(shared) = &state.drop_data_shared { - if shared.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - shared.accepted.store(true, Ordering::Relaxed); - - Ok(()) - } else { - return Err(UnknownDataTransfer(id)); - } - } + // TODO: `set_valid_actions` } pub(super) struct InitData<'a> { diff --git a/winit-x11/src/atoms.rs b/winit-x11/src/atoms.rs index f9876cf96f..24d9091766 100644 --- a/winit-x11/src/atoms.rs +++ b/winit-x11/src/atoms.rs @@ -98,9 +98,9 @@ atom_manager! { // MIME types for reading selections TextUriList: b"text/uri-list", TextPlain: b"text/plain", - TextPlainCharsetUtf8: b"text/plain; charset=utf-8", + TextPlainCharsetUtf8: b"text/plain;charset=utf-8", TextHtml: b"text/html", - TextHtmlCharsetUtf8: b"text/html; charset=utf-8", + TextHtmlCharsetUtf8: b"text/html;charset=utf-8", ApplicationRtf: b"application/rtf", AudioAac: b"audio/aac", AudioAiff: b"audio/aiff", diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 1cb34807fc..d2476ac5e9 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -263,23 +263,15 @@ impl TypedData for SelectionReader { Some(TypeHint::Plaintext) | Some(TypeHint::Html) => { let data = self.data.try_data()?; - // Bad way to detect UTF-16 - some applications (confirmed to at least happen with - // Firefox) don't emit a BOM when passing HTML, so we need to check: - // A) Does the string contain a null - // B) Can the string be decoded as UTF-8 - if data.contains(&0) { - decode_utf16_bytes(data) - // Even if we guess that it's utf-16, we'll still try utf-8 just in case - .or_else(|_| { - std::str::from_utf8(data) - .map(|str| str.to_owned()) - .map_err(invalid_data) - }) - } else { - std::str::from_utf8(data) + // TODO: Is it correct to default to UTF-8? + let charset = self.type_.charset.unwrap_or(Charset::Utf8); + + match charset { + Charset::Utf16 => decode_utf16_bytes(data), + Charset::Utf8 => std::str::from_utf8(data) .map(|str| str.to_owned()) .map_err(invalid_data) - .or_else(|_| decode_utf16_bytes(data)) + .or_else(|_| decode_utf16_bytes(data)), } }, Some(TypeHint::UriList) => { @@ -326,30 +318,11 @@ impl SelectionFetchState { } } -#[derive(Default, Debug)] -pub struct DndSharedState { - transfer_id: AtomicI64, - /// Whether the drag operation is accepted (or `None` if the user never indicated that it's - /// accepted or rejected) - // Populated by `Window::accept_drag`/`Window::reject_drag`. - pub accepted: AtomicBool, -} - -impl DndSharedState { - fn reset(&self) { - self.transfer_id.fetch_add(1, Ordering::Relaxed); - self.accepted.store(false, Ordering::Relaxed); - } - - pub fn transfer_id(&self) -> DataTransferId { - DataTransferId::from_raw(self.transfer_id.load(Ordering::Relaxed)) - } -} - -#[derive(Default, Debug)] +#[derive(Debug)] pub struct DragState { // Populated by XdndEnter event handler pub version: c_long, + pub transfer_id: DataTransferId, pub types: Arc<[SelectionType]>, // Populated by Xdnd* event handlers pub source_window: xproto::Window, @@ -357,12 +330,31 @@ pub struct DragState { pub target_window: xproto::Window, // Populated by `fetch_data_transfer` pub last_fetched_selection: Option, + /// Whether the drag operation is accepted (or `None` if the user never indicated that it's + /// accepted or rejected) + // Populated by `Window::accept_drag`/`Window::reject_drag`. + pub accepted: bool, +} + +impl Default for DragState { + fn default() -> Self { + static DATA_TRANSFER_ID: AtomicI64 = AtomicI64::new(0); + + Self { + version: Default::default(), + transfer_id: DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)), + types: Default::default(), + source_window: Default::default(), + target_window: Default::default(), + last_fetched_selection: Default::default(), + accepted: Default::default(), + } + } } #[derive(Debug)] pub struct Dnd { xconn: Arc, - pub shared: Arc, pub deadlock_sentinel: DeadlockSentinel, // If `None`, no drag operation is in progress. pub state: Option, @@ -378,57 +370,66 @@ impl Selection { Selection { types } } } +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +enum Charset { + Utf8, + Utf16, +} #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct SelectionType { hint: Option, atom: xproto::Atom, + charset: Option, } impl SelectionType { pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self { let atom_to_hint = [ // Files - (atoms[TextUriList], TypeHint::UriList), - (atoms[TARGETS], TypeHint::UriList), - (atoms[SAVE_TARGETS], TypeHint::UriList), + (atoms[TextUriList], TypeHint::UriList, None), + (atoms[TARGETS], TypeHint::UriList, None), + (atoms[SAVE_TARGETS], TypeHint::UriList, None), // Plaintext - (atoms[STRING], TypeHint::Plaintext), - (atoms[UTF8_STRING], TypeHint::Plaintext), - (atoms[TextPlain], TypeHint::Plaintext), - (atoms[TextPlainCharsetUtf8], TypeHint::Plaintext), + (atoms[STRING], TypeHint::Plaintext, None), + (atoms[UTF8_STRING], TypeHint::Plaintext, None), + (atoms[TextPlain], TypeHint::Plaintext, Some(Charset::Utf16)), + (atoms[TextPlainCharsetUtf8], TypeHint::Plaintext, Some(Charset::Utf8)), // HTML - (atoms[TextHtml], TypeHint::Html), - (atoms[TextHtmlCharsetUtf8], TypeHint::Html), + (atoms[TextHtml], TypeHint::Html, Some(Charset::Utf16)), + (atoms[TextHtmlCharsetUtf8], TypeHint::Html, Some(Charset::Utf8)), // RTF - (atoms[ApplicationRtf], TypeHint::Rtf), + (atoms[ApplicationRtf], TypeHint::Rtf, None), // Audio - (atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }), - (atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }), - (atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }), - (atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }), - (atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }), - (atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }), - (atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }), - (atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }), - (atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }), - (atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }), + (atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }, None), + (atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }, None), + (atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }, None), + (atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }, None), + (atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }, None), // Image - (atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }), - (atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }), - (atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }), - (atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }), - (atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }), - (atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }), - (atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }), - (atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }), - (atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }), - (atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }), + (atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }, None), + (atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }, None), + (atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }, None), + (atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }, None), + (atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }, None), + (atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }, None), + (atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }, None), + (atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }, None), + (atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }, None), + (atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }, None), ]; - let hint = - atom_to_hint.iter().find_map(|(haystack, hint)| (*haystack == atom).then_some(*hint)); + let hint_and_charset = atom_to_hint.iter().find_map(|(haystack, hint, charset)| { + (*haystack == atom).then_some((Some(*hint), *charset)) + }); + + let (hint, charset) = hint_and_charset.unwrap_or((None, None)); - Self { hint, atom } + Self { hint, charset, atom } } pub fn atom(&self) -> xproto::Atom { @@ -440,6 +441,15 @@ impl TransferType for SelectionType { fn hint(&self) -> Option { self.hint } + + fn matches(&self, other: &dyn TransferType) -> bool { + if let Some(other_mime) = other.cast_ref::() { + *self == *other_mime + } else { + // If either hint is `None`, return false + self.hint().is_some_and(|hint| other.hint() == Some(hint)) + } + } } impl DataTransfer for Selection { @@ -447,43 +457,20 @@ impl DataTransfer for Selection { &'this self, func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, ) { - for ty in &self.types[..] { - if let ControlFlow::Break(()) = func(ty) { - break; - } - } - } - - fn has_type(&self, type_: &dyn TransferType) -> bool { - if let Some(x11_type) = type_.cast_ref() { - self.types.iter().any(|haystack| haystack == x11_type) - } else { - let Some(hint) = type_.hint() else { - return false; - }; - - self.types.iter().any(|haystack| haystack.hint().is_some_and(|hs| hs.matches(&hint))) - } + let _ = self.types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func); } } impl Dnd { pub fn new(xconn: Arc, deadlock_sentinel: DeadlockSentinel) -> Self { - let shared = Arc::new(Default::default()); - - Dnd { xconn, shared, state: None, deadlock_sentinel } + Dnd { xconn, state: None, deadlock_sentinel } } pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> { self.state.as_ref()?.types.iter().find(|haystack| haystack.hint() == Some(hint)) } - pub fn transfer_id(&self) -> DataTransferId { - DataTransferId::from_raw(self.shared.transfer_id.load(Ordering::Relaxed)) - } - pub fn reset(&mut self) { - self.shared.reset(); self.state = None; } @@ -499,7 +486,7 @@ impl Dnd { types, source_window, target_window, - last_fetched_selection: None, + ..Default::default() }) } @@ -509,11 +496,13 @@ impl Dnd { target_window: xproto::Window, ) -> Result<(), X11Error> { let atoms = self.xconn.atoms(); - let (accepted, action) = if self.shared.accepted.load(Ordering::Relaxed) { - (1, atoms[XdndActionPrivate]) - } else { - (0, atoms[DndNone]) + let Some(state) = &self.state else { + return Err(X11Error::UnexpectedNull( + "Drag-and-drop state was not initialized (called `send_finished` before XdndEnter", + )); }; + let (accepted, action) = + if state.accepted { (1, atoms[XdndActionPrivate]) } else { (0, atoms[DndNone]) }; self.xconn .send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [ this_window, diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 4538b6c3f8..3e7ad3406c 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -24,9 +24,9 @@ use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::event::{DeviceId, StartCause, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider, - OwnedDisplayHandle as CoreOwnedDisplayHandle, + OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::{Theme, Window as CoreWindow, WindowAttributes, WindowId}; @@ -769,7 +769,7 @@ impl RootActiveEventLoop for ActiveEventLoop { fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { let dnd = self.dnd.borrow(); - if dnd.transfer_id() != id { + if !dnd.state.as_ref().is_some_and(|state| state.transfer_id == id) { return Err(RequestError::Ignored); } @@ -787,7 +787,7 @@ impl RootActiveEventLoop for ActiveEventLoop { ) -> Result, RequestError> { let mut dnd = self.dnd.borrow_mut(); - if dnd.transfer_id() != id { + if !dnd.state.as_ref().is_some_and(|state| state.transfer_id == id) { return Err(RequestError::NotSupported(NotSupportedError::new( "Unknown data transfer", ))); @@ -832,6 +832,26 @@ impl RootActiveEventLoop for ActiveEventLoop { Ok(Box::new(reader)) } + + fn set_valid_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> Result<(), UnknownDataTransfer> { + let mut dnd = self.dnd.borrow_mut(); + + let Some(state) = &mut dnd.state else { + return Err(UnknownDataTransfer(id)); + }; + + if state.transfer_id != id { + return Err(UnknownDataTransfer(id)); + } + + state.accepted = actions.hint().any(); + + Ok(()) + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 906de52830..201c7abd1c 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -457,8 +457,7 @@ impl EventProcessor { Default::default() }; - dnd.init_state(version, source_window, window, types.into()); - dnd.transfer_id() + dnd.init_state(version, source_window, window, types.into()).transfer_id }; app.window_event(&self.target, window_id, WindowEvent::DragEntered { @@ -496,11 +495,11 @@ impl EventProcessor { // never contending the lock. let transfer_id = { let dnd = self.target.dnd.borrow(); + let Some(state) = &dnd.state else { + return; + }; // By our own state flow, `state` should never be `None` at this point. - let version = dnd.state.as_ref().map(|s| s.version).unwrap_or_else(|| { - warn!("Received `XdndPosition` without `XdndEnter`"); - 5 - }); + let version = state.version; let time = if version == 0 { // In version 0, time isn't specified @@ -516,16 +515,12 @@ impl EventProcessor { dnd.send_status( window, source_window, - if dnd.shared.accepted.load(Ordering::Relaxed) { - DndState::Accepted - } else { - DndState::Rejected - }, + if state.accepted { DndState::Accepted } else { DndState::Rejected }, ) .expect("Failed to send `XdndStatus` message."); } - dnd.transfer_id() + state.transfer_id }; app.window_event(&self.target, window_id, WindowEvent::DragPosition { @@ -539,12 +534,15 @@ impl EventProcessor { if xev.message_type == atoms[XdndDrop] as c_ulong { let (source_window, transfer_id) = { let dnd = self.target.dnd.borrow(); + let Some(state) = &dnd.state else { + return; + }; let Some(source_window) = dnd.state.as_ref().map(|s| s.source_window) else { warn!("Received `XdndDrop` without `XdndEnter`"); return; }; - (source_window, dnd.transfer_id()) + (source_window, state.transfer_id) }; app.window_event(&self.target, window_id, WindowEvent::DragDropped { id: transfer_id }); @@ -562,8 +560,13 @@ impl EventProcessor { } if xev.message_type == atoms[XdndLeave] as c_ulong { - let transfer_id = self.target.dnd.borrow().transfer_id(); - app.window_event(&self.target, window_id, WindowEvent::DragLeft { id: transfer_id }); + let dnd = self.target.dnd.borrow(); + let Some(state) = &dnd.state else { + return; + }; + app.window_event(&self.target, window_id, WindowEvent::DragLeft { + id: state.transfer_id, + }); } } diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index ba81850f69..4c1a0f3562 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -5,7 +5,6 @@ use std::num::NonZeroU32; use std::ops::Deref; use std::os::raw::*; use std::path::Path; -use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex, MutexGuard}; use std::{cmp, env}; @@ -13,7 +12,6 @@ use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use tracing::{debug, info, warn}; use winit_core::application::ApplicationHandler; use winit_core::cursor::Cursor; -use winit_core::data_transfer::DataTransferId; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::AsyncRequestSerial; @@ -23,8 +21,8 @@ use winit_core::monitor::{ }; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest as CoreImeRequest, ImeRequestError, - ResizeDirection, Theme, UnknownDataTransfer, UserAttentionType, Window as CoreWindow, - WindowAttributes, WindowButtons, WindowId, WindowLevel, + ResizeDirection, Theme, UserAttentionType, Window as CoreWindow, WindowAttributes, + WindowButtons, WindowId, WindowLevel, }; use x11rb::connection::{Connection, RequestConnection}; use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; @@ -41,7 +39,6 @@ use crate::atoms::{ _NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE, WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware, }; -use crate::dnd::DndSharedState; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, WakeSender, X11Error, xinput_fp1616_to_float, @@ -312,26 +309,6 @@ impl CoreWindow for Window { fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { self } - - fn reject_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - if self.dnd_shared.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - self.dnd_shared.accepted.store(false, Ordering::Relaxed); - - Ok(()) - } - - fn accept_drag(&self, id: DataTransferId) -> Result<(), UnknownDataTransfer> { - if self.dnd_shared.transfer_id() != id { - return Err(UnknownDataTransfer(id)); - } - - self.dnd_shared.accepted.store(true, Ordering::Relaxed); - - Ok(()) - } } impl rwh_06::HasDisplayHandle for Window { @@ -443,11 +420,10 @@ unsafe impl Sync for UnownedWindow {} #[derive(Debug)] pub struct UnownedWindow { pub(crate) xconn: Arc, // never changes - dnd_shared: Arc, - xwindow: xproto::Window, // never changes + xwindow: xproto::Window, // never changes #[allow(dead_code)] visual: u32, // never changes - root: xproto::Window, // never changes + root: xproto::Window, // never changes #[allow(dead_code)] screen_id: i32, // never changes sync_counter_id: Option, // never changes @@ -669,12 +645,9 @@ impl UnownedWindow { .visual; } - let dnd_shared = event_loop.dnd.borrow().shared.clone(); - #[allow(clippy::mutex_atomic)] let mut window = UnownedWindow { xconn: Arc::clone(xconn), - dnd_shared, xwindow: xwindow as xproto::Window, visual, root, diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index f7aecee1fb..5ff0b1ca58 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -4,7 +4,7 @@ use tracing::{error, info, warn}; use winit::application::ApplicationHandler; use winit::data_transfer::{TypeHint, TypedData}; use winit::event::WindowEvent; -use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::event_loop::{ActiveEventLoop, EventLoop, DndActions}; use winit::window::{Window, WindowAttributes, WindowId}; #[path = "util/fill.rs"] @@ -95,24 +95,18 @@ impl ApplicationHandler for Application { "Types: {:#?}", data_transfer .available_types() - .into_iter() - .filter_map(|ty| ty.hint()) - .collect::>() ); - let wanted_types = [TypeHint::Html, TypeHint::UriList, TypeHint::Plaintext] + let valid_type = [TypeHint::Html, TypeHint::UriList, TypeHint::Plaintext] .into_iter() - .filter(|ty| data_transfer.has_type(ty)) - .collect::>(); + .find(|ty| data_transfer.has_type(ty)); - info!("Supported types: {:#?}", wanted_types); - - let Some(type_) = wanted_types.first().copied() else { - window.reject_drag(id).unwrap(); + let Some(type_) = valid_type else { + event_loop.set_valid_actions(id, &DndActions::none()).unwrap(); return; }; - window.accept_drag_type(id, &type_).unwrap(); + event_loop.set_valid_actions(id, &DndActions::all()).unwrap(); self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok(); From 9966b836c1a07e16604628c21e28defb722ed53a Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Mon, 1 Jun 2026 15:57:28 +0200 Subject: [PATCH 45/87] Fix appkit --- winit-appkit/src/app_state.rs | 23 +++++-- winit-appkit/src/dnd.rs | 101 ++++++++++++++++++++-------- winit-appkit/src/event_loop.rs | 28 +++++++- winit-appkit/src/window.rs | 5 +- winit-appkit/src/window_delegate.rs | 86 ++++++++++------------- winit-x11/src/dnd.rs | 3 +- winit-x11/src/event_processor.rs | 1 - winit/examples/dnd.rs | 12 +--- 8 files changed, 156 insertions(+), 103 deletions(-) diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index 8fa2d81f75..bd48fc9418 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -11,6 +11,7 @@ use objc2_foundation::NSNotification; use winit_common::core_foundation::{EventLoopProxy, MainRunLoop}; use winit_common::event_handler::EventHandler; use winit_core::application::ApplicationHandler; +use winit_core::data_transfer::DataTransferId; use winit_core::event::{StartCause, WindowEvent}; use winit_core::event_loop::ControlFlow; use winit_core::window::WindowId; @@ -18,12 +19,13 @@ use winit_core::window::WindowId; use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately}; use super::menu; use super::observer::EventLoopWaker; -use crate::dnd::DndState; +use crate::dnd::{DragOperation, Pasteboards}; #[derive(Debug)] pub(super) struct AppState { mtm: MainThreadMarker, - dnd: DndState, + drag_state: Cell>, + pasteboards: Pasteboards, activation_policy: Option, default_menu: bool, activate_ignoring_other_apps: bool, @@ -49,6 +51,12 @@ pub(super) struct AppState { // as such should be careful to not add fields that, in turn, strongly reference those. } +#[derive(Copy, Clone, Debug)] +pub(crate) struct DragState { + pub id: DataTransferId, + pub valid_operations: DragOperation, +} + // SAFETY: Creating `MainThreadBound` in a `const` context, where there is no concept of the // main thread. static GLOBAL: MainThreadBound>> = @@ -67,7 +75,8 @@ impl AppState { let this = Rc::new(Self { mtm, - dnd: Default::default(), + pasteboards: Default::default(), + drag_state: Default::default(), activation_policy, default_menu, activate_ignoring_other_apps, @@ -375,8 +384,12 @@ impl AppState { self.waker.borrow_mut().start_at(min_timeout(wait_timeout, app_timeout)); } - pub fn dnd(&self) -> &DndState { - &self.dnd + pub fn pasteboards(&self) -> &Pasteboards { + &self.pasteboards + } + + pub fn drag_state(&self) -> &Cell> { + &self.drag_state } } diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 5bfafe155f..266a54eacf 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -2,21 +2,21 @@ use std::cell::{OnceCell, RefCell}; use std::collections::HashMap; use std::ffi::OsString; use std::io; -use std::ops::{ControlFlow, Deref}; +use std::ops::Deref; use std::rc::Rc; -use std::sync::atomic::{AtomicI64, Ordering}; use objc2::Message; use objc2::rc::{Retained, Weak}; use objc2_app_kit::{ - NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, + NSDragOperation, NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, }; use objc2_foundation::{NSArray, NSData, NSString}; use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use winit_core::event_loop::{DndActionMask, DndActions}; /// A thin wrapper around [`NSPasteboardType`], implementing [`TransferType`]. -#[derive(Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone)] pub struct PasteboardType { hint: Option, // We need to convert `NSString` to `str` since `NSString` isn't `Send`/`Sync` @@ -81,8 +81,8 @@ impl TransferType for PasteboardType { } fn matches(&self, other: &dyn TransferType) -> bool { - if let Some(other_mime) = other.cast_ref::() { - *self == *other_mime + if let Some(other_pb_type) = other.cast_ref::() { + *self == *other_pb_type } else { // If either hint is `None`, return false self.hint().is_some_and(|hint| other.hint() == Some(hint)) @@ -92,9 +92,9 @@ impl TransferType for PasteboardType { /// A thin wrapper around [`NSPasteboard`], implementing [`DataTransfer`]. #[derive(Clone, Debug)] -pub struct Pasteboard { +pub struct Pasteboard> { transfer_id: DataTransferId, - inner: Retained, + inner: PB, types: OnceCell>, } @@ -116,12 +116,7 @@ impl Pasteboard { self.types.get_or_init(|| { self.inner .types() - .map(|types| { - types - .into_iter() - .map(|pb_type| PasteboardType::from(pb_type)) - .collect::>() - }) + .map(|types| types.into_iter().map(PasteboardType::from).collect::>()) .unwrap_or_default() .into() }) @@ -189,6 +184,65 @@ impl PasteboardTypeSpec { } } +/// A thin wrapper around [`NSDragOperation`], implementing [`DndActionMask`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct DragOperation(pub NSDragOperation); + +impl DragOperation { + pub(crate) fn from_dyn(actions: &dyn DndActionMask) -> Self { + if let Some(op) = actions.cast_ref::() { + *op + } else { + match actions.hint() { + DndActions::Flags { move_, copy, link } => { + let move_flag = + if move_ { NSDragOperation::Move } else { NSDragOperation::empty() }; + let copy_flag = + if copy { NSDragOperation::Copy } else { NSDragOperation::empty() }; + let link_flag = + if link { NSDragOperation::Link } else { NSDragOperation::empty() }; + Self(move_flag | copy_flag | link_flag) + }, + DndActions::All => Self(NSDragOperation::all()), + } + } + } + + fn intersection(&self, other: &Self) -> Self { + Self(self.0.intersection(other.0)) + } + + fn intersects(&self, other: &Self) -> bool { + self.0.intersects(other.0) + } +} + +impl DndActionMask for DragOperation { + fn hint(&self) -> DndActions { + if self.0.is_all() { + DndActions::All + } else { + DndActions::Flags { + move_: self.0.contains(NSDragOperation::Move), + copy: self.0.contains(NSDragOperation::Copy), + link: self.0.contains(NSDragOperation::Link), + } + } + } + + fn intersection(&self, other: &dyn DndActionMask) -> Box { + Box::new(self.intersection(&Self::from_dyn(other))) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + self.intersects(&Self::from_dyn(other)) + } +} + /// A thin wrapper around [`NSPasteboard`], implementing [`TypedValue`]. #[derive(Debug)] pub struct PasteboardValue { @@ -282,7 +336,7 @@ impl TypedData for PasteboardValue { None => { return self .single_file_url() - .map(|str| vec![str]) + .map(|str| vec![str.into()]) .ok_or_else(|| io::ErrorKind::InvalidData.into()); }, }; @@ -300,7 +354,7 @@ impl TypedData for PasteboardValue { Ok(items .into_iter() .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL })) - .map(|ns_str| ns_str.to_string()) + .map(|ns_str| ns_str.to_string().into()) .collect()) } @@ -313,11 +367,11 @@ impl TypedData for PasteboardValue { } #[derive(Debug, Default)] -pub struct DndState { +pub struct Pasteboards { inner: RefCell>>, } -impl DndState { +impl Pasteboards { pub fn remove_deloaded_pasteboards(&self) { self.inner.borrow_mut().retain(|_, v| v.load().is_some()); } @@ -330,15 +384,8 @@ impl DndState { } } - pub fn insert(&self, pb: &Retained) -> DataTransferId { - static TRANSFER_ID: AtomicI64 = AtomicI64::new(0); - - let id = TRANSFER_ID.fetch_add(1, Ordering::Relaxed); - let id = DataTransferId::from_raw(id); - - self.inner.borrow_mut().insert(id, Weak::from_retained(pb)); - - id + pub fn insert(&self, transfer_id: DataTransferId, pb: &Retained) { + self.inner.borrow_mut().insert(transfer_id, Weak::from_retained(pb)); } pub fn get(&self, id: DataTransferId) -> Option { diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index f66caf8541..8108bcea80 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -21,8 +21,9 @@ use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, Type use winit_core::error::{EventLoopError, RequestError}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, EventLoopProxy as CoreEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle, + UnknownDataTransfer, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::Theme; @@ -33,6 +34,8 @@ use super::cursor::CustomCursor; use super::event::dummy_event; use super::monitor; use crate::ActivationPolicy; +use crate::app_state::DragState; +use crate::dnd::DragOperation; use crate::window::Window; #[derive(Debug)] @@ -133,7 +136,7 @@ impl RootActiveEventLoop for ActiveEventLoop { id: DataTransferId, type_: &dyn TransferType, ) -> Result, RequestError> { - let Some(pb) = self.app_state.dnd().get(id) else { + let Some(pb) = self.app_state.pasteboards().get(id) else { return Err(RequestError::Ignored); }; @@ -141,12 +144,31 @@ impl RootActiveEventLoop for ActiveEventLoop { } fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { - let Some(pb) = self.app_state.dnd().get(id) else { + let Some(pb) = self.app_state.pasteboards().get(id) else { return Err(RequestError::Ignored); }; Ok(Box::new(pb)) } + + fn set_valid_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> Result<(), UnknownDataTransfer> { + let Some(drag_state) = self.app_state.drag_state().get() else { + return Err(UnknownDataTransfer(id)); + }; + + if drag_state.id != id { + return Err(UnknownDataTransfer(id)); + } + let new_drag_state = DragState { id, valid_operations: DragOperation::from_dyn(actions) }; + + self.app_state.drag_state().set(Some(new_drag_state)); + + Ok(()) + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index bbd1a392c7..002a620b29 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -10,13 +10,12 @@ use objc2_app_kit::{NSPanel, NSResponder, NSWindow}; use objc2_foundation::NSObject; use tracing::trace_span; use winit_core::cursor::Cursor; -use winit_core::data_transfer::DataTransferId; use winit_core::error::RequestError; use winit_core::icon::Icon; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; use winit_core::window::{ - ImeCapabilities, ImeRequest, ImeRequestError, Theme, UnknownDataTransfer, UserAttentionType, - Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, + ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow, + WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use super::event_loop::ActiveEventLoop; diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 9c06670684..c9c1b46bbe 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -19,12 +19,12 @@ use objc2::{ use objc2_app_kit::{ NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, - NSColor, NSDraggingDestination, NSDraggingInfo, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, - NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, - NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification, - NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, - NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, - NSWindowTitleVisibility, NSWindowToolbarStyle, + NSColor, NSDragOperation, NSDraggingDestination, NSDraggingInfo, NSPasteboardTypeFileURL, + NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, + NSPasteboardTypeTIFF, NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, + NSViewFrameDidChangeNotification, NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, + NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, + NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle, }; use objc2_core_foundation::{CGFloat, CGPoint}; use objc2_core_graphics::{ @@ -60,21 +60,15 @@ use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_di use super::util::cgerr; use super::view::WinitView; use super::window::{WinitPanel, WinitWindow, window_id}; +use crate::app_state::DragState; +use crate::dnd::DragOperation; use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS}; -#[derive(Copy, Clone, Debug)] -struct DragState { - id: DataTransferId, - accepted: bool, -} - #[derive(Debug)] pub(crate) struct State { /// Strong reference to the global application state. app_state: Rc, - drag_state: Cell>, - window: Retained, // During `windowDidResize`, we use this to only send Moved if the position changed. @@ -369,7 +363,7 @@ define_class!( unsafe impl NSDraggingDestination for WindowDelegate { /// Invoked when the dragged image enters destination bounds or frame #[unsafe(method(draggingEntered:))] - fn dragging_entered(&self, sender: &ProtocolObject) -> bool { + fn dragging_entered(&self, sender: &ProtocolObject) -> NSDragOperation { let _entered = debug_span!("draggingEntered:").entered(); let pb = sender.draggingPasteboard(); @@ -380,15 +374,20 @@ define_class!( let vars = self.ivars(); - let transfer_id = vars.app_state.dnd().insert(&pb); - vars.drag_state.set(Some(DragState { id: transfer_id, accepted: false })); + let transfer_id = DataTransferId::from_raw(sender.draggingSequenceNumber() as i64); + vars.app_state.pasteboards().insert(transfer_id, &pb); + let valid_operations = NSDragOperation::empty(); + vars.app_state.drag_state().set(Some(DragState { + id: transfer_id, + valid_operations: DragOperation(valid_operations), + })); self.queue_event(WindowEvent::DragEntered { id: transfer_id, position: Some(position), }); - false + valid_operations } #[unsafe(method(wantsPeriodicDraggingUpdates))] @@ -400,18 +399,20 @@ define_class!( /// Invoked periodically as the image is held within the destination area, allowing /// modification of the dragging operation or mouse-pointer position. #[unsafe(method(draggingUpdated:))] - fn dragging_updated(&self, sender: &ProtocolObject) -> bool { + fn dragging_updated(&self, sender: &ProtocolObject) -> NSDragOperation { let _entered = debug_span!("draggingUpdated:").entered(); let vars = self.ivars(); - let Some(DragState { id: transfer_id, accepted }) = vars.drag_state.get() else { - return false.into(); + let Some(DragState { id: transfer_id, valid_operations }) = + vars.app_state.drag_state().get() + else { + return NSDragOperation::empty(); }; let pb = sender.draggingPasteboard(); - vars.app_state.dnd().set_pasteboard(transfer_id, &pb); + vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); @@ -420,7 +421,7 @@ define_class!( self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); - accepted + valid_operations.0 } /// Invoked when the image is released @@ -437,13 +438,15 @@ define_class!( let vars = self.ivars(); - let Some(DragState { id: transfer_id, accepted }) = vars.drag_state.get() else { + let Some(DragState { id: transfer_id, valid_operations }) = + vars.app_state.drag_state().get() + else { return false.into(); }; let pb = sender.draggingPasteboard(); - vars.app_state.dnd().set_pasteboard(transfer_id, &pb); + vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); @@ -453,7 +456,7 @@ define_class!( self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); self.queue_event(WindowEvent::DragDropped { id: transfer_id }); - accepted + sender.draggingSourceOperationMask().intersects(valid_operations.0) } /// Invoked when the dragging operation is complete @@ -462,8 +465,8 @@ define_class!( let _entered = debug_span!("concludeDragOperation:").entered(); let vars = self.ivars(); - vars.app_state.dnd().remove_deloaded_pasteboards(); - vars.drag_state.set(None); + vars.app_state.pasteboards().remove_deloaded_pasteboards(); + vars.app_state.drag_state().set(None); } /// Invoked when the dragging operation is cancelled @@ -472,13 +475,13 @@ define_class!( let _entered = debug_span!("draggingExited:").entered(); let vars = self.ivars(); - let Some(DragState { id: transfer_id, accepted: _ }) = vars.drag_state.get() else { + let Some(DragState { id: transfer_id, .. }) = vars.app_state.drag_state().get() else { return; }; if let Some(sender) = sender { let pb = sender.draggingPasteboard(); - vars.app_state.dnd().set_pasteboard(transfer_id, &pb); + vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); @@ -490,7 +493,7 @@ define_class!( self.queue_event(WindowEvent::DragLeft { id: transfer_id }); - vars.drag_state.set(None); + vars.app_state.drag_state().set(None); } } @@ -833,7 +836,6 @@ impl WindowDelegate { let delegate = mtm.alloc().set_ivars(State { app_state: Rc::clone(app_state), - drag_state: Default::default(), window: window.retain(), previous_position: Cell::new(flip_window_screen_coordinates(window.frame())), previous_scale_factor: Cell::new(scale_factor), @@ -920,26 +922,6 @@ impl WindowDelegate { Ok(delegate) } - // TODO: Proper error - pub(super) fn set_drag_accepted( - &self, - transfer_id: DataTransferId, - accepted: bool, - ) -> Result<(), ()> { - let vars = self.ivars(); - let Some(DragState { id, accepted: _ }) = vars.drag_state.get() else { - return Err(()); - }; - - if id != transfer_id { - return Err(()); - } - - vars.drag_state.set(Some(DragState { id, accepted })); - - Ok(()) - } - #[track_caller] pub(super) fn view(&self) -> Retained { // The view inside WinitWindow should always be set and be `WinitView`. diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index d2476ac5e9..a2eb9c2da7 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -2,10 +2,9 @@ use std::cell::Cell; use std::ffi::OsString; use std::io; use std::marker::PhantomData; -use std::ops::ControlFlow; use std::os::raw::*; use std::str::Utf8Error; -use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, OnceLock, RwLock}; use std::thread::ThreadId; diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 201c7abd1c..37cd51f669 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -3,7 +3,6 @@ use std::collections::{HashMap, VecDeque}; use std::mem::MaybeUninit; use std::os::raw::{c_char, c_int, c_long, c_ulong}; use std::slice; -use std::sync::atomic::Ordering; use std::sync::{Arc, Mutex}; use dpi::{PhysicalPosition, PhysicalSize}; diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 5ff0b1ca58..d2627cf15c 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -4,7 +4,7 @@ use tracing::{error, info, warn}; use winit::application::ApplicationHandler; use winit::data_transfer::{TypeHint, TypedData}; use winit::event::WindowEvent; -use winit::event_loop::{ActiveEventLoop, EventLoop, DndActions}; +use winit::event_loop::{ActiveEventLoop, DndActions, EventLoop}; use winit::window::{Window, WindowAttributes, WindowId}; #[path = "util/fill.rs"] @@ -47,10 +47,6 @@ impl ApplicationHandler for Application { _window_id: WindowId, event: WindowEvent, ) { - let Some(window) = self.window.as_ref() else { - return; - }; - match event { WindowEvent::DragLeft { .. } => { info!("{event:?}"); @@ -91,11 +87,7 @@ impl ApplicationHandler for Application { }, }; - info!( - "Types: {:#?}", - data_transfer - .available_types() - ); + info!("Types: {:#?}", data_transfer.available_types()); let valid_type = [TypeHint::Html, TypeHint::UriList, TypeHint::Plaintext] .into_iter() From f3eec1542d2a31759f720d17b6191bedb88bbb96 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Mon, 1 Jun 2026 16:39:34 +0200 Subject: [PATCH 46/87] Fix clippy warnings, fix on wayland --- winit-wayland/src/dnd.rs | 57 ++++++++++++++++------------- winit-wayland/src/event_loop/mod.rs | 4 +- winit-wayland/src/window/state.rs | 1 - winit-x11/src/event_loop.rs | 23 +++++++----- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index 8c02825c03..4b6c1e2a44 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -18,6 +18,7 @@ use wayland_client::{Connection, Proxy, QueueHandle}; use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; use winit_core::event::WindowEvent; use winit_core::event_loop::{DndActionMask, DndActions}; +use winit_core::window::WindowId; use crate::state::WinitState; @@ -223,8 +224,15 @@ impl fmt::Display for MimeType { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct UnknownTypeHint(pub TypeHint); +impl fmt::Display for UnknownTypeHint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unknown type hint: {:?}", self.0) + } +} + impl TryFrom for MimeType { type Error = UnknownTypeHint; @@ -268,7 +276,7 @@ impl MimeData { let fd_clone = // TODO: Is it ok that this may only work once, depending on what the fd points to? if let Ok(cloned) = self.fd.as_ref()?.try_clone() { cloned } else { self.fd.take()? }; - self.fd.take().map(Into::into) + Some(fd_clone.into()) } } @@ -278,12 +286,10 @@ impl TypedData for MimeData { } fn try_read(&mut self) -> Option> { - dbg!(&self.mime_type); Some(Box::new(BufReader::new(self.try_as_file()?))) } fn try_as_uris(&mut self) -> io::Result> { - dbg!(&self.mime_type); let Some(file) = self.try_as_file() else { return Err(io::Error::new( io::ErrorKind::BrokenPipe, @@ -340,6 +346,7 @@ pub struct CurrentDrag { accepted_type: Option, data: WlDataOffer, transfer_id: DataTransferId, + window_id: WindowId, } impl CurrentDrag { @@ -347,6 +354,10 @@ impl CurrentDrag { self.transfer_id } + pub(crate) fn window_id(&self) -> WindowId { + self.window_id + } + pub(crate) fn set_actions(&self, action_set: &DndActionSet) { self.data.set_actions(action_set.dnd_actions, action_set.preferred_action()); } @@ -402,8 +413,8 @@ fn guess_preferred_action(action: DndAction) -> DndAction { } impl DndActionSet { - pub fn all() -> Self { - Self { dnd_actions: DndAction::all(), preferred_action: None } + pub fn empty() -> Self { + Self { dnd_actions: DndAction::empty(), preferred_action: None } } pub(crate) fn from_dyn(mask: &dyn DndActionMask) -> Self { @@ -536,9 +547,10 @@ impl DataDeviceHandler for WinitState { transfer_id: DataTransferId::from_raw(drag.serial as i64), data: drag.inner().clone(), accepted_type: None, + window_id, }); - current_drag.set_actions(&DndActionSet::all()); + current_drag.set_actions(&DndActionSet::empty()); self.dnd_state.current_drag = Some(current_drag); @@ -563,19 +575,22 @@ impl DataDeviceHandler for WinitState { let Some(data) = data_device.data::() else { return; }; - let Some(drag) = data.drag_offer() else { - // Selections are not yet implemented - return; - }; - let window_id = crate::make_wid(&drag.surface); + if let Some(current_drag) = self.dnd_state.current_drag() { + self.events_sink.push_window_event( + WindowEvent::DragLeft { id: current_drag.transfer_id() }, + current_drag.window_id(), + ); - self.events_sink.push_window_event( - WindowEvent::DragLeft { id: DataTransferId::from_raw(drag.serial.into()) }, - window_id, - ); + self.dnd_state.current_drag = None; + } - self.dnd_state.current_drag = None; + if let Some(drag) = data.drag_offer() { + drag.destroy(); + } + if let Some(selection) = data.selection_offer() { + selection.destroy(); + } } fn motion( @@ -590,7 +605,7 @@ impl DataDeviceHandler for WinitState { return; }; let Some(drag) = data.drag_offer() else { - // Selections are not yet implemented + // Selections (copy/paste) are not yet implemented return; }; @@ -611,8 +626,6 @@ impl DataDeviceHandler for WinitState { }, window_id, ); - - self.dnd_state.current_drag = None; } fn selection(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) { @@ -640,12 +653,6 @@ impl DataDeviceHandler for WinitState { window_id, ); - self.dnd_state.current_drag().inspect(|current_drag| { - current_drag - .accept(drag.serial, current_drag.accepted_type.as_ref().map(ToString::to_string)); - current_drag.destroy(); - }); - self.dnd_state.current_drag = None; } } diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 2c50eb33e3..3b3bfef21d 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -31,7 +31,7 @@ use winit_core::event_loop::{ use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::Theme; -use crate::dnd::{DndActionSet, MimeData, MimeType}; +use crate::dnd::{DndActionSet, MimeData}; use crate::types::cursor::WaylandCustomCursor; mod proxy; @@ -706,6 +706,8 @@ impl RootActiveEventLoop for ActiveEventLoop { let (readfd, writefd) = pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK).map_err(|e| os_error!(e))?; + current_drag + .accept(current_drag.transfer_id().into_raw() as _, Some(mime_type_str.clone())); data_offer::receive_to_fd(current_drag, mime_type_str, writefd); let mime_type = mime_type.clone(); diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 85238177b9..dcac4b69d1 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -35,7 +35,6 @@ use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, WindowId, }; -use crate::dnd::DndState; use crate::event_loop::OwnedDisplayHandle; use crate::logical_to_physical_rounded; use crate::seat::{ diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 3e7ad3406c..4019a95d69 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -769,7 +769,7 @@ impl RootActiveEventLoop for ActiveEventLoop { fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { let dnd = self.dnd.borrow(); - if !dnd.state.as_ref().is_some_and(|state| state.transfer_id == id) { + if dnd.state.as_ref().is_none_or(|state| state.transfer_id != id) { return Err(RequestError::Ignored); } @@ -787,7 +787,7 @@ impl RootActiveEventLoop for ActiveEventLoop { ) -> Result, RequestError> { let mut dnd = self.dnd.borrow_mut(); - if !dnd.state.as_ref().is_some_and(|state| state.transfer_id == id) { + if dnd.state.as_ref().is_none_or(|state| state.transfer_id != id) { return Err(RequestError::NotSupported(NotSupportedError::new( "Unknown data transfer", ))); @@ -1131,15 +1131,18 @@ impl Device { let ty = unsafe { (*class_ptr)._type }; if ty == ffi::XIScrollClass { let info = unsafe { &*(class_ptr as *const ffi::XIScrollClassInfo) }; - scroll_axes.push((info.number, ScrollAxis { - increment: info.increment, - orientation: match info.scroll_type { - ffi::XIScrollTypeHorizontal => ScrollOrientation::Horizontal, - ffi::XIScrollTypeVertical => ScrollOrientation::Vertical, - _ => unreachable!(), + scroll_axes.push(( + info.number, + ScrollAxis { + increment: info.increment, + orientation: match info.scroll_type { + ffi::XIScrollTypeHorizontal => ScrollOrientation::Horizontal, + ffi::XIScrollTypeVertical => ScrollOrientation::Vertical, + _ => unreachable!(), + }, + position: 0.0, }, - position: 0.0, - })); + )); } else if ty == ffi::XITouchClass { r#type = Some(DeviceType::Touch); } else if r#type.is_none() && ty == ffi::XIValuatorClass { From 3a2231fe48589d34ea229ce12fe83b14f4f3523d Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Mon, 1 Jun 2026 17:14:53 +0200 Subject: [PATCH 47/87] WIP: Initiate drag --- winit-core/src/data_transfer.rs | 85 +++++++++++++++++++++++++++++++- winit-core/src/event_loop/mod.rs | 15 +++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 9f9c1e0e11..216f9b78c6 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -44,7 +44,7 @@ //! using the methods defined on [`dyn AsAny`]. See each platform's documentation for details. use std::ffi::OsString; -use std::fmt::{self, Debug}; +use std::fmt; use std::io; use std::ops::ControlFlow; @@ -73,7 +73,7 @@ impl DataTransferId { /// The set of types supported cross-platform. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum TypeHint { - /// Plain UTF-8 text (see [`TypedData::try_as_plaintext`]). + /// Plain UTF-8 text (see [`TypedData::try_as_string`]). /// /// **Note for platform implementations**: this hint is _only_ for UTF-8 text. If the platform /// returns plaintext in some format other than UTF-8 by default, a [`TypedData`] @@ -243,3 +243,84 @@ pub trait DataTransfer: AsAny + fmt::Debug { } impl_dyn_casting!(DataTransfer); + +pub enum SendData { + Uris(Vec), + String(String), + Bytes(Vec), +} + +pub trait NewDataTransfer: DataTransfer { + fn make_type(&self, type_: &dyn TransferType) -> Option; +} + +#[derive(Default)] +pub struct NewDataTransferBuilder { + state: T, + types: Vec<(Box, Box Option>)>, +} + +impl fmt::Debug for NewDataTransferBuilder +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NewDataTransferBuilder").field("state", &self.state).finish_non_exhaustive() + } +} + +impl DataTransfer for NewDataTransferBuilder +where + T: fmt::Debug + 'static, +{ + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>, + ) { + let _ = self.types.iter().try_for_each(|(ty, _)| func(&**ty)); + } +} + +impl NewDataTransfer for NewDataTransferBuilder +where + T: fmt::Debug + 'static, +{ + fn make_type(&self, type_: &dyn TransferType) -> Option { + let (_, func) = self.types.iter().find(|(ty, _)| ty.matches(type_))?; + + func(&self.state) + } +} + +impl NewDataTransferBuilder { + pub fn new(state: T) -> Self { + Self { state, types: vec![] } + } + + pub fn add_type(&mut self, type_: Ty, func: F) -> &mut Self + where + Ty: TransferType, + F: Fn(&T) -> Option + 'static, + { + self.types.push((Box::new(type_), Box::new(func))); + self + } + + pub fn with_type(mut self, type_: Ty, func: F) -> Self + where + Ty: TransferType, + F: Fn(&T) -> Option + 'static, + { + self.add_type(type_, func); + self + } +} + +impl NewDataTransferBuilder +where + T: fmt::Debug + 'static, +{ + pub fn build(self) -> Box { + Box::new(self) + } +} diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index 74236a9b41..8e2cecb116 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -13,7 +13,9 @@ use rwh_06::{DisplayHandle, HandleError, HasDisplayHandle}; use crate::Instant; use crate::as_any::AsAny; use crate::cursor::{CustomCursor, CustomCursorSource}; -use crate::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; +use crate::data_transfer::{ + DataTransfer, DataTransferId, NewDataTransfer, TransferType, TypedData, +}; use crate::error::{NotSupportedError, RequestError}; use crate::monitor::MonitorHandle; use crate::window::{Theme, Window, WindowAttributes}; @@ -170,6 +172,17 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { let _ = actions; Err(UnknownDataTransfer(id)) } + + fn start_drag( + &self, + data_transfer: Box, + ) -> Result { + let _ = data_transfer; + Err(RequestError::NotSupported(NotSupportedError::new( + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ + this platform", + ))) + } } impl HasDisplayHandle for dyn ActiveEventLoop + '_ { From 74a338337aa55b1bca9eb3634f13130ab31f119b Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 12:17:26 +0200 Subject: [PATCH 48/87] WIP: Initiate drag --- winit-core/src/data_transfer.rs | 142 ++++++++++++++-- winit-core/src/event.rs | 48 +++--- winit-core/src/event_loop/mod.rs | 109 +++++++++---- winit-wayland/src/dnd.rs | 241 ++++++++++++++++++++++------ winit-wayland/src/event_loop/mod.rs | 145 +++++++++++++++-- winit-wayland/src/lib.rs | 1 + winit-wayland/src/seat/mod.rs | 4 + winit-x11/src/event_loop.rs | 26 ++- 8 files changed, 578 insertions(+), 138 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 216f9b78c6..907b51c730 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -43,9 +43,12 @@ //! implementing the traits in this module, which can then be accessed in an application //! using the methods defined on [`dyn AsAny`]. See each platform's documentation for details. +#![warn(missing_docs)] + use std::ffi::OsString; use std::fmt; use std::io; +use std::marker::PhantomData; use std::ops::ControlFlow; use crate::as_any::AsAny; @@ -244,23 +247,85 @@ pub trait DataTransfer: AsAny + fmt::Debug { impl_dyn_casting!(DataTransfer); +/// Kinds of data that can be sent via a `DataTransfer`. +/// +/// Some kinds of data cannot be represented by just a binary blob in a cross-platform way. +/// File URIs on Windows and macOS are represented as arrays of strings, and strings have +/// different encoding on different platforms. To allow this to be represented, we allow +/// supplying strings and URIs separately from binary blobs. pub enum SendData { + /// File URIs Uris(Vec), + /// String String(String), + /// Binary blob Bytes(Vec), } -pub trait NewDataTransfer: DataTransfer { - fn make_type(&self, type_: &dyn TransferType) -> Option; +impl From for SendData { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From> for SendData { + fn from(value: Vec) -> Self { + Self::Bytes(value) + } +} + +// We monomorphize these `From` implementations instead of making them generic, in order to +// prevent accidentally casting to the wrong type. +impl From> for SendData { + fn from(value: Vec) -> Self { + Self::Uris(value) + } +} + +/// Trait for sending data via a data transfer. +/// +/// See [`StartDrag`](crate::event_loop::StartDrag) for where this is used. To build an +/// implementation of this trait dynamically in a cross-platform way, use [`DataTransferSendBuilder`]. +pub trait DataTransferSend: DataTransfer { + /// Get the data for the specified type, or `None` if this value does not supply the given data type. + fn data_for_type(&self, type_: &dyn TransferType) -> Option; + + /// If `true`, this data transfer is only valid for the application sending the data. + /// + /// This is useful on Wayland and macOS, which allow expressing internal drag-and-drop in the API. + /// On platforms which make no distinction between internal and external drag-and-drop, this is + /// ignored. + fn is_internal_only(&self) -> bool; } -#[derive(Default)] -pub struct NewDataTransferBuilder { +impl_dyn_casting!(DataTransferSend); + +/// Marker for a [`DataTransferSendBuilder`] which is internal-only. +pub enum InternalTransferMarker {} +/// Marker for a [`DataTransferSendBuilder`] which is external. +pub enum ExternalTransferMarker {} + +type SendDataCallback = Box SendData>; + +/// Dynamic builder for an implementation of [`DataTransferSend`]. +/// +/// On all platforms, inter-application data transfer (i.e. clipboard and drag-and-drop) works like so: +/// +/// - The source advertises a set of types that it can transfer. +/// - The destination picks one or more of those types to receive. +/// - The source sends the data for that type. +/// +/// This type abstracts that in a way that allows data to be sent cross-platform. `T` is an optional +/// state value, which allows the user to have a single source of truth for their data, converting +/// it lazily to the requested type. +pub struct DataTransferSendBuilder { state: T, - types: Vec<(Box, Box Option>)>, + types: Vec<(Box, SendDataCallback)>, + /// + _is_internal: PhantomData, } -impl fmt::Debug for NewDataTransferBuilder +impl fmt::Debug for DataTransferSendBuilder where T: fmt::Debug, { @@ -269,8 +334,9 @@ where } } -impl DataTransfer for NewDataTransferBuilder +impl DataTransfer for DataTransferSendBuilder where + M: 'static, T: fmt::Debug + 'static, { fn for_each_available_type<'this>( @@ -281,46 +347,88 @@ where } } -impl NewDataTransfer for NewDataTransferBuilder +impl DataTransferSend for DataTransferSendBuilder where T: fmt::Debug + 'static, { - fn make_type(&self, type_: &dyn TransferType) -> Option { - let (_, func) = self.types.iter().find(|(ty, _)| ty.matches(type_))?; + fn data_for_type(&self, type_: &dyn TransferType) -> Option { + self.data_for_type(type_) + } + + fn is_internal_only(&self) -> bool { + false + } +} + +impl DataTransferSend for DataTransferSendBuilder +where + T: fmt::Debug + 'static, +{ + fn data_for_type(&self, type_: &dyn TransferType) -> Option { + self.data_for_type(type_) + } - func(&self.state) + fn is_internal_only(&self) -> bool { + true } } -impl NewDataTransferBuilder { +impl DataTransferSendBuilder { + /// Create a new [`DataTransferSendBuilder`], with a state value which acts as + /// the single source of truth for the underlying data. pub fn new(state: T) -> Self { - Self { state, types: vec![] } + Self { state, types: vec![], _is_internal: PhantomData } + } +} + +impl DataTransferSendBuilder { + /// Create a new [`DataTransferSendBuilder`], with a state value which acts as + /// the single source of truth for the underlying data. + pub fn new_internal(state: T) -> Self { + Self { state, types: vec![], _is_internal: PhantomData } + } +} + +impl DataTransferSendBuilder { + fn data_for_type(&self, type_: &dyn TransferType) -> Option { + let (_, func) = self.types.iter().find(|(ty, _)| ty.matches(type_))?; + + Some(func(&self.state)) } + /// Add a callback which converts the builder's state to the given type. In + /// most cases, `type_` will be [`TypeHint`]. pub fn add_type(&mut self, type_: Ty, func: F) -> &mut Self where Ty: TransferType, - F: Fn(&T) -> Option + 'static, + F: Fn(&T) -> SendData + 'static, { self.types.push((Box::new(type_), Box::new(func))); self } + /// Return a new builder, adding a callback which converts the builder's state + /// to the given type. In most cases, `type_` will be [`TypeHint`]. pub fn with_type(mut self, type_: Ty, func: F) -> Self where Ty: TransferType, - F: Fn(&T) -> Option + 'static, + F: Fn(&T) -> SendData + 'static, { self.add_type(type_, func); self } } -impl NewDataTransferBuilder +impl DataTransferSendBuilder where T: fmt::Debug + 'static, + Self: DataTransferSend, { - pub fn build(self) -> Box { + /// Consume the builder, returning an implementation of [`DataTransferSend`]. + /// + /// Note that this is only provided for explicitness and ergonomics. [`DataTransferSendBuilder`] + /// implements [`DataTransferSend`] and this method is equivalent to [`Box::new`]. + pub fn build(self) -> Box { Box::new(self) } } diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index e1bbafa461..b2365d7bd4 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -1665,24 +1665,24 @@ mod tests { const TILT_TO_ANGLE: &[(TabletToolTilt, TabletToolAngle)] = &[ (TabletToolTilt { x: 0, y: 0 }, TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }), (TabletToolTilt { x: 0, y: 90 }, TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }), - (TabletToolTilt { x: 0, y: -90 }, TabletToolAngle { - altitude: 0., - azimuth: 3. * FRAC_PI_2, - }), + ( + TabletToolTilt { x: 0, y: -90 }, + TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, + ), (TabletToolTilt { x: 90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: PI }), (TabletToolTilt { x: -90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), - (TabletToolTilt { x: 0, y: 45 }, TabletToolAngle { - altitude: FRAC_PI_4, - azimuth: FRAC_PI_2, - }), - (TabletToolTilt { x: 0, y: -45 }, TabletToolAngle { - altitude: FRAC_PI_4, - azimuth: 3. * FRAC_PI_2, - }), + ( + TabletToolTilt { x: 0, y: 45 }, + TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, + ), + ( + TabletToolTilt { x: 0, y: -45 }, + TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, + ), (TabletToolTilt { x: 45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }), (TabletToolTilt { x: -45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }), ]; @@ -1697,20 +1697,20 @@ mod tests { (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }, TabletToolTilt { x: 45, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }, TabletToolTilt { x: 0, y: 0 }), (TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }, TabletToolTilt { x: 0, y: 90 }), - (TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, TabletToolTilt { - x: 0, - y: 45, - }), + ( + TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, + TabletToolTilt { x: 0, y: 45 }, + ), (TabletToolAngle { altitude: 0., azimuth: PI }, TabletToolTilt { x: -90, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }, TabletToolTilt { x: -45, y: 0 }), - (TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { - x: 0, - y: -90, - }), - (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { - x: 0, - y: -45, - }), + ( + TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, + TabletToolTilt { x: 0, y: -90 }, + ), + ( + TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, + TabletToolTilt { x: 0, y: -45 }, + ), ]; for (angle, tilt) in ANGLE_TO_TILT { diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index 8e2cecb116..072b0fd9f1 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -14,24 +14,12 @@ use crate::Instant; use crate::as_any::AsAny; use crate::cursor::{CustomCursor, CustomCursorSource}; use crate::data_transfer::{ - DataTransfer, DataTransferId, NewDataTransfer, TransferType, TypedData, + DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData, }; use crate::error::{NotSupportedError, RequestError}; +use crate::icon::Icon; use crate::monitor::MonitorHandle; -use crate::window::{Theme, Window, WindowAttributes}; - -/// An operation was attempted on a data transfer ID, but that ID was invalid. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct UnknownDataTransfer(pub DataTransferId); - -impl fmt::Display for UnknownDataTransfer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let id = self.0.into_raw(); - write!(f, "Unknown data transfer with ID {id}") - } -} - -impl std::error::Error for UnknownDataTransfer {} +use crate::window::{Theme, Window, WindowAttributes, WindowId}; pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Creates an [`EventLoopProxy`] that can be used to dispatch user events @@ -142,8 +130,7 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { let _ = id; let _ = type_; Err(RequestError::NotSupported(NotSupportedError::new( - "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ - this platform", + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, ))) } @@ -154,8 +141,7 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { let _ = id; Err(RequestError::NotSupported(NotSupportedError::new( - "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ - this platform", + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, ))) } @@ -168,23 +154,64 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { &self, id: DataTransferId, actions: &dyn DndActionMask, - ) -> Result<(), UnknownDataTransfer> { + ) -> Result<(), RequestError> { + let _ = id; let _ = actions; - Err(UnknownDataTransfer(id)) + Err(RequestError::NotSupported(NotSupportedError::new( + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, + ))) } + /// Initiate a new drag-and-drop operation. + /// + /// See [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSend) for how to create a + /// new cross-platform data transfer, or see the platform-specific implementation of + /// [`DataTransferSend`]. + /// + /// ### Arguments + /// + /// - `send_data` - The data provided by this drag operation. See + /// [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder). + /// - `action_mask` - The set of valid actions for this drag operation. See + /// [`DndActions`](crate::data_transfer::DndActions). + /// - `icon` - icon to show while dragging. + /// + /// Some platforms have a more-expressive way of setting the visual component of a drag operation. For + /// those platforms, consider using the platform-specific implementation of [`DataTransferSend`] for + /// `send_data` and set this field to `None`. fn start_drag( &self, - data_transfer: Box, + source: WindowId, + send_data: Box, + action_mask: &dyn DndActionMask, + icon: Option, ) -> Result { - let _ = data_transfer; + let _ = source; + let _ = send_data; + let _ = action_mask; + let _ = icon; Err(RequestError::NotSupported(NotSupportedError::new( - "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ - this platform", + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, + ))) + } + + /// Cancel a drag-and-drop operation. + /// + /// This can be called on data transfers initiated by this application, as well as data transfers + /// received from an external application. + fn cancel_drag(&self, id: DataTransferId) -> Result<(), RequestError> { + let _ = id; + Err(RequestError::NotSupported(NotSupportedError::new( + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, ))) } } +const DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE: &str = { + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ + this platform" +}; + impl HasDisplayHandle for dyn ActiveEventLoop + '_ { fn display_handle(&self) -> Result, HandleError> { self.rwh_06_handle().display_handle() @@ -193,16 +220,44 @@ impl HasDisplayHandle for dyn ActiveEventLoop + '_ { impl_dyn_casting!(ActiveEventLoop); -// Inspired by https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect +/// Information needed to initiate a new drag operation. +pub struct StartDrag {} + +/// A mask of valid actions for a drag and drop operation. +/// +/// Inspired by [the `dropEffect` DOM API](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect). #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum DndActions { /// A specific set of operations. - Flags { move_: bool, copy: bool, link: bool }, + /// + /// This is limited to the same set of cross-platform actions supported by `dropEffect`. + Flags { + /// Move the dragged item from the source to the destination. + move_: bool, + /// Copy the dragged item from the source to the destination. + copy: bool, + /// A link is established between the source and the destination. + link: bool, + }, /// All actions, including platform-specific ones not represented in `Self::Flags`. + /// + /// Most commonly used as a default mask, in case the user just wants to support everything. All, } impl DndActions { + pub const fn new_copy() -> Self { + Self::Flags { move_: false, copy: true, link: false } + } + + pub const fn new_move() -> Self { + Self::Flags { move_: true, copy: false, link: false } + } + + pub const fn new_link() -> Self { + Self::Flags { move_: false, copy: false, link: true } + } + pub const fn copy(&self) -> bool { match *self { DndActions::Flags { copy, .. } => copy, diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index 4b6c1e2a44..bc5a7c7f7b 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -1,21 +1,29 @@ +//! Types related to drag-and-drop and data transfer on Wayland. + +#![warn(missing_docs)] + use std::ffi::OsString; use std::fmt; use std::fs::File; -use std::io::{self, BufRead, BufReader, Read}; +use std::io::{self, BufRead, BufReader, Read, Write}; use std::ops::Deref; use std::os::fd::OwnedFd; use std::sync::Arc; use dpi::{LogicalPosition, PhysicalPosition}; +use sctk::data_device_manager::WritePipe; use sctk::data_device_manager::data_device::{DataDeviceData, DataDeviceHandler}; use sctk::data_device_manager::data_offer::{DataOfferHandler, DragOffer}; -use sctk::data_device_manager::data_source::DataSourceHandler; +use sctk::data_device_manager::data_source::{DataSourceHandler, DragSource as SctkDragSource}; use wayland_client::protocol::wl_data_device::WlDataDevice; use wayland_client::protocol::wl_data_device_manager::DndAction; use wayland_client::protocol::wl_data_offer::WlDataOffer; +use wayland_client::protocol::wl_data_source::WlDataSource; use wayland_client::protocol::wl_surface::WlSurface; use wayland_client::{Connection, Proxy, QueueHandle}; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, +}; use winit_core::event::WindowEvent; use winit_core::event_loop::{DndActionMask, DndActions}; use winit_core::window::WindowId; @@ -27,42 +35,85 @@ impl DataSourceHandler for WinitState { &mut self, conn: &Connection, qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, + source: &WlDataSource, mime: Option, ) { let _ = mime; let _ = source; let _ = qh; let _ = conn; - // Not implemented, but required for `DataDeviceHandler`. + // This is unnecessary. } fn send_request( &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, mime: String, - fd: sctk::data_device_manager::WritePipe, + mut fd: WritePipe, ) { - let _ = fd; - let _ = mime; - let _ = source; - let _ = qh; - let _ = conn; - // Not implemented, but required for `DataDeviceHandler`. + let Some(data) = self.dnd_state.send_drag_data() else { + // TODO: Is there a way to explicitly express that the data was not sent? + return; + }; + + let mime = MimeType::parse(mime); + + let Some(send_data) = data.data_for_type(&mime) else { + return; + }; + + match send_data { + SendData::Uris(os_strings) => { + let mut iter = os_strings.into_iter(); + let Some(first) = iter.next() else { + return; + }; + + if fd.write_all(first.as_encoded_bytes()).is_err() { + return; + } + + // TODO: Is there something better we can do than unconditionally encoding as `text/uri-list`? + for os_str in iter { + // TODO: Is `as_encoded_bytes` correct here? + if fd + .write_all(b"\n") + .and_then(|()| fd.write_all(os_str.as_encoded_bytes())) + .is_err() + { + return; + } + } + }, + SendData::String(str) => match mime.charset().unwrap_or(Charset::Utf16) { + Charset::Utf8 => { + if fd.write_all(str.as_bytes()).is_err() { + return; + } + }, + Charset::Utf16 => { + let utf16_binary = str + .encode_utf16() + .flat_map(|uint16| uint16.to_le_bytes()) + .collect::>(); + if fd.write_all(&utf16_binary).is_err() { + return; + } + }, + }, + SendData::Bytes(binary) => { + if fd.write_all(&binary).is_err() { + return; + } + }, + } } - fn cancelled( - &mut self, - conn: &Connection, - qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - ) { - let _ = source; - let _ = qh; - let _ = conn; - // Not implemented, but required for `DataDeviceHandler`. + // TODO: Send `DragCancel` event. + fn cancelled(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) { + self.dnd_state.clear_send_drag(); } fn dnd_dropped( @@ -74,7 +125,7 @@ impl DataSourceHandler for WinitState { let _ = source; let _ = qh; let _ = conn; - // Not implemented, but required for `DataDeviceHandler`. + // TODO: Send message to window. } fn dnd_finished( @@ -86,21 +137,22 @@ impl DataSourceHandler for WinitState { let _ = source; let _ = qh; let _ = conn; - // Not implemented, but required for `DataDeviceHandler`. + // TODO: Send message to window. + self.dnd_state.clear_send_drag(); } fn action( &mut self, conn: &Connection, qh: &QueueHandle, - source: &wayland_client::protocol::wl_data_source::WlDataSource, - action: wayland_client::protocol::wl_data_device_manager::DndAction, + source: &WlDataSource, + action: DndAction, ) { let _ = action; let _ = source; let _ = qh; let _ = conn; - // Not implemented, but required for `DataDeviceHandler`. + // TODO: Send message to window } } @@ -110,6 +162,7 @@ enum Charset { Utf16, } +/// MIME type as string, with an optional hint detected from the MIME type. #[derive(Debug, PartialEq, Eq, Clone, Hash)] pub struct MimeType { mime: Arc, @@ -155,11 +208,11 @@ impl MimeType { // Files (TEXT_URI_LIST, TypeHint::UriList), // Plaintext - (TEXT_PLAIN, TypeHint::Plaintext), (TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext), + (TEXT_PLAIN, TypeHint::Plaintext), // HTML - (TEXT_HTML, TypeHint::Html), (TEXT_HTML_CHARSET_UTF8, TypeHint::Html), + (TEXT_HTML, TypeHint::Html), // RTF (APPLICATION_RTF, TypeHint::Rtf), // Audio @@ -186,6 +239,17 @@ impl MimeType { (IMAGE_X_ICON, TypeHint::Image { extension_hint: Some("ico") }), ]; + pub(crate) fn from_dyn(type_: &dyn TransferType) -> Option { + type_.cast_ref::().cloned().or_else(|| { + let hint = type_.hint()?; + Self::MIME_HINT_MAP + .iter() + .find_map(|(mime, haystack)| (*haystack == hint).then_some(mime)) + .map(|mime| Self { mime: mime.to_string().into(), hint: Some(hint) }) + }) + } + + // TODO: We should properly parse MIME types using `mime` or a similar crate. fn charset(&self) -> Option { let (_essence, options) = self.mime.split_once(';')?; @@ -261,6 +325,7 @@ impl TransferType for MimeType { } } +/// In-progress typed data transfer from another application. #[derive(Debug)] pub struct MimeData { mime_type: MimeType, @@ -340,16 +405,17 @@ impl TypedData for MimeData { } } +/// A #[derive(Debug, Clone)] -pub struct CurrentDrag { +pub struct DataOffer { mime_types: Arc<[MimeType]>, - accepted_type: Option, + // TODO: Internal drag-and-drop. data: WlDataOffer, transfer_id: DataTransferId, window_id: WindowId, } -impl CurrentDrag { +impl DataOffer { pub(crate) fn transfer_id(&self) -> DataTransferId { self.transfer_id } @@ -375,7 +441,7 @@ impl CurrentDrag { } } -impl Deref for CurrentDrag { +impl Deref for DataOffer { type Target = WlDataOffer; fn deref(&self) -> &Self::Target { @@ -383,7 +449,7 @@ impl Deref for CurrentDrag { } } -impl DataTransfer for CurrentDrag { +impl DataTransfer for DataOffer { fn for_each_available_type<'this>( &'this self, func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, @@ -392,17 +458,76 @@ impl DataTransfer for CurrentDrag { } } +/// Wrapper for [`WlDataSource`], which exposes the types that are advertised by a data +/// transfer operation, along with the data that the source represents +#[derive(Debug)] +pub struct DragSource { + /// The `WlDataSource` generated from `data`. + /// + /// This is stored internally, as if this source is dropped then the + /// drag operation will be cancelled. If this is `None`, then this is + /// a purely-internal data source that will not be transferred to + /// external applications. + data_source: Option, + /// The supplied [`DataTransferSend`]. + data: Box, + /// The set of available actions. + action_set: DndActionSet, + /// (Optionally) an icon for the drag-and-drop operation. + icon: Option, +} + +impl DragSource { + pub(crate) fn new( + data_source: Option, + data: Box, + action_set: DndActionSet, + icon: Option, + ) -> Self { + Self { data_source, action_set, data, icon } + } + + /// The underlying externally-visible `WlDataSource`, or `None` if this operation is purely internal. + pub fn wl_data_source(&self) -> Option<&SctkDragSource> { + self.data_source.as_ref() + } + + /// Per-type data to be sent. See [`DataTransferSend`]. + pub fn data(&self) -> &dyn DataTransferSend { + &*self.data + } + + /// If `Some`, a surface to be displayed while dragging. If `None`, no icon can be displayed. + pub fn icon(&self) -> Option<&WlSurface> { + self.icon.as_ref() + } +} + +/// The current state of an in-progress drag-and-drop operation. #[derive(Debug, Default)] pub struct DndState { - current_drag: Option, + receive_drag: Option, + send_drag: Option, } +/// The set of actions supported on a drag operation. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct DndActionSet { /// The set of available actions. pub dnd_actions: DndAction, - /// If set, the preferred action. - preferred_action: Option, + /// If supplied, the preferred action. If `None`, will be + /// determined from `dnd_actions`. In order of preference: + /// + /// - [`DndAction::Move`] + /// - [`DndAction::Copy`] + /// - [`DndAction::Ask`] + pub preferred_action: Option, +} + +impl From for DndActionSet { + fn from(value: DndAction) -> Self { + Self { dnd_actions: value, preferred_action: None } + } } fn guess_preferred_action(action: DndAction) -> DndAction { @@ -413,6 +538,7 @@ fn guess_preferred_action(action: DndAction) -> DndAction { } impl DndActionSet { + /// A new, empty `DndActionSet`. pub fn empty() -> Self { Self { dnd_actions: DndAction::empty(), preferred_action: None } } @@ -421,10 +547,16 @@ impl DndActionSet { mask.cast_ref::().copied().unwrap_or_else(|| mask.hint().into()) } + /// Get the preferred action, or guess it from `dnd_actions`. In order of preference: + /// + /// - [`DndAction::Move`] + /// - [`DndAction::Copy`] + /// - [`DndAction::Ask`] pub fn preferred_action(&self) -> DndAction { self.preferred_action.unwrap_or_else(|| guess_preferred_action(self.dnd_actions)) } + /// Get the intersection of this action set with another action set. pub fn intersection(&self, other: &Self) -> Self { let preferred_action = match (self.preferred_action, other.preferred_action) { (Some(this_pref), Some(other_pref)) if this_pref.intersects(other_pref) => { @@ -477,14 +609,20 @@ impl From for DndActionSet { } impl DndState { - pub(crate) fn current_drag(&self) -> Option<&CurrentDrag> { - self.current_drag.as_ref() + pub(crate) fn receive_drag(&self) -> Option<&DataOffer> { + self.receive_drag.as_ref() } - pub(crate) fn accept_type(&mut self, mime_type: MimeType) { - if let Some(cur) = self.current_drag.as_mut() { - cur.accepted_type = Some(mime_type); - } + pub(crate) fn set_send_drag(&mut self, source: DragSource) { + self.send_drag = Some(source); + } + + pub(crate) fn clear_send_drag(&mut self) { + self.send_drag = None; + } + + pub(crate) fn send_drag_data(&self) -> Option<&dyn DataTransferSend> { + self.send_drag.as_ref().map(|send| send.data()) } } @@ -538,7 +676,7 @@ impl DataDeviceHandler for WinitState { let window_id = crate::make_wid(wl_surface); - let current_drag = drag.with_mime_types(|types| CurrentDrag { + let current_drag = drag.with_mime_types(|types| DataOffer { mime_types: types .iter() .map(|str| MimeType::parse(str.clone())) @@ -546,13 +684,12 @@ impl DataDeviceHandler for WinitState { .into(), transfer_id: DataTransferId::from_raw(drag.serial as i64), data: drag.inner().clone(), - accepted_type: None, window_id, }); current_drag.set_actions(&DndActionSet::empty()); - self.dnd_state.current_drag = Some(current_drag); + self.dnd_state.receive_drag = Some(current_drag); let scale_factor = self .windows @@ -576,13 +713,13 @@ impl DataDeviceHandler for WinitState { return; }; - if let Some(current_drag) = self.dnd_state.current_drag() { + if let Some(current_drag) = self.dnd_state.receive_drag() { self.events_sink.push_window_event( WindowEvent::DragLeft { id: current_drag.transfer_id() }, current_drag.window_id(), ); - self.dnd_state.current_drag = None; + self.dnd_state.receive_drag = None; } if let Some(drag) = data.drag_offer() { @@ -653,7 +790,7 @@ impl DataDeviceHandler for WinitState { window_id, ); - self.dnd_state.current_drag = None; + self.dnd_state.receive_drag = None; } } diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 3b3bfef21d..ade9d7c977 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -2,37 +2,45 @@ use std::cell::{Cell, RefCell}; use std::io::Result as IOResult; -use std::mem; use std::os::fd::OwnedFd; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; +use std::{fmt, mem}; use calloop::ping::Ping; use dpi::LogicalSize; use rustix::event::{PollFd, PollFlags}; use rustix::pipe::{self, PipeFlags}; use sctk::data_device_manager::data_offer; +use sctk::data_device_manager::data_source::DragSource as SctkDragSource; use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::{Connection, QueueHandle, globals}; +use sctk::shell::WaylandSurface; use tracing::warn; +use wayland_client::Proxy; +use wayland_client::protocol::wl_shm::Format; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData, +}; use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError}; use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, - OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, + OwnedDisplayHandle as CoreOwnedDisplayHandle, }; +use winit_core::icon::{Icon, RgbaIcon}; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::Theme; use crate::dnd::{DndActionSet, MimeData}; use crate::types::cursor::WaylandCustomCursor; +use crate::{DragSource, MimeType, image_to_buffer}; mod proxy; pub mod sink; @@ -687,8 +695,8 @@ impl RootActiveEventLoop for ActiveEventLoop { id: DataTransferId, type_: &dyn TransferType, ) -> Result, RequestError> { - let mut state = self.state.borrow_mut(); - let Some(current_drag) = state.dnd_state.current_drag() else { + let state = self.state.borrow(); + let Some(current_drag) = state.dnd_state.receive_drag() else { return Err(RequestError::Ignored); }; @@ -712,14 +720,12 @@ impl RootActiveEventLoop for ActiveEventLoop { let mime_type = mime_type.clone(); - state.dnd_state.accept_type(mime_type.clone()); - Ok(Box::new(MimeData::new(readfd, mime_type))) } fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { let state = self.state.borrow(); - let Some(state) = state.dnd_state.current_drag() else { + let Some(state) = state.dnd_state.receive_drag() else { return Err(RequestError::Ignored); }; @@ -734,22 +740,135 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, id: DataTransferId, mask: &dyn DndActionMask, - ) -> Result<(), UnknownDataTransfer> { + ) -> Result<(), RequestError> { let state = self.state.borrow(); - let Some(state) = state.dnd_state.current_drag() else { - return Err(UnknownDataTransfer(id)); + let Some(state) = state.dnd_state.receive_drag() else { + return Err(os_error!(UnknownDataTransfer(id)).into()); }; if state.transfer_id() != id { - return Err(UnknownDataTransfer(id)); + return Err(os_error!(UnknownDataTransfer(id)).into()); } state.set_actions(&DndActionSet::from_dyn(mask)); Ok(()) } + + fn start_drag( + &self, + source: WindowId, + send_data: Box, + action_mask: &dyn DndActionMask, + icon: Option, + ) -> Result { + static DRAG_EVENT_SERIAL: AtomicU32 = AtomicU32::new(0); + + let mut state = self.state.borrow_mut(); + let action_set = DndActionSet::from_dyn(action_mask); + + let data_device_manager = state + .data_device_manager_state + .as_ref() + .ok_or(NotSupportedError::new("Tried to initiate drag, but data device not enabled"))?; + + let windows = state.windows.borrow(); + let source_window_state = windows + .get(&source) + .ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window ID was invalid", + ))? + .lock() + .unwrap(); + let source_surface = source_window_state.window.wl_surface(); + let data_device = state + .seats + .get(&source_surface.id()) + .ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window ID was invalid", + ))? + .data_device() + .ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window does not have the pointer capability", + ))?; + + let data_source = if send_data.is_internal_only() { + None + } else { + let mut mime_types = Vec::new(); + send_data.for_each_available_type(&mut |ty_| { + if let Some(mime) = MimeType::from_dyn(ty_) { + mime_types.push(mime); + } + + std::ops::ControlFlow::Continue(()) + }); + + Some(data_device_manager.create_drag_and_drop_source( + &self.queue_handle, + mime_types, + action_set.dnd_actions, + )) + }; + + let mut pool = state.image_pool.lock().unwrap(); + let icon_surface = icon.and_then(|icon| { + let rgba = icon.cast_ref::()?; + + let width = rgba.width().try_into().ok()?; + let height = rgba.height().try_into().ok()?; + + let buffer = + image_to_buffer(width, height, rgba.buffer(), Format::Argb8888, &mut pool).ok()?; + + let surface = state.compositor_state.create_surface(&self.queue_handle); + surface.attach(Some(buffer.wl_buffer()), 0, 0); + + Some(surface) + }); + + let serial = DRAG_EVENT_SERIAL.fetch_add(1, Ordering::Relaxed); + + match &data_source { + Some(source) => { + source.start_drag(data_device, source_surface, icon_surface.as_ref(), serial) + }, + None => SctkDragSource::start_internal_drag( + data_device, + source_surface, + icon_surface.as_ref(), + serial, + ), + } + + std::mem::drop(pool); + std::mem::drop(source_window_state); + std::mem::drop(windows); + + state.dnd_state.set_send_drag(DragSource::new( + data_source, + send_data, + action_set, + icon_surface, + )); + + todo!() + } +} + +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } } +impl std::error::Error for UnknownDataTransfer {} + impl ActiveEventLoop { fn clear_exit(&self) { self.exit.set(None) diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index e294222b47..2bbec5f302 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -42,6 +42,7 @@ mod state; mod types; mod window; +pub use self::dnd::{DataOffer, DndActionSet, DragSource, MimeData, MimeType}; pub use self::event_loop::{ActiveEventLoop, EventLoop}; pub use self::window::Window; diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index 20139c4726..87fe5ec08d 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -78,6 +78,10 @@ impl WinitSeatState { pub fn new() -> Self { Default::default() } + + pub(crate) fn data_device(&self) -> Option<&DataDevice> { + self.data_device.as_ref() + } } impl SeatHandler for WinitState { diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 4019a95d69..1fc09b2ebe 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -26,7 +26,7 @@ use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider, - OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, + OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::{Theme, Window as CoreWindow, WindowAttributes, WindowId}; @@ -37,7 +37,10 @@ use x11rb::protocol::{xkb, xproto}; use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; -use crate::atoms::*; +use crate::atoms::{ + _NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms, + WM_DELETE_WINDOW, +}; use crate::dnd::{DeadlockSentinelGuard, Dnd, SelectionFetchState}; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; @@ -837,15 +840,15 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, id: DataTransferId, actions: &dyn DndActionMask, - ) -> Result<(), UnknownDataTransfer> { + ) -> Result<(), RequestError> { let mut dnd = self.dnd.borrow_mut(); let Some(state) = &mut dnd.state else { - return Err(UnknownDataTransfer(id)); + return Err(os_error!(UnknownDataTransfer(id)).into()); }; if state.transfer_id != id { - return Err(UnknownDataTransfer(id)); + return Err(os_error!(UnknownDataTransfer(id)).into()); } state.accepted = actions.hint().any(); @@ -860,6 +863,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop { } } +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + pub(crate) struct DeviceInfo<'a> { xconn: &'a XConnection, info: *const ffi::XIDeviceInfo, From 9f4859799d612b81e99876f2152f9dd8ce966033 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 13:17:56 +0200 Subject: [PATCH 49/87] WIP: Initiate drag --- winit-core/src/data_transfer.rs | 1 - winit-wayland/src/dnd.rs | 32 +++++++++---------- winit-wayland/src/event_loop/mod.rs | 22 +++++++------ winit-wayland/src/lib.rs | 13 ++++++++ winit-wayland/src/window/state.rs | 6 ++++ winit-x11/src/event_loop.rs | 5 +-- winit/examples/dnd.rs | 48 ++++++++++++++++++++++++++--- 7 files changed, 90 insertions(+), 37 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 907b51c730..4d891fe7b8 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -321,7 +321,6 @@ type SendDataCallback = Box SendData>; pub struct DataTransferSendBuilder { state: T, types: Vec<(Box, SendDataCallback)>, - /// _is_internal: PhantomData, } diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index bc5a7c7f7b..479896f15b 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -28,6 +28,7 @@ use winit_core::event::WindowEvent; use winit_core::event_loop::{DndActionMask, DndActions}; use winit_core::window::WindowId; +use crate::make_data_transfer_id; use crate::state::WinitState; impl DataSourceHandler for WinitState { @@ -89,24 +90,18 @@ impl DataSourceHandler for WinitState { }, SendData::String(str) => match mime.charset().unwrap_or(Charset::Utf16) { Charset::Utf8 => { - if fd.write_all(str.as_bytes()).is_err() { - return; - } + let _ = fd.write_all(str.as_bytes()); }, Charset::Utf16 => { let utf16_binary = str .encode_utf16() .flat_map(|uint16| uint16.to_le_bytes()) .collect::>(); - if fd.write_all(&utf16_binary).is_err() { - return; - } + let _ = fd.write_all(&utf16_binary); }, }, SendData::Bytes(binary) => { - if fd.write_all(&binary).is_err() { - return; - } + let _ = fd.write_all(&binary); }, } } @@ -462,6 +457,7 @@ impl DataTransfer for DataOffer { /// transfer operation, along with the data that the source represents #[derive(Debug)] pub struct DragSource { + data_transfer_id: DataTransferId, /// The `WlDataSource` generated from `data`. /// /// This is stored internally, as if this source is dropped then the @@ -471,20 +467,22 @@ pub struct DragSource { data_source: Option, /// The supplied [`DataTransferSend`]. data: Box, - /// The set of available actions. - action_set: DndActionSet, /// (Optionally) an icon for the drag-and-drop operation. icon: Option, } impl DragSource { pub(crate) fn new( + data_transfer_id: DataTransferId, data_source: Option, data: Box, - action_set: DndActionSet, icon: Option, ) -> Self { - Self { data_source, action_set, data, icon } + Self { data_transfer_id, data_source, data, icon } + } + + pub fn transfer_id(&self) -> DataTransferId { + self.data_transfer_id } /// The underlying externally-visible `WlDataSource`, or `None` if this operation is purely internal. @@ -682,7 +680,7 @@ impl DataDeviceHandler for WinitState { .map(|str| MimeType::parse(str.clone())) .collect::>() .into(), - transfer_id: DataTransferId::from_raw(drag.serial as i64), + transfer_id: make_data_transfer_id(data_device, drag.serial), data: drag.inner().clone(), window_id, }); @@ -701,7 +699,7 @@ impl DataDeviceHandler for WinitState { self.events_sink.push_window_event( WindowEvent::DragEntered { - id: DataTransferId::from_raw(drag.serial.into()), + id: make_data_transfer_id(data_device, drag.serial), position: Some(position), }, window_id, @@ -758,7 +756,7 @@ impl DataDeviceHandler for WinitState { self.events_sink.push_window_event( WindowEvent::DragPosition { - id: DataTransferId::from_raw(drag.serial.into()), + id: make_data_transfer_id(data_device, drag.serial), position, }, window_id, @@ -786,7 +784,7 @@ impl DataDeviceHandler for WinitState { let window_id = crate::make_wid(&drag.surface); self.events_sink.push_window_event( - WindowEvent::DragDropped { id: DataTransferId::from_raw(drag.serial.into()) }, + WindowEvent::DragDropped { id: make_data_transfer_id(data_device, drag.serial) }, window_id, ); diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index ade9d7c977..6830e3d619 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -40,7 +40,7 @@ use winit_core::window::Theme; use crate::dnd::{DndActionSet, MimeData}; use crate::types::cursor::WaylandCustomCursor; -use crate::{DragSource, MimeType, image_to_buffer}; +use crate::{DragSource, MimeType, image_to_buffer, make_data_transfer_id}; mod proxy; pub mod sink; @@ -781,13 +781,13 @@ impl RootActiveEventLoop for ActiveEventLoop { .lock() .unwrap(); let source_surface = source_window_state.window.wl_surface(); - let data_device = state - .seats - .get(&source_surface.id()) - .ok_or(NotSupportedError::new( - "Tried to initiate drag, but source window ID was invalid", - ))? - .data_device() + // HACK: How do we get the correct seat for pointers here? + let data_device = source_window_state + .focused_seats() + .find_map(|seat_id| { + let seat = state.seats.get(seat_id)?; + seat.data_device() + }) .ok_or(NotSupportedError::new( "Tried to initiate drag, but source window does not have the pointer capability", ))?; @@ -841,18 +841,20 @@ impl RootActiveEventLoop for ActiveEventLoop { ), } + let transfer_id = make_data_transfer_id(data_device.inner(), serial); + std::mem::drop(pool); std::mem::drop(source_window_state); std::mem::drop(windows); state.dnd_state.set_send_drag(DragSource::new( + transfer_id, data_source, send_data, - action_set, icon_surface, )); - todo!() + Ok(transfer_id) } } diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 2bbec5f302..33191ab4bd 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -18,13 +18,16 @@ #![allow(clippy::mutable_key_type)] use std::ffi::c_void; +use std::hash::BuildHasher; use std::ptr::NonNull; use dpi::{LogicalSize, PhysicalSize}; use sctk::reexports::client::Proxy; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool}; +use wayland_client::protocol::wl_data_device::WlDataDevice; use wayland_client::protocol::wl_shm::Format; +use winit_core::data_transfer::DataTransferId; use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop; use winit_core::window::{ ActivationToken, PlatformWindowAttributes, Window as CoreWindow, WindowId, @@ -151,6 +154,16 @@ fn make_wid(surface: &WlSurface) -> WindowId { WindowId::from_raw(surface.id().as_ptr() as usize) } +/// Create a `DataTransferId` for the given data device and serial. +/// +/// It's currently unclear if this will result in the same ID when transferring to the same application. +#[inline] +fn make_data_transfer_id(data_device: &WlDataDevice, serial: u32) -> DataTransferId { + const BUILD_HASHER: foldhash::fast::FixedState = foldhash::fast::FixedState::with_seed(0); + + DataTransferId::from_raw(BUILD_HASHER.hash_one((data_device.id(), serial)) as i64) +} + /// The default routine does floor, but we need round on Wayland. fn logical_to_physical_rounded(size: LogicalSize, scale_factor: f64) -> PhysicalSize { let width = size.width as f64 * scale_factor; diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index dcac4b69d1..279c989360 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -243,6 +243,12 @@ impl WindowState { } } + // HACK: Currently to get the data device to initiate a drag-and-drop, we iterate through all + // focused seats to find one with a pointer capability. This is definitely wrong. + pub(crate) fn focused_seats(&self) -> impl Iterator { + self.seat_focus.iter() + } + /// Apply closure on the given pointer. fn apply_on_pointer, &WinitPointerData)>( &self, diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 1fc09b2ebe..1d7a9649c1 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -37,10 +37,7 @@ use x11rb::protocol::{xkb, xproto}; use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; -use crate::atoms::{ - _NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms, - WM_DELETE_WINDOW, -}; +use crate::atoms::*; use crate::dnd::{DeadlockSentinelGuard, Dnd, SelectionFetchState}; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index d2627cf15c..7e71393844 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -2,9 +2,10 @@ use std::error::Error; use tracing::{error, info, warn}; use winit::application::ApplicationHandler; -use winit::data_transfer::{TypeHint, TypedData}; -use winit::event::WindowEvent; +use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, TypeHint, TypedData}; +use winit::event::{MouseButton, WindowEvent}; use winit::event_loop::{ActiveEventLoop, DndActions, EventLoop}; +use winit::icon::{Icon, RgbaIcon}; use winit::window::{Window, WindowAttributes, WindowId}; #[path = "util/fill.rs"] @@ -22,18 +23,32 @@ fn main() -> Result<(), Box> { } /// Application state and event handling. -#[derive(Debug, Default)] +#[derive(Debug)] struct Application { window: Option>, last_dnd_fetch: Option>, + last_drag_start: Option, + drag_icon: Icon, } impl Application { fn new() -> Self { - Self::default() + let drag_icon = load_icon(include_bytes!("data/icon.png")); + + Self { window: None, last_dnd_fetch: None, last_drag_start: None, drag_icon } } } +fn load_icon(bytes: &[u8]) -> Icon { + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(bytes).unwrap().into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into() +} + impl ApplicationHandler for Application { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { let window_attributes = @@ -44,10 +59,33 @@ impl ApplicationHandler for Application { fn window_event( &mut self, event_loop: &dyn ActiveEventLoop, - _window_id: WindowId, + window_id: WindowId, event: WindowEvent, ) { match event { + WindowEvent::PointerButton { button, state, .. } => { + let Some(button) = button.mouse_button() else { + return; + }; + + if button == MouseButton::Left && state.is_pressed() { + if let Some(last_drag) = self.last_drag_start.take() { + let _ = event_loop.cancel_drag(last_drag); + } + + self.last_drag_start = dbg!(event_loop.start_drag( + window_id, + DataTransferSendBuilder::new(()) + .with_type(TypeHint::Plaintext, |()| { + "Winit example".to_string().into() + }) + .build(), + &DndActions::new_copy(), + Some(self.drag_icon.clone()), + )) + .ok(); + } + }, WindowEvent::DragLeft { .. } => { info!("{event:?}"); self.last_dnd_fetch = None; From 5d28920f8489845934bd4267253b6c00ab712a85 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 13:23:36 +0200 Subject: [PATCH 50/87] WIP: Initiate drag --- winit/examples/dnd.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 7e71393844..db0b25dca7 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -31,9 +31,11 @@ struct Application { drag_icon: Icon, } +const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png"); + impl Application { fn new() -> Self { - let drag_icon = load_icon(include_bytes!("data/icon.png")); + let drag_icon = load_icon(DRAG_IMAGE); Self { window: None, last_dnd_fetch: None, last_drag_start: None, drag_icon } } @@ -73,17 +75,22 @@ impl ApplicationHandler for Application { let _ = event_loop.cancel_drag(last_drag); } - self.last_drag_start = dbg!(event_loop.start_drag( + let result = event_loop.start_drag( window_id, DataTransferSendBuilder::new(()) - .with_type(TypeHint::Plaintext, |()| { - "Winit example".to_string().into() + .with_type(TypeHint::Plaintext, |()| "Winit example".to_string().into()) + .with_type(TypeHint::Html, |()| { + format!("Winit example").into() + }) + .with_type(TypeHint::Image { extension_hint: Some("png") }, |()| { + DRAG_IMAGE.to_vec().into() }) .build(), &DndActions::new_copy(), Some(self.drag_icon.clone()), - )) - .ok(); + ); + + self.last_drag_start = dbg!(result).ok(); } }, WindowEvent::DragLeft { .. } => { From 75ffbf5e78a4fdd3d08b24fce75368c80791dd54 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 14:11:14 +0200 Subject: [PATCH 51/87] Initiate drag on Wayland --- winit-wayland/src/dnd.rs | 27 +++++++++++++++++---------- winit-wayland/src/event_loop/mod.rs | 24 +++++++++++++++--------- winit-wayland/src/seat/mod.rs | 4 ++++ winit-wayland/src/window/state.rs | 6 ++++++ winit/examples/dnd.rs | 13 ++++++++++++- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index 479896f15b..bca6eadfbd 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -169,10 +169,10 @@ pub struct MimeType { const TEXT_URI_LIST: &str = "text/uri-list"; // Plaintext const TEXT_PLAIN: &str = "text/plain"; -const TEXT_PLAIN_CHARSET_UTF8: &str = "text/plain;charset=utf-8"; +const TEXT_PLAIN_CHARSET_UTF8: &str = "text/plain; charset=utf-8"; // HTML const TEXT_HTML: &str = "text/html"; -const TEXT_HTML_CHARSET_UTF8: &str = "text/html;charset=utf-8"; +const TEXT_HTML_CHARSET_UTF8: &str = "text/html; charset=utf-8"; // RTF const APPLICATION_RTF: &str = "application/rtf"; // Audio @@ -203,11 +203,11 @@ impl MimeType { // Files (TEXT_URI_LIST, TypeHint::UriList), // Plaintext - (TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext), (TEXT_PLAIN, TypeHint::Plaintext), + (TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext), // HTML - (TEXT_HTML_CHARSET_UTF8, TypeHint::Html), (TEXT_HTML, TypeHint::Html), + (TEXT_HTML_CHARSET_UTF8, TypeHint::Html), // RTF (APPLICATION_RTF, TypeHint::Rtf), // Audio @@ -234,14 +234,21 @@ impl MimeType { (IMAGE_X_ICON, TypeHint::Image { extension_hint: Some("ico") }), ]; - pub(crate) fn from_dyn(type_: &dyn TransferType) -> Option { - type_.cast_ref::().cloned().or_else(|| { - let hint = type_.hint()?; + // Returns an iterator so that things like the multiple charsets for plaintext/HTML + // and the multiple ways of expressing .wav work correctly. + pub(crate) fn from_dyn(type_: &dyn TransferType) -> impl Iterator { + let downcast = type_.cast_ref::().cloned(); + let downcast_failed = downcast.is_none(); + // This filter is a bit hacky, but it's the only way to ensure that we always + // return the same type. + let from_hint = type_.hint().filter(|_| downcast_failed).into_iter().flat_map(|hint| { Self::MIME_HINT_MAP .iter() - .find_map(|(mime, haystack)| (*haystack == hint).then_some(mime)) - .map(|mime| Self { mime: mime.to_string().into(), hint: Some(hint) }) - }) + .filter(move |(_, haystack)| *haystack == hint) + .map(move |(mime, _)| Self { mime: mime.to_string().into(), hint: Some(hint) }) + }); + + downcast.into_iter().chain(from_hint) } // TODO: We should properly parse MIME types using `mime` or a similar crate. diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 6830e3d619..7a4c99a104 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -762,8 +762,6 @@ impl RootActiveEventLoop for ActiveEventLoop { action_mask: &dyn DndActionMask, icon: Option, ) -> Result { - static DRAG_EVENT_SERIAL: AtomicU32 = AtomicU32::new(0); - let mut state = self.state.borrow_mut(); let action_set = DndActionSet::from_dyn(action_mask); @@ -781,23 +779,33 @@ impl RootActiveEventLoop for ActiveEventLoop { .lock() .unwrap(); let source_surface = source_window_state.window.wl_surface(); - // HACK: How do we get the correct seat for pointers here? - let data_device = source_window_state + let seat = source_window_state .focused_seats() .find_map(|seat_id| { - let seat = state.seats.get(seat_id)?; - seat.data_device() + // HACK: How do we get the correct seat for pointers here? + state.seats.get(seat_id).filter(|seat| seat.data_device().is_some()) }) .ok_or(NotSupportedError::new( "Tried to initiate drag, but source window does not have the pointer capability", ))?; + let data_device = seat.data_device().ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window does not have the pointer capability", + ))?; + + let serial = seat + .pointer_data() + .ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window does not have the pointer capability", + ))? + // TODO: Seems like a footgun that this requires a pointer serial + .latest_button_serial(); let data_source = if send_data.is_internal_only() { None } else { let mut mime_types = Vec::new(); send_data.for_each_available_type(&mut |ty_| { - if let Some(mime) = MimeType::from_dyn(ty_) { + for mime in MimeType::from_dyn(ty_) { mime_types.push(mime); } @@ -827,8 +835,6 @@ impl RootActiveEventLoop for ActiveEventLoop { Some(surface) }); - let serial = DRAG_EVENT_SERIAL.fetch_add(1, Ordering::Relaxed); - match &data_source { Some(source) => { source.start_drag(data_device, source_surface, icon_surface.as_ref(), serial) diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index 87fe5ec08d..aa7c6f33b7 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -82,6 +82,10 @@ impl WinitSeatState { pub(crate) fn data_device(&self) -> Option<&DataDevice> { self.data_device.as_ref() } + + pub(crate) fn pointer_data(&self) -> Option<&WinitPointerData> { + self.pointer.as_ref().and_then(|pointer| pointer.pointer().data()) + } } impl SeatHandler for WinitState { diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 279c989360..fde54f27b5 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -80,6 +80,10 @@ pub struct WindowState { /// Queue handle. pub queue_handle: QueueHandle, + pub last_seat: Option, + + pub last_event_serial: Option, + /// Theme variant. theme: Option, @@ -225,6 +229,8 @@ impl WindowState { min_surface_size: MIN_WINDOW_SIZE, resize_increments: None, pointer_constraints, + last_seat: None, + last_event_serial: None, pointers: Default::default(), queue_handle: queue_handle.clone(), resizable: true, diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index db0b25dca7..6369f52875 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -1,4 +1,6 @@ use std::error::Error; +use std::ffi::OsString; +use std::path::PathBuf; use tracing::{error, info, warn}; use winit::application::ApplicationHandler; @@ -85,8 +87,17 @@ impl ApplicationHandler for Application { .with_type(TypeHint::Image { extension_hint: Some("png") }, |()| { DRAG_IMAGE.to_vec().into() }) + .with_type(TypeHint::UriList, |()| { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root = manifest_dir.parent().unwrap(); + let this_file = root.join(file!()); + let icon_file = this_file.parent().unwrap().join("data/icon.png"); + let icon_file = icon_file.display(); + + vec![OsString::from(format!("file://{icon_file}"))].into() + }) .build(), - &DndActions::new_copy(), + &DndActions::All, Some(self.drag_icon.clone()), ); From 4596833acb2d5c48572664920dc99402285c41f5 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 15:08:37 +0200 Subject: [PATCH 52/87] Fix icon on drag --- winit-core/src/event_loop/mod.rs | 17 +++++++++++++++-- winit-wayland/src/dnd.rs | 22 ++++------------------ winit-wayland/src/event_loop/mod.rs | 20 +++++++++++++------- winit/examples/dnd.rs | 20 +++++++++++++------- 4 files changed, 45 insertions(+), 34 deletions(-) diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index 072b0fd9f1..524863729b 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; +use dpi::PhysicalPosition; use rwh_06::{DisplayHandle, HandleError, HasDisplayHandle}; use crate::Instant; @@ -184,7 +185,7 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { source: WindowId, send_data: Box, action_mask: &dyn DndActionMask, - icon: Option, + icon: Option, ) -> Result { let _ = source; let _ = send_data; @@ -221,7 +222,19 @@ impl HasDisplayHandle for dyn ActiveEventLoop + '_ { impl_dyn_casting!(ActiveEventLoop); /// Information needed to initiate a new drag operation. -pub struct StartDrag {} +pub struct DragIcon { + /// The icon to apply to the cursor. + pub icon: Icon, + /// An offset applied to the dragged icon. 0,0 means that the top-left point + /// of the icon will be at the cursor. + pub offset: PhysicalPosition, +} + +impl From for DragIcon { + fn from(value: Icon) -> Self { + Self { icon: value, offset: Default::default() } + } +} /// A mask of valid actions for a drag and drop operation. /// diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index bca6eadfbd..be3c9cc307 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -464,18 +464,18 @@ impl DataTransfer for DataOffer { /// transfer operation, along with the data that the source represents #[derive(Debug)] pub struct DragSource { - data_transfer_id: DataTransferId, + _data_transfer_id: DataTransferId, /// The `WlDataSource` generated from `data`. /// /// This is stored internally, as if this source is dropped then the /// drag operation will be cancelled. If this is `None`, then this is /// a purely-internal data source that will not be transferred to /// external applications. - data_source: Option, + _data_source: Option, /// The supplied [`DataTransferSend`]. data: Box, /// (Optionally) an icon for the drag-and-drop operation. - icon: Option, + _icon: Option, } impl DragSource { @@ -485,27 +485,13 @@ impl DragSource { data: Box, icon: Option, ) -> Self { - Self { data_transfer_id, data_source, data, icon } - } - - pub fn transfer_id(&self) -> DataTransferId { - self.data_transfer_id - } - - /// The underlying externally-visible `WlDataSource`, or `None` if this operation is purely internal. - pub fn wl_data_source(&self) -> Option<&SctkDragSource> { - self.data_source.as_ref() + Self { _data_transfer_id: data_transfer_id, _data_source: data_source, data, _icon: icon } } /// Per-type data to be sent. See [`DataTransferSend`]. pub fn data(&self) -> &dyn DataTransferSend { &*self.data } - - /// If `Some`, a surface to be displayed while dragging. If `None`, no icon can be displayed. - pub fn icon(&self) -> Option<&WlSurface> { - self.icon.as_ref() - } } /// The current state of an in-progress drag-and-drop operation. diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 7a4c99a104..ec719c7c66 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -4,7 +4,7 @@ use std::cell::{Cell, RefCell}; use std::io::Result as IOResult; use std::os::fd::OwnedFd; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; @@ -20,7 +20,6 @@ use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::{Connection, QueueHandle, globals}; use sctk::shell::WaylandSurface; use tracing::warn; -use wayland_client::Proxy; use wayland_client::protocol::wl_shm::Format; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; @@ -31,10 +30,10 @@ use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, DragIcon, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; -use winit_core::icon::{Icon, RgbaIcon}; +use winit_core::icon::RgbaIcon; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::Theme; @@ -760,7 +759,7 @@ impl RootActiveEventLoop for ActiveEventLoop { source: WindowId, send_data: Box, action_mask: &dyn DndActionMask, - icon: Option, + icon: Option, ) -> Result { let mut state = self.state.borrow_mut(); let action_set = DndActionSet::from_dyn(action_mask); @@ -821,7 +820,7 @@ impl RootActiveEventLoop for ActiveEventLoop { let mut pool = state.image_pool.lock().unwrap(); let icon_surface = icon.and_then(|icon| { - let rgba = icon.cast_ref::()?; + let rgba = icon.icon.cast_ref::()?; let width = rgba.width().try_into().ok()?; let height = rgba.height().try_into().ok()?; @@ -830,7 +829,8 @@ impl RootActiveEventLoop for ActiveEventLoop { image_to_buffer(width, height, rgba.buffer(), Format::Argb8888, &mut pool).ok()?; let surface = state.compositor_state.create_surface(&self.queue_handle); - surface.attach(Some(buffer.wl_buffer()), 0, 0); + buffer.attach_to(&surface).ok()?; + surface.offset(icon.offset.x, icon.offset.y); Some(surface) }); @@ -853,6 +853,12 @@ impl RootActiveEventLoop for ActiveEventLoop { std::mem::drop(source_window_state); std::mem::drop(windows); + // For some reason, if we commit before starting the drag then the offset isn't applied. + // This doesn't seem to be documented anywhere, and it's possible that it's a bug in KDE. + if let Some(surface) = &icon_surface { + surface.commit(); + } + state.dnd_state.set_send_drag(DragSource::new( transfer_id, data_source, diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 6369f52875..a4a28b2e8c 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -2,11 +2,12 @@ use std::error::Error; use std::ffi::OsString; use std::path::PathBuf; +use dpi::PhysicalPosition; use tracing::{error, info, warn}; use winit::application::ApplicationHandler; use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, TypeHint, TypedData}; use winit::event::{MouseButton, WindowEvent}; -use winit::event_loop::{ActiveEventLoop, DndActions, EventLoop}; +use winit::event_loop::{ActiveEventLoop, DndActions, DragIcon, EventLoop}; use winit::icon::{Icon, RgbaIcon}; use winit::window::{Window, WindowAttributes, WindowId}; @@ -30,7 +31,7 @@ struct Application { window: Option>, last_dnd_fetch: Option>, last_drag_start: Option, - drag_icon: Icon, + drag_icon: (Icon, PhysicalPosition), } const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png"); @@ -43,14 +44,17 @@ impl Application { } } -fn load_icon(bytes: &[u8]) -> Icon { +fn load_icon(bytes: &[u8]) -> (Icon, PhysicalPosition) { let (icon_rgba, icon_width, icon_height) = { let image = image::load_from_memory(bytes).unwrap().into_rgba8(); let (width, height) = image.dimensions(); let rgba = image.into_raw(); (rgba, width, height) }; - RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into() + ( + RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into(), + PhysicalPosition { x: -(icon_width as i32) / 2, y: -(icon_height as i32) / 2 }, + ) } impl ApplicationHandler for Application { @@ -77,12 +81,14 @@ impl ApplicationHandler for Application { let _ = event_loop.cancel_drag(last_drag); } + let (icon, offset) = self.drag_icon.clone(); + let result = event_loop.start_drag( window_id, DataTransferSendBuilder::new(()) .with_type(TypeHint::Plaintext, |()| "Winit example".to_string().into()) .with_type(TypeHint::Html, |()| { - format!("Winit example").into() + "Winit example".to_string().into() }) .with_type(TypeHint::Image { extension_hint: Some("png") }, |()| { DRAG_IMAGE.to_vec().into() @@ -98,10 +104,10 @@ impl ApplicationHandler for Application { }) .build(), &DndActions::All, - Some(self.drag_icon.clone()), + Some(DragIcon { icon, offset }), ); - self.last_drag_start = dbg!(result).ok(); + self.last_drag_start = result.ok(); } }, WindowEvent::DragLeft { .. } => { From 4d92ce5c29899026172b03a97efa959dfa4577f1 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 16:14:25 +0200 Subject: [PATCH 53/87] Allow switching on requested type, allowing the application to be generic over e.g. image types --- winit-core/src/data_transfer.rs | 37 +++++++++----- winit-wayland/src/dnd.rs | 55 ++++++++++++--------- winit/examples/dnd.rs | 86 ++++++++++++++++++++++++++++----- 3 files changed, 132 insertions(+), 46 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 4d891fe7b8..1fbd143e91 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -159,7 +159,7 @@ impl TransferType for TypeHint { } fn matches(&self, other: &dyn TransferType) -> bool { - other.hint() == Some(*self) + other.hint().is_some_and(|hint| self.matches(&hint)) } } @@ -288,7 +288,7 @@ impl From> for SendData { /// implementation of this trait dynamically in a cross-platform way, use [`DataTransferSendBuilder`]. pub trait DataTransferSend: DataTransfer { /// Get the data for the specified type, or `None` if this value does not supply the given data type. - fn data_for_type(&self, type_: &dyn TransferType) -> Option; + fn data_for_type(&mut self, type_: &dyn TransferType) -> Option; /// If `true`, this data transfer is only valid for the application sending the data. /// @@ -305,7 +305,7 @@ pub enum InternalTransferMarker {} /// Marker for a [`DataTransferSendBuilder`] which is external. pub enum ExternalTransferMarker {} -type SendDataCallback = Box SendData>; +type SendDataCallback = Box Option>; /// Dynamic builder for an implementation of [`DataTransferSend`]. /// @@ -350,7 +350,7 @@ impl DataTransferSend for DataTransferSendBuilder where T: fmt::Debug + 'static, { - fn data_for_type(&self, type_: &dyn TransferType) -> Option { + fn data_for_type(&mut self, type_: &dyn TransferType) -> Option { self.data_for_type(type_) } @@ -363,7 +363,7 @@ impl DataTransferSend for DataTransferSendBuilder where T: fmt::Debug + 'static, { - fn data_for_type(&self, type_: &dyn TransferType) -> Option { + fn data_for_type(&mut self, type_: &dyn TransferType) -> Option { self.data_for_type(type_) } @@ -389,29 +389,40 @@ impl DataTransferSendBuilder { } impl DataTransferSendBuilder { - fn data_for_type(&self, type_: &dyn TransferType) -> Option { + fn data_for_type(&mut self, type_: &dyn TransferType) -> Option { let (_, func) = self.types.iter().find(|(ty, _)| ty.matches(type_))?; - Some(func(&self.state)) + func(&mut self.state, type_) } /// Add a callback which converts the builder's state to the given type. In /// most cases, `type_` will be [`TypeHint`]. - pub fn add_type(&mut self, type_: Ty, func: F) -> &mut Self + pub fn add_type(&mut self, type_: Ty, func: F) -> &mut Self where Ty: TransferType, - F: Fn(&T) -> SendData + 'static, + F: Fn(&mut T, &dyn TransferType) -> Option + 'static, + O: Into, { - self.types.push((Box::new(type_), Box::new(func))); + self.types + .push((Box::new(type_), Box::new(move |state, ty| func(state, ty).map(Into::into)))); self } /// Return a new builder, adding a callback which converts the builder's state - /// to the given type. In most cases, `type_` will be [`TypeHint`]. - pub fn with_type(mut self, type_: Ty, func: F) -> Self + /// to the given type. + /// + /// For cross-platform use, `type_` will be [`TypeHint`]. The closure additionally receives + /// a [`TransferType`], which is not necessarily the same as `type_` for the following reasons: + /// + /// - The OS may have multiple types which are equivalent to the supplied type + /// - `TypeHint::Audio` and `TypeHint::Image` with `extension_hint: None` will advertise all + /// supported audio and image formats, in which case the closure may receive a type with + /// an extension chosen by the receiving application. + pub fn with_type(mut self, type_: Ty, func: F) -> Self where Ty: TransferType, - F: Fn(&T) -> SendData + 'static, + F: Fn(&mut T, &dyn TransferType) -> Option + 'static, + O: Into, { self.add_type(type_, func); self diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index be3c9cc307..229ee9dc33 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -54,7 +54,7 @@ impl DataSourceHandler for WinitState { mime: String, mut fd: WritePipe, ) { - let Some(data) = self.dnd_state.send_drag_data() else { + let Some(data) = self.dnd_state.send_drag_data_mut() else { // TODO: Is there a way to explicitly express that the data was not sent? return; }; @@ -88,17 +88,19 @@ impl DataSourceHandler for WinitState { } } }, - SendData::String(str) => match mime.charset().unwrap_or(Charset::Utf16) { - Charset::Utf8 => { - let _ = fd.write_all(str.as_bytes()); - }, - Charset::Utf16 => { - let utf16_binary = str - .encode_utf16() - .flat_map(|uint16| uint16.to_le_bytes()) - .collect::>(); - let _ = fd.write_all(&utf16_binary); - }, + SendData::String(str) => { + match mime.parse_charset().or(mime.default_charset()).unwrap_or_default() { + Charset::Utf8 => { + let _ = fd.write_all(str.as_bytes()); + }, + Charset::Utf16 => { + let utf16_binary = str + .encode_utf16() + .flat_map(|uint16| uint16.to_ne_bytes()) + .collect::>(); + let _ = fd.write_all(&utf16_binary); + }, + } }, SendData::Bytes(binary) => { let _ = fd.write_all(&binary); @@ -151,8 +153,9 @@ impl DataSourceHandler for WinitState { } } -#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)] enum Charset { + #[default] Utf8, Utf16, } @@ -241,18 +244,18 @@ impl MimeType { let downcast_failed = downcast.is_none(); // This filter is a bit hacky, but it's the only way to ensure that we always // return the same type. - let from_hint = type_.hint().filter(|_| downcast_failed).into_iter().flat_map(|hint| { + let from_hint = Some(()).filter(|_| downcast_failed).into_iter().flat_map(move |()| { Self::MIME_HINT_MAP .iter() - .filter(move |(_, haystack)| *haystack == hint) - .map(move |(mime, _)| Self { mime: mime.to_string().into(), hint: Some(hint) }) + .filter(move |(_, haystack)| TransferType::matches(haystack, type_)) + .map(move |(mime, _)| Self { mime: mime.to_string().into(), hint: type_.hint() }) }); downcast.into_iter().chain(from_hint) } // TODO: We should properly parse MIME types using `mime` or a similar crate. - fn charset(&self) -> Option { + fn parse_charset(&self) -> Option { let (_essence, options) = self.mime.split_once(';')?; let (_, charset) = options.split_once("charset=")?; @@ -266,6 +269,14 @@ impl MimeType { } } + fn default_charset(&self) -> Option { + match self.hint? { + TypeHint::Plaintext => Some(Charset::Utf8), + TypeHint::Html => Some(Charset::Utf16), + _ => None, + } + } + fn parse(mime: String) -> Self { let hint = Self::MIME_HINT_MAP .iter() @@ -378,7 +389,7 @@ impl TypedData for MimeData { }; // Default charset is UTF-16 for some reason - let charset = self.mime_type.charset().unwrap_or(Charset::Utf16); + let charset = self.mime_type.parse_charset().unwrap_or(Charset::Utf16); match charset { Charset::Utf8 => { @@ -489,8 +500,8 @@ impl DragSource { } /// Per-type data to be sent. See [`DataTransferSend`]. - pub fn data(&self) -> &dyn DataTransferSend { - &*self.data + pub fn data(&mut self) -> &mut dyn DataTransferSend { + &mut *self.data } } @@ -612,8 +623,8 @@ impl DndState { self.send_drag = None; } - pub(crate) fn send_drag_data(&self) -> Option<&dyn DataTransferSend> { - self.send_drag.as_ref().map(|send| send.data()) + pub(crate) fn send_drag_data_mut(&mut self) -> Option<&mut dyn DataTransferSend> { + self.send_drag.as_mut().map(|send| send.data()) } } diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index a4a28b2e8c..e7142fc109 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -86,21 +86,48 @@ impl ApplicationHandler for Application { let result = event_loop.start_drag( window_id, DataTransferSendBuilder::new(()) - .with_type(TypeHint::Plaintext, |()| "Winit example".to_string().into()) - .with_type(TypeHint::Html, |()| { - "Winit example".to_string().into() + .with_type(TypeHint::Plaintext, |(), _| { + Some("Winit example".to_string()) }) - .with_type(TypeHint::Image { extension_hint: Some("png") }, |()| { - DRAG_IMAGE.to_vec().into() + .with_type(TypeHint::Html, |(), _| { + Some("Winit example".to_string()) }) - .with_type(TypeHint::UriList, |()| { + // You can advertise a `TypeHint` that can match many types, and switch + // inside the callback. For example, this will match any image type. This + // may be desirable on some platforms which restrict the set of image types + // that can be sent. + .with_type(TypeHint::Image { extension_hint: None }, |(), ty| { + let hint = ty.hint()?; + match hint { + TypeHint::Image { extension_hint: None | Some("png") } => { + info!("Destination requested image as png"); + Some(DRAG_IMAGE.to_vec()) + }, + TypeHint::Image { extension_hint: Some(ext) } => { + info!( + "Destination requested image as {ext}, converting..." + ); + let image = image::load_from_memory(DRAG_IMAGE) + .unwrap() + .into_rgb8(); + let format = image::ImageFormat::from_extension(ext)?; + let mut out_buf = Vec::new(); + let mut out_writer = std::io::Cursor::new(&mut out_buf); + + image.write_to(&mut out_writer, format).ok()?; + + Some(out_buf) + }, + _ => None, + } + }) + .with_type(TypeHint::UriList, |(), _| { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let root = manifest_dir.parent().unwrap(); let this_file = root.join(file!()); let icon_file = this_file.parent().unwrap().join("data/icon.png"); let icon_file = icon_file.display(); - - vec![OsString::from(format!("file://{icon_file}"))].into() + Some(vec![OsString::from(format!("file://{icon_file}"))]) }) .build(), &DndActions::All, @@ -130,6 +157,31 @@ impl ApplicationHandler for Application { let uris = data.try_as_uris().unwrap(); info!("{uris:#?}"); }, + Some(TypeHint::Image { extension_hint: ext }) => { + let mut bytes = Vec::new(); + 'read_image: { + if data.try_read().unwrap().read_to_end(&mut bytes).is_err() { + warn!("Could not read!"); + break 'read_image; + } + + let format = ext.and_then(image::ImageFormat::from_extension); + + let reader = std::io::Cursor::new(&bytes[..]); + let reader = match format { + Some(fmt) => image::ImageReader::with_format(reader, fmt), + None => image::ImageReader::new(reader), + }; + + if let Ok(image) = reader.decode() { + let width = image.width(); + let height = image.height(); + info!("Received image ({width}x{height})"); + } else { + warn!("Failed to decode jpeg"); + } + } + }, _ => { unreachable!("Received a type we didn't ask for!"); }, @@ -151,9 +203,21 @@ impl ApplicationHandler for Application { info!("Types: {:#?}", data_transfer.available_types()); - let valid_type = [TypeHint::Html, TypeHint::UriList, TypeHint::Plaintext] - .into_iter() - .find(|ty| data_transfer.has_type(ty)); + let readable_image_types = image::ImageFormat::all() + .filter(|fmt| fmt.reading_enabled()) + .filter_map(|fmt| { + let ext = fmt.extensions_str().first()?; + + Some(TypeHint::Image { extension_hint: Some(ext) }) + }); + + let mut valid_types = readable_image_types.chain([ + TypeHint::Html, + TypeHint::UriList, + TypeHint::Plaintext, + ]); + + let valid_type = valid_types.find(|ty| data_transfer.has_type(ty)); let Some(type_) = valid_type else { event_loop.set_valid_actions(id, &DndActions::none()).unwrap(); From cce666a6e06f32d2b143918a6bba8cd0cd7b4c03 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 28 May 2026 13:57:27 +0200 Subject: [PATCH 54/87] Complete win32 drag-and-drop by reading dropped data Read the dropped data on the event-loop thread and serve it through the cross-platform data-transfer API, so plain text (and files) can be received. --- winit-win32/Cargo.toml | 1 + winit-win32/src/definitions.rs | 11 +- winit-win32/src/dnd.rs | 284 ++++++++++++++++++++------- winit-win32/src/event_loop.rs | 18 +- winit-win32/src/event_loop/runner.rs | 26 ++- winit-win32/src/lib.rs | 2 +- winit-win32/src/window.rs | 6 +- 7 files changed, 262 insertions(+), 86 deletions(-) diff --git a/winit-win32/Cargo.toml b/winit-win32/Cargo.toml index 5a5df3ff9a..4f71eebcb7 100644 --- a/winit-win32/Cargo.toml +++ b/winit-win32/Cargo.toml @@ -33,6 +33,7 @@ windows-sys = { workspace = true, features = [ "Win32_System_Com_StructuredStorage", "Win32_System_Com", "Win32_System_LibraryLoader", + "Win32_System_Memory", "Win32_System_Ole", "Win32_Security", "Win32_System_SystemInformation", diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index b9b4dae73f..4b7fede5eb 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -8,12 +8,12 @@ use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM}; use windows_sys::core::{BOOL, GUID, HRESULT}; pub type IUnknown = *mut c_void; -#[expect(dead_code, reason = "TODO")] +#[allow(dead_code, reason = "part of the IDataObject vtable ABI; not called by winit")] pub type IAdviseSink = *mut c_void; pub type IDataObject = *mut c_void; -#[expect(dead_code, reason = "TODO")] +#[allow(dead_code, reason = "part of the IDataObject vtable ABI; not called by winit")] pub type IEnumFORMATETC = *mut c_void; -#[expect(dead_code, reason = "TODO")] +#[allow(dead_code, reason = "part of the IDataObject vtable ABI; not called by winit")] pub type IEnumSTATDATA = *mut c_void; #[repr(C)] @@ -27,7 +27,10 @@ pub struct IUnknownVtbl { pub Release: unsafe extern "system" fn(This: *mut IUnknown) -> u32, } -#[expect(dead_code, reason = "TODO")] +#[allow( + dead_code, + reason = "the full vtable layout is required for ABI; not all methods are called" +)] #[repr(C)] pub struct IDataObjectVtbl { pub parent: IUnknownVtbl, diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 3eabea1d09..d1e7f5bfa6 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -1,25 +1,29 @@ use std::collections::HashMap; use std::ffi::{OsString, c_void}; +use std::io; +use std::ops::ControlFlow; use std::os::windows::ffi::OsStringExt; -use std::path::PathBuf; -use std::ptr; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; use dpi::PhysicalPosition; -use tracing::debug; -use windows_sys::Win32::Foundation::{DV_E_FORMATETC, E_ABORT, HWND, POINT, POINTL, S_OK}; +use windows_sys::Win32::Foundation::{E_ABORT, HGLOBAL, HWND, POINT, POINTL, S_OK}; use windows_sys::Win32::Graphics::Gdi::ScreenToClient; -use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}; -use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE}; +use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_HGLOBAL}; +use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock}; +use windows_sys::Win32::System::Ole::{ + CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_NONE, ReleaseStgMedium, +}; use windows_sys::Win32::UI::Shell::{DragQueryFileW, HDROP}; use windows_sys::core::{GUID, HRESULT}; -use winit_core::data_transfer::{DataTransferId, TypeHint}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; use winit_core::event::WindowEvent; use crate::definitions::{ IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl, }; +use crate::event_loop::EventLoopRunner; #[derive(Default, Debug)] pub struct FileDropDataShared { @@ -33,25 +37,200 @@ impl FileDropDataShared { } } +#[derive(Debug)] enum DataKind { - Uris(Vec), + Uris(Vec), String(String), + #[allow(dead_code, reason = "populated once more types are read eagerly")] Bytes(Vec), } -struct DataObject { - // TODO: Exposing the full native API to client applications is too error-prone so long as - // winit is still manually implementing refcounting and using the win32 APIs. For now, we - // just eagerly read all the data supported by cross-platform type hints on Windows. This - // would be resolved by migrating to `windows-rs`. +// TODO: Exposing the full native API to client applications is too error-prone so long as +// winit is still manually implementing refcounting and using the win32 APIs. For now, we +// just eagerly read all the data supported by cross-platform type hints on Windows. This +// would be resolved by migrating to `windows-rs`. +#[derive(Debug)] +pub(crate) struct DataObject { data: HashMap, } +impl DataObject { + unsafe fn from_idataobject(data_obj: *const IDataObject) -> Self { + let mut data = HashMap::new(); + + if let Some(text) = unsafe { read_unicode_text(data_obj) } { + data.insert(TypeHint::Plaintext, DataKind::String(text)); + } + + if let Some(uris) = unsafe { read_uri_list(data_obj) } { + if !uris.is_empty() { + data.insert(TypeHint::UriList, DataKind::Uris(uris)); + } + } + + Self { data } + } + + fn resolve(&self, requested: TypeHint) -> Option { + self.data.keys().copied().find(|stored| stored.matches(&requested)) + } +} + +/// RAII wrapper around an STGMEDIUM returned by IDataObject::GetData, releasing it on drop. +struct StgMedium(STGMEDIUM); + +impl StgMedium { + /// Returns `None` if the object doesn't provide the format. + unsafe fn get(data_obj: *const IDataObject, cf_format: u16) -> Option { + let format = FORMATETC { + cfFormat: cf_format, + ptd: std::ptr::null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL as u32, + }; + + let mut medium = unsafe { std::mem::zeroed::() }; + let get_data = unsafe { (*(*data_obj).cast::()).GetData }; + if unsafe { get_data(data_obj as *mut _, &format, &mut medium) } < 0 { + return None; + } + + Some(Self(medium)) + } + + fn hglobal(&self) -> HGLOBAL { + unsafe { self.0.u.hGlobal } + } +} + +impl Drop for StgMedium { + fn drop(&mut self) { + unsafe { ReleaseStgMedium(&mut self.0) }; + } +} + +unsafe fn read_unicode_text(data_obj: *const IDataObject) -> Option { + let medium = unsafe { StgMedium::get(data_obj, CF_UNICODETEXT) }?; + let hglobal = medium.hglobal(); + + let ptr = unsafe { GlobalLock(hglobal) }; + if ptr.is_null() { + return None; + } + + // `CF_UNICODETEXT` is a NUL-terminated UTF-16 string. Cap the scan by the allocation size in + // case the buffer isn't terminated. + let max_units = unsafe { GlobalSize(hglobal) } / std::mem::size_of::(); + let wide = unsafe { std::slice::from_raw_parts(ptr.cast::(), max_units) }; + let len = wide.iter().position(|&c| c == 0).unwrap_or(max_units); + let text = String::from_utf16_lossy(&wide[..len]); + + unsafe { GlobalUnlock(hglobal) }; + + Some(text) +} + +unsafe fn read_uri_list(data_obj: *const IDataObject) -> Option> { + let medium = unsafe { StgMedium::get(data_obj, CF_HDROP) }?; + let hdrop = medium.hglobal() as HDROP; + + // The second parameter (0xFFFFFFFF) instructs the function to return the item count. + let item_count = unsafe { DragQueryFileW(hdrop, 0xffff_ffff, std::ptr::null_mut(), 0) }; + + let mut paths = Vec::with_capacity(item_count as usize); + for i in 0..item_count { + // Get the length of the path string NOT including the terminating null character. + // Previously, this was using a fixed size array of MAX_PATH length, but the Windows + // API allows longer paths under certain circumstances. + let character_count = unsafe { DragQueryFileW(hdrop, i, std::ptr::null_mut(), 0) } as usize; + let str_len = character_count + 1; + + let mut path_buf = Vec::::with_capacity(str_len); + unsafe { + DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); + path_buf.set_len(str_len); + } + + paths.push(OsString::from_wide(&path_buf[..character_count])); + } + + Some(paths) +} + +#[derive(Debug)] +pub(crate) struct WinDataTransfer { + data: Rc, +} + +impl WinDataTransfer { + pub(crate) fn new(data: Rc) -> Self { + Self { data } + } +} + +impl DataTransfer for WinDataTransfer { + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>, + ) { + for hint in self.data.data.keys() { + if let ControlFlow::Break(()) = func(hint) { + break; + } + } + } +} + +#[derive(Debug)] +pub(crate) struct WinTypedData { + type_: TypeHint, + data: Rc, +} + +impl WinTypedData { + pub(crate) fn new(data: Rc, requested: TypeHint) -> Option { + let type_ = data.resolve(requested)?; + Some(Self { type_, data }) + } +} + +impl TypedData for WinTypedData { + fn type_(&self) -> &dyn TransferType { + &self.type_ + } + + fn try_read(&mut self) -> Option> { + match self.data.data.get(&self.type_)? { + DataKind::Bytes(bytes) => Some(Box::new(io::Cursor::new(bytes.clone()))), + DataKind::String(string) => { + Some(Box::new(io::Cursor::new(string.clone().into_bytes()))) + }, + DataKind::Uris(_) => None, + } + } + + fn try_as_uris(&mut self) -> io::Result> { + match self.data.data.get(&self.type_) { + Some(DataKind::Uris(uris)) => Ok(uris.clone()), + _ => Err(io::ErrorKind::InvalidData.into()), + } + } + + fn try_as_string(&mut self) -> io::Result { + match self.data.data.get(&self.type_) { + Some(DataKind::String(string)) => Ok(string.clone()), + _ => Err(io::ErrorKind::InvalidData.into()), + } + } +} + #[repr(C)] pub struct FileDropHandlerData { interface: IDropTarget, refcount: AtomicUsize, window: HWND, + runner: Rc, send_event: Box, shared: Arc, active_data_transfer_id: Option, @@ -76,6 +255,7 @@ pub struct FileDropHandler { impl FileDropHandler { pub(crate) fn new( window: HWND, + runner: Rc, shared: Arc, send_event: Box, ) -> FileDropHandler { @@ -83,6 +263,7 @@ impl FileDropHandler { interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl }, refcount: AtomicUsize::new(1), window, + runner, send_event, active_data_transfer_id: None, shared, @@ -115,6 +296,11 @@ impl FileDropHandler { let drop_handler = unsafe { Self::from_interface(this) }; let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; if count == 0 { + // Drop any transfer still in flight (e.g. the window was destroyed mid-drag, so no + // `DragLeave`/`Drop` ever arrived to clean it up). + if let Some(id) = drop_handler.active_data_transfer_id.take() { + drop_handler.runner.remove_data_transfer(id); + } // Destroy the underlying data drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); } @@ -123,7 +309,7 @@ impl FileDropHandler { unsafe extern "system" fn DragEnter( this: *mut IDropTarget, - _pDataObj: *const IDataObject, + pDataObj: *const IDataObject, _grfKeyState: u32, pt: POINTL, pdwEffect: *mut u32, @@ -134,6 +320,15 @@ impl FileDropHandler { let data_transfer_id = DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)); drop_handler.active_data_transfer_id = Some(data_transfer_id); + + // Make the new transfer visible to `accept_drag`/`reject_drag` and reset acceptance for + // this drag. + drop_handler.shared.transfer_id.store(data_transfer_id.into_raw(), Ordering::Relaxed); + drop_handler.shared.accepted.store(false, Ordering::Relaxed); + + let data = Rc::new(unsafe { DataObject::from_idataobject(pDataObj) }); + drop_handler.runner.register_data_transfer(data_transfer_id, data); + let mut pt = POINT { x: pt.x, y: pt.y }; unsafe { ScreenToClient(drop_handler.window, &mut pt); @@ -180,11 +375,12 @@ impl FileDropHandler { unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { let drop_handler = unsafe { Self::from_interface(this) }; - let Some(data_transfer_id) = drop_handler.active_data_transfer_id else { + let Some(data_transfer_id) = drop_handler.active_data_transfer_id.take() else { return E_ABORT; }; (drop_handler.send_event)(WindowEvent::DragLeft { id: data_transfer_id }); + drop_handler.runner.remove_data_transfer(data_transfer_id); S_OK } @@ -197,7 +393,7 @@ impl FileDropHandler { pdwEffect: *mut u32, ) -> HRESULT { let drop_handler = unsafe { Self::from_interface(this) }; - let Some(data_transfer_id) = drop_handler.active_data_transfer_id else { + let Some(data_transfer_id) = drop_handler.active_data_transfer_id.take() else { unsafe { *pdwEffect = DROPEFFECT_NONE; } @@ -217,64 +413,16 @@ impl FileDropHandler { *pdwEffect = drop_handler.cursor_effect(); } + // The application has had a chance to read the data while handling `DragDropped`; the + // transfer's lifecycle ends here. + drop_handler.runner.remove_data_transfer(data_transfer_id); + S_OK } unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData { unsafe { &mut *(this as *mut _) } } - - #[allow(dead_code, reason = "TODO")] - unsafe fn iterate_filenames(data_obj: *const IDataObject, mut callback: F) -> Option - where - F: FnMut(PathBuf), - { - let drop_format = FORMATETC { - cfFormat: CF_HDROP, - ptd: ptr::null_mut(), - dwAspect: DVASPECT_CONTENT, - lindex: -1, - tymed: TYMED_HGLOBAL as u32, - }; - - let mut medium = unsafe { std::mem::zeroed() }; - let get_data_fn = unsafe { (*(*data_obj).cast::()).GetData }; - let get_data_result = unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) }; - if get_data_result >= 0 { - let hdrop = unsafe { medium.u.hGlobal as HDROP }; - - // The second parameter (0xFFFFFFFF) instructs the function to return the item count - let item_count = unsafe { DragQueryFileW(hdrop, 0xffffffff, ptr::null_mut(), 0) }; - - for i in 0..item_count { - // Get the length of the path string NOT including the terminating null character. - // Previously, this was using a fixed size array of MAX_PATH length, but the - // Windows API allows longer paths under certain circumstances. - let character_count = - unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize }; - let str_len = character_count + 1; - - // Fill path_buf with the null-terminated file name - let mut path_buf = Vec::with_capacity(str_len); - unsafe { - DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); - path_buf.set_len(str_len); - } - - callback(OsString::from_wide(&path_buf[0..character_count]).into()); - } - - Some(hdrop) - } else if get_data_result == DV_E_FORMATETC { - // If the dropped item is not a file this error will occur. - // In this case it is OK to return without taking further action. - debug!("Error occurred while processing dropped/hovered item: item is not a file."); - None - } else { - debug!("Unexpected error occurred while processing dropped/hovered item."); - None - } - } } impl Drop for FileDropHandler { diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 593a49d7fa..f1827aca06 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -83,7 +83,7 @@ pub(super) use self::runner::{Event, EventLoopRunner}; use super::SelectedCursor; use super::window::set_skip_taskbar; use crate::dark_mode::try_theme; -use crate::dnd::FileDropHandler; +use crate::dnd::{FileDropHandler, WinDataTransfer, WinTypedData}; use crate::dpi::{become_dpi_aware, dpi_to_scale_factor}; use crate::icon::WinCursor; use crate::ime::ImeContext; @@ -485,14 +485,20 @@ impl RootActiveEventLoop for ActiveEventLoop { id: DataTransferId, type_: &dyn TransferType, ) -> Result, RequestError> { - let _ = id; - let _ = type_; - todo!() + let Some(data) = self.0.data_transfer(id) else { + return Err(RequestError::Ignored); + }; + let hint = type_.hint().ok_or(RequestError::Ignored)?; + + WinTypedData::new(data, hint).map(|value| Box::new(value) as _).ok_or(RequestError::Ignored) } fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { - let _ = id; - todo!() + let Some(data) = self.0.data_transfer(id) else { + return Err(RequestError::Ignored); + }; + + Ok(Box::new(WinDataTransfer::new(data))) } } diff --git a/winit-win32/src/event_loop/runner.rs b/winit-win32/src/event_loop/runner.rs index f4bc643598..cc4465060f 100644 --- a/winit-win32/src/event_loop/runner.rs +++ b/winit-win32/src/event_loop/runner.rs @@ -1,6 +1,6 @@ use std::any::Any; use std::cell::{Cell, RefCell}; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -9,11 +9,13 @@ use std::{fmt, mem, panic}; use dpi::PhysicalSize; use windows_sys::Win32::Foundation::HWND; use winit_core::application::ApplicationHandler; +use winit_core::data_transfer::DataTransferId; use winit_core::event::{DeviceEvent, DeviceId, StartCause, SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::ActiveEventLoop as RootActiveEventLoop; use winit_core::window::WindowId; use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor}; +use crate::dnd::DataObject; use crate::event_loop::{GWL_USERDATA, WindowData}; use crate::util::get_window_long; @@ -37,8 +39,11 @@ pub(crate) struct EventLoopRunner { event_handler: Rc, event_buffer: RefCell>, - // TODO - // data_transfers_per_window: RefCell>>, + /// Drag-and-drop data, cached for the lifetime of each transfer (i.e. between `DragEntered` + /// and `DragLeft`/`DragDropped`). Looked up by [`Self::data_transfer`] to serve the + /// asynchronous `ActiveEventLoop` data-transfer API. + data_transfers: RefCell>>, + panic_error: Cell>, } @@ -89,9 +94,22 @@ impl EventLoopRunner { last_events_cleared: Cell::new(Instant::now()), event_handler: Rc::new(Cell::new(None)), event_buffer: RefCell::new(VecDeque::new()), + data_transfers: RefCell::new(HashMap::new()), } } + pub(crate) fn register_data_transfer(&self, id: DataTransferId, data: Rc) { + self.data_transfers.borrow_mut().insert(id, data); + } + + pub(crate) fn remove_data_transfer(&self, id: DataTransferId) { + self.data_transfers.borrow_mut().remove(&id); + } + + pub(crate) fn data_transfer(&self, id: DataTransferId) -> Option> { + self.data_transfers.borrow().get(&id).cloned() + } + /// Associate the application's event handler with the runner. /// /// # Safety @@ -140,12 +158,14 @@ impl EventLoopRunner { last_events_cleared: _, event_handler, event_buffer: _, + data_transfers, } = self; interrupt_msg_dispatch.set(false); runner_state.set(RunnerState::Uninitialized); panic_error.set(None); exit.set(None); event_handler.set(None); + data_transfers.borrow_mut().clear(); } } diff --git a/winit-win32/src/lib.rs b/winit-win32/src/lib.rs index 4f922831a3..3c946b64a8 100644 --- a/winit-win32/src/lib.rs +++ b/winit-win32/src/lib.rs @@ -557,7 +557,7 @@ impl WindowAttributesWindows { /// does that, but there may be more in the future. If you need COM API with /// `COINIT_MULTITHREADED` you must initialize it before calling any winit functions. See for more information. pub fn with_drag_and_drop(mut self, flag: bool) -> Self { - self.drag_and_drop = flag.then(|| Default::default()); + self.drag_and_drop = flag.then(Default::default); self } diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index 5a09b35101..d2ffc6c73a 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -3,7 +3,6 @@ use std::cell::Cell; use std::ffi::c_void; use std::mem::{self, MaybeUninit}; use std::rc::Rc; -use std::sync::atomic::Ordering; use std::sync::mpsc::channel; use std::sync::{Arc, Mutex, MutexGuard}; use std::{io, panic, ptr}; @@ -49,14 +48,12 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ WM_SYSCOMMAND, WNDCLASSEXW, }; use winit_core::cursor::Cursor; -use winit_core::data_transfer::DataTransferId; use winit_core::error::RequestError; use winit_core::icon::{Icon, RgbaIcon}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, - UnknownDataTransfer, UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, - WindowId, WindowLevel, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, }; use crate::dark_mode::try_theme; @@ -1237,6 +1234,7 @@ impl InitData<'_> { let window_id = win.id(); let mut file_drop_handler = FileDropHandler::new( win.window.hwnd(), + self.runner.clone(), shared, Box::new(move |event| { file_drop_runner.send_event(Event::Window { window_id, event }) From 708962e67a17edd30de1e6c8c1159df1621d831d Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 28 May 2026 14:23:23 +0200 Subject: [PATCH 55/87] Use the actual copied length when reading dropped file paths The copying `DragQueryFileW` call's return value was discarded and the buffer length was assumed, so a short copy would expose uninitialized memory to `OsString::from_wide`. --- winit-win32/src/dnd.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index d1e7f5bfa6..8ed8e0b877 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -140,19 +140,18 @@ unsafe fn read_uri_list(data_obj: *const IDataObject) -> Option> { let mut paths = Vec::with_capacity(item_count as usize); for i in 0..item_count { - // Get the length of the path string NOT including the terminating null character. - // Previously, this was using a fixed size array of MAX_PATH length, but the Windows - // API allows longer paths under certain circumstances. + // Query the path length (excluding the NUL terminator), reserve room for it plus the + // terminator, then copy. `set_len` uses the count actually written, so a short copy can + // never expose uninitialized memory. let character_count = unsafe { DragQueryFileW(hdrop, i, std::ptr::null_mut(), 0) } as usize; - let str_len = character_count + 1; - let mut path_buf = Vec::::with_capacity(str_len); - unsafe { - DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); - path_buf.set_len(str_len); - } + let mut path_buf = Vec::::with_capacity(character_count + 1); + let copied = + unsafe { DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), character_count as u32 + 1) } + as usize; + unsafe { path_buf.set_len(copied) }; - paths.push(OsString::from_wide(&path_buf[..character_count])); + paths.push(OsString::from_wide(&path_buf)); } Some(paths) From 649ad6fda3ca0b024ca6ed045c0c9453134faff7 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Mon, 1 Jun 2026 16:33:59 +0200 Subject: [PATCH 56/87] Receive PNG image drops on win32 --- winit-win32/Cargo.toml | 1 + winit-win32/src/dnd.rs | 32 +++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/winit-win32/Cargo.toml b/winit-win32/Cargo.toml index 4f71eebcb7..3564a0d9c1 100644 --- a/winit-win32/Cargo.toml +++ b/winit-win32/Cargo.toml @@ -32,6 +32,7 @@ windows-sys = { workspace = true, features = [ "Win32_Media", "Win32_System_Com_StructuredStorage", "Win32_System_Com", + "Win32_System_DataExchange", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Ole", diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 8ed8e0b877..58878e5bab 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -11,6 +11,7 @@ use dpi::PhysicalPosition; use windows_sys::Win32::Foundation::{E_ABORT, HGLOBAL, HWND, POINT, POINTL, S_OK}; use windows_sys::Win32::Graphics::Gdi::ScreenToClient; use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_HGLOBAL}; +use windows_sys::Win32::System::DataExchange::RegisterClipboardFormatW; use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock}; use windows_sys::Win32::System::Ole::{ CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_NONE, ReleaseStgMedium, @@ -24,6 +25,7 @@ use crate::definitions::{ IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl, }; use crate::event_loop::EventLoopRunner; +use crate::util; #[derive(Default, Debug)] pub struct FileDropDataShared { @@ -41,7 +43,6 @@ impl FileDropDataShared { enum DataKind { Uris(Vec), String(String), - #[allow(dead_code, reason = "populated once more types are read eagerly")] Bytes(Vec), } @@ -68,6 +69,12 @@ impl DataObject { } } + if let Some(png) = unsafe { read_png(data_obj) } { + if !png.is_empty() { + data.insert(TypeHint::Image { extension_hint: Some("png") }, DataKind::Bytes(png)); + } + } + Self { data } } @@ -157,6 +164,29 @@ unsafe fn read_uri_list(data_obj: *const IDataObject) -> Option> { Some(paths) } +unsafe fn read_png(data_obj: *const IDataObject) -> Option> { + let format_name = util::encode_wide("PNG"); + let format = unsafe { RegisterClipboardFormatW(format_name.as_ptr()) }; + if format == 0 { + return None; + } + + let medium = unsafe { StgMedium::get(data_obj, format as u16) }?; + let hglobal = medium.hglobal(); + + let ptr = unsafe { GlobalLock(hglobal) }; + if ptr.is_null() { + return None; + } + + let len = unsafe { GlobalSize(hglobal) }; + let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::(), len) }.to_vec(); + + unsafe { GlobalUnlock(hglobal) }; + + Some(bytes) +} + #[derive(Debug)] pub(crate) struct WinDataTransfer { data: Rc, From 5fb620869b255d8b05c6cd12eb69e3e852fa9eb0 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Mon, 1 Jun 2026 17:52:45 +0200 Subject: [PATCH 57/87] Accept drops on win32 via set_valid_actions --- winit-win32/src/dnd.rs | 92 ++++++++++++++++------------ winit-win32/src/event_loop.rs | 17 ++++- winit-win32/src/event_loop/runner.rs | 45 ++++++++++---- winit-win32/src/lib.rs | 7 +-- winit-win32/src/window.rs | 5 +- winit-win32/src/window_state.rs | 9 +-- 6 files changed, 108 insertions(+), 67 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 58878e5bab..14c979805d 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -4,8 +4,7 @@ use std::io; use std::ops::ControlFlow; use std::os::windows::ffi::OsStringExt; use std::rc::Rc; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; use dpi::PhysicalPosition; use windows_sys::Win32::Foundation::{E_ABORT, HGLOBAL, HWND, POINT, POINTL, S_OK}; @@ -14,11 +13,13 @@ use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TY use windows_sys::Win32::System::DataExchange::RegisterClipboardFormatW; use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock}; use windows_sys::Win32::System::Ole::{ - CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_NONE, ReleaseStgMedium, + CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, + ReleaseStgMedium, }; use windows_sys::Win32::UI::Shell::{DragQueryFileW, HDROP}; use windows_sys::core::{GUID, HRESULT}; use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use winit_core::event_loop::DndActions; use winit_core::event::WindowEvent; use crate::definitions::{ @@ -27,18 +28,6 @@ use crate::definitions::{ use crate::event_loop::EventLoopRunner; use crate::util; -#[derive(Default, Debug)] -pub struct FileDropDataShared { - transfer_id: AtomicI64, - pub accepted: AtomicBool, -} - -impl FileDropDataShared { - pub fn transfer_id(&self) -> DataTransferId { - DataTransferId::from_raw(self.transfer_id.load(Ordering::Relaxed)) - } -} - #[derive(Debug)] enum DataKind { Uris(Vec), @@ -261,21 +250,9 @@ pub struct FileDropHandlerData { window: HWND, runner: Rc, send_event: Box, - shared: Arc, active_data_transfer_id: Option, } -impl FileDropHandlerData { - fn cursor_effect(&self) -> u32 { - if self.shared.accepted.load(Ordering::Relaxed) { - // TODO: Handle other kinds of drop effect - DROPEFFECT_COPY - } else { - DROPEFFECT_NONE - } - } -} - pub struct FileDropHandler { data: *mut FileDropHandlerData, } @@ -285,7 +262,6 @@ impl FileDropHandler { pub(crate) fn new( window: HWND, runner: Rc, - shared: Arc, send_event: Box, ) -> FileDropHandler { let data = Box::new(FileDropHandlerData { @@ -295,7 +271,6 @@ impl FileDropHandler { runner, send_event, active_data_transfer_id: None, - shared, }); FileDropHandler { data: Box::into_raw(data) } } @@ -350,11 +325,6 @@ impl FileDropHandler { DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)); drop_handler.active_data_transfer_id = Some(data_transfer_id); - // Make the new transfer visible to `accept_drag`/`reject_drag` and reset acceptance for - // this drag. - drop_handler.shared.transfer_id.store(data_transfer_id.into_raw(), Ordering::Relaxed); - drop_handler.shared.accepted.store(false, Ordering::Relaxed); - let data = Rc::new(unsafe { DataObject::from_idataobject(pDataObj) }); drop_handler.runner.register_data_transfer(data_transfer_id, data); @@ -376,7 +346,7 @@ impl FileDropHandler { unsafe extern "system" fn DragOver( this: *mut IDropTarget, - _grfKeyState: u32, + grfKeyState: u32, pt: POINTL, pdwEffect: *mut u32, ) -> HRESULT { @@ -389,6 +359,9 @@ impl FileDropHandler { return E_ABORT; }; + let actions = drop_handler.runner.current_drag_actions(data_transfer_id); + let source_allowed = unsafe { *pdwEffect }; + let mut pt = POINT { x: pt.x, y: pt.y }; unsafe { ScreenToClient(drop_handler.window, &mut pt); @@ -396,7 +369,7 @@ impl FileDropHandler { let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); (drop_handler.send_event)(WindowEvent::DragPosition { id: data_transfer_id, position }); unsafe { - *pdwEffect = drop_handler.cursor_effect(); + *pdwEffect = pick_effect(actions, grfKeyState, source_allowed); } S_OK @@ -417,7 +390,7 @@ impl FileDropHandler { unsafe extern "system" fn Drop( this: *mut IDropTarget, _pDataObj: *const IDataObject, - _grfKeyState: u32, + grfKeyState: u32, pt: POINTL, pdwEffect: *mut u32, ) -> HRESULT { @@ -430,6 +403,9 @@ impl FileDropHandler { return E_ABORT; }; + let actions = drop_handler.runner.current_drag_actions(data_transfer_id); + let source_allowed = unsafe { *pdwEffect }; + let mut pt = POINT { x: pt.x, y: pt.y }; unsafe { ScreenToClient(drop_handler.window, &mut pt); @@ -439,7 +415,7 @@ impl FileDropHandler { (drop_handler.send_event)(WindowEvent::DragPosition { id: data_transfer_id, position }); (drop_handler.send_event)(WindowEvent::DragDropped { id: data_transfer_id }); unsafe { - *pdwEffect = drop_handler.cursor_effect(); + *pdwEffect = pick_effect(actions, grfKeyState, source_allowed); } // The application has had a chance to read the data while handling `DragDropped`; the @@ -473,3 +449,43 @@ static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { DragLeave: FileDropHandler::DragLeave, Drop: FileDropHandler::Drop, }; + +// Intersect the app's valid actions with the source's allowed effects, honoring Ctrl/Shift. +fn pick_effect(actions: DndActions, key_state: u32, source_allowed: u32) -> u32 { + const MK_SHIFT: u32 = 0x0004; + const MK_CONTROL: u32 = 0x0008; + + let mut allowed = 0u32; + if actions.copy() && (source_allowed & DROPEFFECT_COPY) != 0 { + allowed |= DROPEFFECT_COPY; + } + if actions.move_() && (source_allowed & DROPEFFECT_MOVE) != 0 { + allowed |= DROPEFFECT_MOVE; + } + if actions.link() && (source_allowed & DROPEFFECT_LINK) != 0 { + allowed |= DROPEFFECT_LINK; + } + if allowed == 0 { + return DROPEFFECT_NONE; + } + + let ctrl = key_state & MK_CONTROL != 0; + let shift = key_state & MK_SHIFT != 0; + if ctrl && shift && (allowed & DROPEFFECT_LINK) != 0 { + return DROPEFFECT_LINK; + } + if ctrl && !shift && (allowed & DROPEFFECT_COPY) != 0 { + return DROPEFFECT_COPY; + } + if !ctrl && shift && (allowed & DROPEFFECT_MOVE) != 0 { + return DROPEFFECT_MOVE; + } + + if (allowed & DROPEFFECT_COPY) != 0 { + DROPEFFECT_COPY + } else if (allowed & DROPEFFECT_MOVE) != 0 { + DROPEFFECT_MOVE + } else { + DROPEFFECT_LINK + } +} diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index f1827aca06..9ef96d17d2 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -71,9 +71,9 @@ use winit_core::event::{ }; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider, - OwnedDisplayHandle as CoreOwnedDisplayHandle, + OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, }; use winit_core::keyboard::ModifiersState; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; @@ -500,6 +500,19 @@ impl RootActiveEventLoop for ActiveEventLoop { Ok(Box::new(WinDataTransfer::new(data))) } + + fn set_valid_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> Result<(), UnknownDataTransfer> { + let mut state = self.0.drag_state.borrow_mut(); + let Some(state) = state.as_mut().filter(|s| s.id == id) else { + return Err(UnknownDataTransfer(id)); + }; + state.actions = actions.hint(); + Ok(()) + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { diff --git a/winit-win32/src/event_loop/runner.rs b/winit-win32/src/event_loop/runner.rs index cc4465060f..29e3bd2246 100644 --- a/winit-win32/src/event_loop/runner.rs +++ b/winit-win32/src/event_loop/runner.rs @@ -1,6 +1,6 @@ use std::any::Any; use std::cell::{Cell, RefCell}; -use std::collections::{HashMap, VecDeque}; +use std::collections::VecDeque; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -11,7 +11,7 @@ use windows_sys::Win32::Foundation::HWND; use winit_core::application::ApplicationHandler; use winit_core::data_transfer::DataTransferId; use winit_core::event::{DeviceEvent, DeviceId, StartCause, SurfaceSizeWriter, WindowEvent}; -use winit_core::event_loop::ActiveEventLoop as RootActiveEventLoop; +use winit_core::event_loop::{ActiveEventLoop as RootActiveEventLoop, DndActions}; use winit_core::window::WindowId; use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor}; @@ -21,6 +21,14 @@ use crate::util::get_window_long; type EventHandler = Cell>; +/// State for the single drag-and-drop transfer currently in flight (OLE guarantees at most one +/// active drag per process). +pub(super) struct DragState { + pub(super) id: DataTransferId, + data: Rc, + pub(super) actions: DndActions, +} + pub(crate) struct EventLoopRunner { pub(super) thread_id: u32, @@ -39,10 +47,9 @@ pub(crate) struct EventLoopRunner { event_handler: Rc, event_buffer: RefCell>, - /// Drag-and-drop data, cached for the lifetime of each transfer (i.e. between `DragEntered` - /// and `DragLeft`/`DragDropped`). Looked up by [`Self::data_transfer`] to serve the - /// asynchronous `ActiveEventLoop` data-transfer API. - data_transfers: RefCell>>, + /// The currently in-flight drag transfer, if any, alive between `DragEntered` and + /// `DragLeft`/`DragDropped`. + pub(super) drag_state: RefCell>, panic_error: Cell>, } @@ -94,20 +101,34 @@ impl EventLoopRunner { last_events_cleared: Cell::new(Instant::now()), event_handler: Rc::new(Cell::new(None)), event_buffer: RefCell::new(VecDeque::new()), - data_transfers: RefCell::new(HashMap::new()), + drag_state: RefCell::new(None), } } pub(crate) fn register_data_transfer(&self, id: DataTransferId, data: Rc) { - self.data_transfers.borrow_mut().insert(id, data); + *self.drag_state.borrow_mut() = + Some(DragState { id, data, actions: DndActions::none() }); } pub(crate) fn remove_data_transfer(&self, id: DataTransferId) { - self.data_transfers.borrow_mut().remove(&id); + let mut state = self.drag_state.borrow_mut(); + if state.as_ref().is_some_and(|s| s.id == id) { + *state = None; + } } pub(crate) fn data_transfer(&self, id: DataTransferId) -> Option> { - self.data_transfers.borrow().get(&id).cloned() + let state = self.drag_state.borrow(); + state.as_ref().filter(|s| s.id == id).map(|s| s.data.clone()) + } + + pub(crate) fn current_drag_actions(&self, id: DataTransferId) -> DndActions { + self.drag_state + .borrow() + .as_ref() + .filter(|s| s.id == id) + .map(|s| s.actions) + .unwrap_or(DndActions::none()) } /// Associate the application's event handler with the runner. @@ -158,14 +179,14 @@ impl EventLoopRunner { last_events_cleared: _, event_handler, event_buffer: _, - data_transfers, + drag_state, } = self; interrupt_msg_dispatch.set(false); runner_state.set(RunnerState::Uninitialized); panic_error.set(None); exit.set(None); event_handler.set(None); - data_transfers.borrow_mut().clear(); + *drag_state.borrow_mut() = None; } } diff --git a/winit-win32/src/lib.rs b/winit-win32/src/lib.rs index 3c946b64a8..4693875b43 100644 --- a/winit-win32/src/lib.rs +++ b/winit-win32/src/lib.rs @@ -39,7 +39,6 @@ use self::icon::{RaiiIcon, SelectedCursor}; pub use self::keyboard::{physicalkey_to_scancode, scancode_to_physicalkey}; pub use self::monitor::{MonitorHandle, VideoModeHandle}; pub use self::window::Window; -use crate::dnd::FileDropDataShared; /// Window Handle type used by Win32 API pub type HWND = *mut c_void; @@ -464,7 +463,7 @@ pub struct WindowAttributesWindows { pub(crate) menu: Option, pub(crate) taskbar_icon: Option, pub(crate) no_redirection_bitmap: bool, - pub(crate) drag_and_drop: Option>, + pub(crate) drag_and_drop: bool, pub(crate) skip_taskbar: bool, pub(crate) class_name: String, pub(crate) decoration_shadow: bool, @@ -484,7 +483,7 @@ impl Default for WindowAttributesWindows { menu: None, taskbar_icon: None, no_redirection_bitmap: false, - drag_and_drop: Some(Default::default()), + drag_and_drop: true, skip_taskbar: false, class_name: "Window Class".to_string(), decoration_shadow: false, @@ -557,7 +556,7 @@ impl WindowAttributesWindows { /// does that, but there may be more in the future. If you need COM API with /// `COINIT_MULTITHREADED` you must initialize it before calling any winit functions. See for more information. pub fn with_drag_and_drop(mut self, flag: bool) -> Self { - self.drag_and_drop = flag.then(Default::default); + self.drag_and_drop = flag; self } diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index d2ffc6c73a..ee899601db 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -1157,8 +1157,6 @@ impl CoreWindow for Window { fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } - - // TODO: `set_valid_actions` } pub(super) struct InitData<'a> { @@ -1216,7 +1214,7 @@ impl InitData<'_> { } unsafe fn create_window_data(&self, win: &Window) -> event_loop::WindowData { - let file_drop_handler = if let Some(shared) = self.win_attributes.drag_and_drop.clone() { + let file_drop_handler = if self.win_attributes.drag_and_drop { let ole_init_result = unsafe { OleInitialize(ptr::null_mut()) }; // It is ok if the initialize result is `S_FALSE` because it might happen that // multiple windows are created on the same thread. @@ -1235,7 +1233,6 @@ impl InitData<'_> { let mut file_drop_handler = FileDropHandler::new( win.window.hwnd(), self.runner.clone(), - shared, Box::new(move |event| { file_drop_runner.send_event(Event::Window { window_id, event }) }), diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index a09e027072..eefb654b03 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, MutexGuard}; +use std::sync::MutexGuard; use std::{fmt, io, ptr}; use bitflags::bitflags; @@ -22,7 +22,6 @@ use winit_core::keyboard::ModifiersState; use winit_core::monitor::Fullscreen; use winit_core::window::{ImeCapabilities, Theme, WindowAttributes}; -use crate::dnd::FileDropDataShared; use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util}; /// Contains information about states and the window that the callback is going to use. @@ -64,8 +63,6 @@ pub(crate) struct WindowState { pub dragging: bool, - pub drop_data_shared: Option>, - pub skip_taskbar: bool, pub use_system_wheel_speed: bool, @@ -157,7 +154,7 @@ pub enum ImeState { impl WindowState { pub(crate) fn new( attributes: &WindowAttributes, - win_attributes: &WindowAttributesWindows, + _win_attributes: &WindowAttributesWindows, scale_factor: f64, current_theme: Theme, preferred_theme: Option, @@ -198,8 +195,6 @@ impl WindowState { dragging: false, - drop_data_shared: win_attributes.drag_and_drop.clone(), - skip_taskbar: false, use_system_wheel_speed: true, From eb7792c1947cea55e51a786aabe176d1573aa7de Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 16:27:32 +0200 Subject: [PATCH 58/87] Update win32 for new set_valid_actions signature --- winit-win32/src/event_loop.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 9ef96d17d2..a3a4ac6d5e 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -73,7 +73,7 @@ use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider, - OwnedDisplayHandle as CoreOwnedDisplayHandle, UnknownDataTransfer, + OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::keyboard::ModifiersState; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; @@ -505,10 +505,10 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, id: DataTransferId, actions: &dyn DndActionMask, - ) -> Result<(), UnknownDataTransfer> { + ) -> Result<(), RequestError> { let mut state = self.0.drag_state.borrow_mut(); let Some(state) = state.as_mut().filter(|s| s.id == id) else { - return Err(UnknownDataTransfer(id)); + return Err(os_error!(UnknownDataTransfer(id)).into()); }; state.actions = actions.hint(); Ok(()) @@ -522,6 +522,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop { } } +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + #[derive(Clone)] pub(crate) struct OwnedDisplayHandle; From ed9e71d1cae95dff29a2e76a42e5af4ecdc7812a Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 17:00:33 +0200 Subject: [PATCH 59/87] Fix nitpicks --- winit-win32/src/definitions.rs | 7 ------- winit-win32/src/dnd.rs | 3 ++- winit-win32/src/event_loop.rs | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index 4b7fede5eb..c5225cfee9 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -8,12 +8,9 @@ use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM}; use windows_sys::core::{BOOL, GUID, HRESULT}; pub type IUnknown = *mut c_void; -#[allow(dead_code, reason = "part of the IDataObject vtable ABI; not called by winit")] pub type IAdviseSink = *mut c_void; pub type IDataObject = *mut c_void; -#[allow(dead_code, reason = "part of the IDataObject vtable ABI; not called by winit")] pub type IEnumFORMATETC = *mut c_void; -#[allow(dead_code, reason = "part of the IDataObject vtable ABI; not called by winit")] pub type IEnumSTATDATA = *mut c_void; #[repr(C)] @@ -27,10 +24,6 @@ pub struct IUnknownVtbl { pub Release: unsafe extern "system" fn(This: *mut IUnknown) -> u32, } -#[allow( - dead_code, - reason = "the full vtable layout is required for ABI; not all methods are called" -)] #[repr(C)] pub struct IDataObjectVtbl { pub parent: IUnknownVtbl, diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 14c979805d..f6158eb42d 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -19,8 +19,8 @@ use windows_sys::Win32::System::Ole::{ use windows_sys::Win32::UI::Shell::{DragQueryFileW, HDROP}; use windows_sys::core::{GUID, HRESULT}; use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; -use winit_core::event_loop::DndActions; use winit_core::event::WindowEvent; +use winit_core::event_loop::DndActions; use crate::definitions::{ IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl, @@ -224,6 +224,7 @@ impl TypedData for WinTypedData { DataKind::String(string) => { Some(Box::new(io::Cursor::new(string.clone().into_bytes()))) }, + // Windows URI drag-and-drop can't be neatly expressed as a binary blob. DataKind::Uris(_) => None, } } diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index a3a4ac6d5e..bff0ba3cec 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -486,7 +486,7 @@ impl RootActiveEventLoop for ActiveEventLoop { type_: &dyn TransferType, ) -> Result, RequestError> { let Some(data) = self.0.data_transfer(id) else { - return Err(RequestError::Ignored); + return Err(os_error!(UnknownDataTransfer(id)).into()); }; let hint = type_.hint().ok_or(RequestError::Ignored)?; @@ -495,7 +495,7 @@ impl RootActiveEventLoop for ActiveEventLoop { fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { let Some(data) = self.0.data_transfer(id) else { - return Err(RequestError::Ignored); + return Err(os_error!(UnknownDataTransfer(id)).into()); }; Ok(Box::new(WinDataTransfer::new(data))) From e99836bcb85a4ace5371a3d6708ab8317136c1d5 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 17:02:32 +0200 Subject: [PATCH 60/87] Fix invalid panic over FFI boundary --- winit-win32/src/dnd.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index f6158eb42d..7c8ff55c61 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -7,7 +7,7 @@ use std::rc::Rc; use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; use dpi::PhysicalPosition; -use windows_sys::Win32::Foundation::{E_ABORT, HGLOBAL, HWND, POINT, POINTL, S_OK}; +use windows_sys::Win32::Foundation::{E_ABORT, E_FAIL, HGLOBAL, HWND, POINT, POINTL, S_OK}; use windows_sys::Win32::Graphics::Gdi::ScreenToClient; use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_HGLOBAL}; use windows_sys::Win32::System::DataExchange::RegisterClipboardFormatW; @@ -288,7 +288,9 @@ impl FileDropHandler { ) -> HRESULT { // This function doesn't appear to be required for an `IDropTarget`. // An implementation would be nice however. - unimplemented!(); + // Can't use `unimplemented` here as it's invalid to panic over an FFI boundary. + tracing::warn!("`QueryInterface` called, but it was unimplemented"); + E_FAIL } unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { From 41e1f5f1b167f5ea23ee509fd7e364f396cece1f Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 17:07:31 +0200 Subject: [PATCH 61/87] Fmt --- winit-core/src/data_transfer.rs | 22 +++++++------ winit-core/src/event.rs | 48 ++++++++++++++-------------- winit-core/src/event_loop/mod.rs | 14 ++++---- winit-wayland/src/dnd.rs | 5 +-- winit-wayland/src/lib.rs | 3 +- winit-win32/src/event_loop/runner.rs | 3 +- winit-win32/src/window.rs | 3 +- winit-x11/src/event_loop.rs | 19 +++++------ winit/examples/dnd.rs | 5 +-- 9 files changed, 62 insertions(+), 60 deletions(-) diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 1fbd143e91..57e76c994d 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -46,10 +46,9 @@ #![warn(missing_docs)] use std::ffi::OsString; -use std::fmt; -use std::io; use std::marker::PhantomData; use std::ops::ControlFlow; +use std::{fmt, io}; use crate::as_any::AsAny; @@ -285,16 +284,18 @@ impl From> for SendData { /// Trait for sending data via a data transfer. /// /// See [`StartDrag`](crate::event_loop::StartDrag) for where this is used. To build an -/// implementation of this trait dynamically in a cross-platform way, use [`DataTransferSendBuilder`]. +/// implementation of this trait dynamically in a cross-platform way, use +/// [`DataTransferSendBuilder`]. pub trait DataTransferSend: DataTransfer { - /// Get the data for the specified type, or `None` if this value does not supply the given data type. + /// Get the data for the specified type, or `None` if this value does not supply the given data + /// type. fn data_for_type(&mut self, type_: &dyn TransferType) -> Option; /// If `true`, this data transfer is only valid for the application sending the data. /// - /// This is useful on Wayland and macOS, which allow expressing internal drag-and-drop in the API. - /// On platforms which make no distinction between internal and external drag-and-drop, this is - /// ignored. + /// This is useful on Wayland and macOS, which allow expressing internal drag-and-drop in the + /// API. On platforms which make no distinction between internal and external drag-and-drop, + /// this is ignored. fn is_internal_only(&self) -> bool; } @@ -309,7 +310,8 @@ type SendDataCallback = Box Option DataTransferSendBuilder { /// /// - The OS may have multiple types which are equivalent to the supplied type /// - `TypeHint::Audio` and `TypeHint::Image` with `extension_hint: None` will advertise all - /// supported audio and image formats, in which case the closure may receive a type with - /// an extension chosen by the receiving application. + /// supported audio and image formats, in which case the closure may receive a type with an + /// extension chosen by the receiving application. pub fn with_type(mut self, type_: Ty, func: F) -> Self where Ty: TransferType, diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index b2365d7bd4..e1bbafa461 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -1665,24 +1665,24 @@ mod tests { const TILT_TO_ANGLE: &[(TabletToolTilt, TabletToolAngle)] = &[ (TabletToolTilt { x: 0, y: 0 }, TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }), (TabletToolTilt { x: 0, y: 90 }, TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }), - ( - TabletToolTilt { x: 0, y: -90 }, - TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, - ), + (TabletToolTilt { x: 0, y: -90 }, TabletToolAngle { + altitude: 0., + azimuth: 3. * FRAC_PI_2, + }), (TabletToolTilt { x: 90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: 90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: PI }), (TabletToolTilt { x: -90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), (TabletToolTilt { x: -90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }), - ( - TabletToolTilt { x: 0, y: 45 }, - TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, - ), - ( - TabletToolTilt { x: 0, y: -45 }, - TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, - ), + (TabletToolTilt { x: 0, y: 45 }, TabletToolAngle { + altitude: FRAC_PI_4, + azimuth: FRAC_PI_2, + }), + (TabletToolTilt { x: 0, y: -45 }, TabletToolAngle { + altitude: FRAC_PI_4, + azimuth: 3. * FRAC_PI_2, + }), (TabletToolTilt { x: 45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }), (TabletToolTilt { x: -45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }), ]; @@ -1697,20 +1697,20 @@ mod tests { (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }, TabletToolTilt { x: 45, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }, TabletToolTilt { x: 0, y: 0 }), (TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }, TabletToolTilt { x: 0, y: 90 }), - ( - TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, - TabletToolTilt { x: 0, y: 45 }, - ), + (TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, TabletToolTilt { + x: 0, + y: 45, + }), (TabletToolAngle { altitude: 0., azimuth: PI }, TabletToolTilt { x: -90, y: 0 }), (TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }, TabletToolTilt { x: -45, y: 0 }), - ( - TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, - TabletToolTilt { x: 0, y: -90 }, - ), - ( - TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, - TabletToolTilt { x: 0, y: -45 }, - ), + (TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { + x: 0, + y: -90, + }), + (TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, TabletToolTilt { + x: 0, + y: -45, + }), ]; for (angle, tilt) in ANGLE_TO_TILT { diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index 524863729b..dc38736a7b 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -177,9 +177,9 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { /// [`DndActions`](crate::data_transfer::DndActions). /// - `icon` - icon to show while dragging. /// - /// Some platforms have a more-expressive way of setting the visual component of a drag operation. For - /// those platforms, consider using the platform-specific implementation of [`DataTransferSend`] for - /// `send_data` and set this field to `None`. + /// Some platforms have a more-expressive way of setting the visual component of a drag + /// operation. For those platforms, consider using the platform-specific implementation of + /// [`DataTransferSend`] for `send_data` and set this field to `None`. fn start_drag( &self, source: WindowId, @@ -198,8 +198,8 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Cancel a drag-and-drop operation. /// - /// This can be called on data transfers initiated by this application, as well as data transfers - /// received from an external application. + /// This can be called on data transfers initiated by this application, as well as data + /// transfers received from an external application. fn cancel_drag(&self, id: DataTransferId) -> Result<(), RequestError> { let _ = id; Err(RequestError::NotSupported(NotSupportedError::new( @@ -209,8 +209,8 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { } const DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE: &str = { - "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on \ - this platform" + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on this \ + platform" }; impl HasDisplayHandle for dyn ActiveEventLoop + '_ { diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index 229ee9dc33..66811b9f5b 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -76,7 +76,8 @@ impl DataSourceHandler for WinitState { return; } - // TODO: Is there something better we can do than unconditionally encoding as `text/uri-list`? + // TODO: Is there something better we can do than unconditionally encoding as + // `text/uri-list`? for os_str in iter { // TODO: Is `as_encoded_bytes` correct here? if fd @@ -351,8 +352,8 @@ impl MimeData { } fn try_as_file(&mut self) -> Option { + // TODO: Is it ok that this may only work once, depending on what the fd points to? let fd_clone = - // TODO: Is it ok that this may only work once, depending on what the fd points to? if let Ok(cloned) = self.fd.as_ref()?.try_clone() { cloned } else { self.fd.take()? }; Some(fd_clone.into()) } diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 33191ab4bd..78d1068ee4 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -156,7 +156,8 @@ fn make_wid(surface: &WlSurface) -> WindowId { /// Create a `DataTransferId` for the given data device and serial. /// -/// It's currently unclear if this will result in the same ID when transferring to the same application. +/// It's currently unclear if this will result in the same ID when transferring to the same +/// application. #[inline] fn make_data_transfer_id(data_device: &WlDataDevice, serial: u32) -> DataTransferId { const BUILD_HASHER: foldhash::fast::FixedState = foldhash::fast::FixedState::with_seed(0); diff --git a/winit-win32/src/event_loop/runner.rs b/winit-win32/src/event_loop/runner.rs index 29e3bd2246..2fc7dbd8d1 100644 --- a/winit-win32/src/event_loop/runner.rs +++ b/winit-win32/src/event_loop/runner.rs @@ -106,8 +106,7 @@ impl EventLoopRunner { } pub(crate) fn register_data_transfer(&self, id: DataTransferId, data: Rc) { - *self.drag_state.borrow_mut() = - Some(DragState { id, data, actions: DndActions::none() }); + *self.drag_state.borrow_mut() = Some(DragState { id, data, actions: DndActions::none() }); } pub(crate) fn remove_data_transfer(&self, id: DataTransferId) { diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index ee899601db..34e6f83a3e 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -53,7 +53,8 @@ use winit_core::icon::{Icon, RgbaIcon}; use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider}; use winit_core::window::{ CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, - UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, }; use crate::dark_mode::try_theme; diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index 1d7a9649c1..2b15a556e9 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -1144,18 +1144,15 @@ impl Device { let ty = unsafe { (*class_ptr)._type }; if ty == ffi::XIScrollClass { let info = unsafe { &*(class_ptr as *const ffi::XIScrollClassInfo) }; - scroll_axes.push(( - info.number, - ScrollAxis { - increment: info.increment, - orientation: match info.scroll_type { - ffi::XIScrollTypeHorizontal => ScrollOrientation::Horizontal, - ffi::XIScrollTypeVertical => ScrollOrientation::Vertical, - _ => unreachable!(), - }, - position: 0.0, + scroll_axes.push((info.number, ScrollAxis { + increment: info.increment, + orientation: match info.scroll_type { + ffi::XIScrollTypeHorizontal => ScrollOrientation::Horizontal, + ffi::XIScrollTypeVertical => ScrollOrientation::Vertical, + _ => unreachable!(), }, - )); + position: 0.0, + })); } else if ty == ffi::XITouchClass { r#type = Some(DeviceType::Touch); } else if r#type.is_none() && ty == ffi::XIValuatorClass { diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index e7142fc109..8937c98ff2 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -93,8 +93,9 @@ impl ApplicationHandler for Application { Some("Winit example".to_string()) }) // You can advertise a `TypeHint` that can match many types, and switch - // inside the callback. For example, this will match any image type. This - // may be desirable on some platforms which restrict the set of image types + // inside the callback. For example, this will match any image type. + // This may be desirable on some platforms + // which restrict the set of image types // that can be sent. .with_type(TypeHint::Image { extension_hint: None }, |(), ty| { let hint = ty.hint()?; From b5375a9d49c04f582abe8c18a128639cd770ec5d Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 2 Jun 2026 17:39:33 +0200 Subject: [PATCH 62/87] WIP: Initiate drag on macOS --- winit-appkit/Cargo.toml | 2 ++ winit-appkit/src/app_state.rs | 21 +++++++++++ winit-appkit/src/event_loop.rs | 56 ++++++++++++++++++++++++----- winit-appkit/src/window.rs | 1 + winit-wayland/src/dnd.rs | 6 ++-- winit-wayland/src/event_loop/mod.rs | 10 ++++++ winit/examples/dnd.rs | 15 ++++---- 7 files changed, 95 insertions(+), 16 deletions(-) diff --git a/winit-appkit/Cargo.toml b/winit-appkit/Cargo.toml index 4172771f3b..54b366a29d 100644 --- a/winit-appkit/Cargo.toml +++ b/winit-appkit/Cargo.toml @@ -36,6 +36,8 @@ objc2-app-kit = { workspace = true, features = [ "NSControl", "NSCursor", "NSDragging", + "NSDraggingItem", + "NSDraggingSession", "NSEvent", "NSGraphics", "NSGraphicsContext", diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index bd48fc9418..a0c7fb90f8 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -1,4 +1,5 @@ use std::cell::{Cell, OnceCell, RefCell}; +use std::collections::HashMap; use std::mem; use std::rc::Rc; use std::sync::Arc; @@ -6,6 +7,7 @@ use std::time::Instant; use dispatch2::MainThreadBound; use objc2::MainThreadMarker; +use objc2::rc::{Retained, Weak}; use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication}; use objc2_foundation::NSNotification; use winit_common::core_foundation::{EventLoopProxy, MainRunLoop}; @@ -20,6 +22,7 @@ use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immedi use super::menu; use super::observer::EventLoopWaker; use crate::dnd::{DragOperation, Pasteboards}; +use crate::window_delegate::WindowDelegate; #[derive(Debug)] pub(super) struct AppState { @@ -47,6 +50,7 @@ pub(super) struct AppState { start_time: Cell>, wait_timeout: Cell>, pending_redraw: RefCell>, + windows: RefCell>>>, // NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and // as such should be careful to not add fields that, in turn, strongly reference those. } @@ -94,6 +98,7 @@ impl AppState { waker: RefCell::new(EventLoopWaker::new()), start_time: Cell::new(None), wait_timeout: Cell::new(None), + windows: Default::default(), pending_redraw: RefCell::new(vec![]), }); @@ -108,6 +113,22 @@ impl AppState { .clone() } + pub fn with_window_delegate_on_main(&self, id: WindowId, func: F) -> Option + where + F: FnOnce(Retained) -> R + Send, + R: Send, + { + self.windows.borrow_mut().get(&id)?.get_on_main(move |delegate| { + if let Some(delegate) = delegate.load() { Some(func(delegate)) } else { None } + }) + } + + pub fn new_window(&self, window: &Retained, mtm: MainThreadMarker) { + let id = window.id(); + let window_downgraded = Weak::from_retained(window); + self.windows.borrow_mut().insert(id, MainThreadBound::new(window_downgraded, mtm)); + } + // NOTE: This notification will, globally, only be emitted once, // no matter how many `EventLoop`s the user creates. pub fn did_finish_launching(self: &Rc, _notification: &NSNotification) { diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 8108bcea80..59b0bd6a31 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::rc::Rc; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -7,7 +8,7 @@ use objc2::runtime::ProtocolObject; use objc2::{MainThreadMarker, available}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification, - NSApplicationWillTerminateNotification, NSWindow, + NSApplicationWillTerminateNotification, NSView, NSWindow, }; use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes}; use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; @@ -17,16 +18,17 @@ use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_ob use winit_common::foundation::create_observer; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData, +}; use winit_core::error::{EventLoopError, RequestError}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, DragIcon, EventLoopProxy as CoreEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle, - UnknownDataTransfer, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; -use winit_core::window::Theme; +use winit_core::window::{Theme, WindowId}; use super::app::override_send_event; use super::app_state::AppState; @@ -155,13 +157,13 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, id: DataTransferId, actions: &dyn DndActionMask, - ) -> Result<(), UnknownDataTransfer> { + ) -> Result<(), RequestError> { let Some(drag_state) = self.app_state.drag_state().get() else { - return Err(UnknownDataTransfer(id)); + return Err(os_error!(UnknownDataTransfer(id)).into()); }; if drag_state.id != id { - return Err(UnknownDataTransfer(id)); + return Err(os_error!(UnknownDataTransfer(id)).into()); } let new_drag_state = DragState { id, valid_operations: DragOperation::from_dyn(actions) }; @@ -169,8 +171,46 @@ impl RootActiveEventLoop for ActiveEventLoop { Ok(()) } + + fn start_drag( + &self, + source: WindowId, + _send_data: Box, + _action_mask: &dyn DndActionMask, + _icon: Option, + ) -> Result { + self.app_state + .with_window_delegate_on_main(source, |delegate| { + #[expect(unreachable_code)] + delegate + .view() + .downcast::() + .ok() + .unwrap() + .beginDraggingSessionWithItems_event_source(todo!(), todo!(), todo!()); + }) + .ok_or(RequestError::Ignored) + } + + fn cancel_drag(&self, id: DataTransferId) -> Result<(), RequestError> { + let _ = id; + todo!() + } +} + +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } } +impl std::error::Error for UnknownDataTransfer {} + impl rwh_06::HasDisplayHandle for ActiveEventLoop { fn display_handle(&self) -> Result, rwh_06::HandleError> { let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new()); diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index 002a620b29..24370d5196 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -36,6 +36,7 @@ impl Window { let mtm = window_target.mtm; let delegate = autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?; + window_target.app_state.new_window(&delegate, mtm); Ok(Window { window: MainThreadBound::new(delegate.window().retain(), mtm), delegate: MainThreadBound::new(delegate, mtm), diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index 66811b9f5b..2f99029309 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -620,8 +620,10 @@ impl DndState { self.send_drag = Some(source); } - pub(crate) fn clear_send_drag(&mut self) { - self.send_drag = None; + /// Returns `true` if a drag operation was in progress, `false` if no drag operation was in + /// progress. + pub(crate) fn clear_send_drag(&mut self) -> bool { + self.send_drag.take().is_some() } pub(crate) fn send_drag_data_mut(&mut self) -> Option<&mut dyn DataTransferSend> { diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index ec719c7c66..e07eb11a7a 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -868,6 +868,16 @@ impl RootActiveEventLoop for ActiveEventLoop { Ok(transfer_id) } + + fn cancel_drag(&self, id: DataTransferId) -> Result<(), RequestError> { + // Clearing the sent drag will drop the inner `WlDataSource`, which will + // cancel the drag operation. + if self.state.borrow_mut().dnd_state.clear_send_drag() { + Ok(()) + } else { + Err(RequestError::Ignored) + } + } } /// An operation was attempted on a data transfer ID, but that ID was invalid. diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 8937c98ff2..5eb5598920 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -174,12 +174,15 @@ impl ApplicationHandler for Application { None => image::ImageReader::new(reader), }; - if let Ok(image) = reader.decode() { - let width = image.width(); - let height = image.height(); - info!("Received image ({width}x{height})"); - } else { - warn!("Failed to decode jpeg"); + match reader.decode() { + Ok(image) => { + let width = image.width(); + let height = image.height(); + info!("Received image ({width}x{height})"); + }, + Err(err) => { + warn!("Failed to decode image: {err}"); + }, } } }, From 340c2cba66488f1935db1cd57568f7dffb48e2de Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 3 Jun 2026 14:04:59 +0200 Subject: [PATCH 63/87] Initiate drag on macOS --- winit-appkit/src/cursor.rs | 35 ++++++++ winit-appkit/src/dnd.rs | 128 +++++++++++++++++++++++++++- winit-appkit/src/event_loop.rs | 113 +++++++++++++++++++----- winit-appkit/src/view.rs | 36 +++++++- winit-appkit/src/window_delegate.rs | 25 ++++-- winit-core/src/data_transfer.rs | 30 +++---- winit-wayland/src/event_loop/mod.rs | 3 +- winit/examples/dnd.rs | 50 ++++++----- 8 files changed, 349 insertions(+), 71 deletions(-) diff --git a/winit-appkit/src/cursor.rs b/winit-appkit/src/cursor.rs index e6e3337fff..0902371467 100644 --- a/winit-appkit/src/cursor.rs +++ b/winit-appkit/src/cursor.rs @@ -14,6 +14,7 @@ use objc2_foundation::{ }; use winit_core::cursor::{CursorIcon, CursorImage, CustomCursorProvider, CustomCursorSource}; use winit_core::error::{NotSupportedError, RequestError}; +use winit_core::icon::{Icon, RgbaIcon}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct CustomCursor(pub(crate) Retained); @@ -42,6 +43,40 @@ impl CustomCursor { } } +pub(crate) fn image_from_icon(icon: &Icon) -> Result, RequestError> { + let rgba_icon = icon + .cast_ref::() + .ok_or(NotSupportedError::new("Only RGBA icons can be converted to `NSImage`"))?; + + let width = rgba_icon.width(); + let height = rgba_icon.height(); + + let bitmap = unsafe { + NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel( + NSBitmapImageRep::alloc(), + std::ptr::null_mut::<*mut c_uchar>(), + width as isize, + height as isize, + 8, + 4, + true, + false, + NSDeviceRGBColorSpace, + width as isize * 4, + 32, + ) + }.ok_or_else(|| os_error!("parent view should be installed in a window"))?; + + let bitmap_data = + unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), rgba_icon.buffer().len()) }; + bitmap_data.copy_from_slice(rgba_icon.buffer()); + + let image = NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into())); + image.addRepresentation(&bitmap); + + Ok(image) +} + pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Result, RequestError> { let width = cursor.width(); let height = cursor.height(); diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 266a54eacf..641d8bcf23 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -2,17 +2,21 @@ use std::cell::{OnceCell, RefCell}; use std::collections::HashMap; use std::ffi::OsString; use std::io; -use std::ops::Deref; +use std::ops::{ControlFlow, Deref}; use std::rc::Rc; -use objc2::Message; use objc2::rc::{Retained, Weak}; +use objc2::runtime::AnyObject; +use objc2::{AnyThread, DefinedClass as _, Message, define_class, msg_send}; use objc2_app_kit::{ NSDragOperation, NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, + NSPasteboardWriting, NSPasteboardWritingOptions, +}; +use objc2_foundation::{NSArray, NSData, NSObject, NSObjectProtocol, NSString}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, }; -use objc2_foundation::{NSArray, NSData, NSString}; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; use winit_core::event_loop::{DndActionMask, DndActions}; /// A thin wrapper around [`NSPasteboardType`], implementing [`TransferType`]. @@ -392,3 +396,119 @@ impl Pasteboards { self.inner.borrow().get(&id).and_then(|weak| weak.load()).map(|pb| Pasteboard::new(id, pb)) } } + +pub(crate) struct PasteboardWriterState { + data: Box, + // The macOS drag-and-drop API has some confusing aspects when handling multi-drag. The best + // we can really do is have the first element contain all the cross-platform items, and + // any further items are file paths only. + uri: Option>, + writeable_types: Retained>, +} + +impl PasteboardWriter { + pub(crate) fn new( + value: Box, + uri: Option>, + ) -> Retained { + let mut writeable_types = Vec::>::new(); + value.for_each_available_type(&mut |type_| { + let Some(spec) = PasteboardTypeSpec::from_dyn(type_) else { + return ControlFlow::Continue(()); + }; + + let Some(pb_type) = spec.pasteboard_type() else { + return ControlFlow::Continue(()); + }; + + writeable_types.push((**pb_type).clone()); + + ControlFlow::Continue(()) + }); + + let pb_writer = Self::alloc().set_ivars(PasteboardWriterState { + data: value, + uri, + writeable_types: NSArray::from_retained_slice(&writeable_types), + }); + + // Unsure if there's an easier way to do this, but this is how `WindowDelegate` does it. + unsafe { msg_send![super(pb_writer), init] } + } +} + +impl PasteboardWriterState { + fn data_for_pasteboard_type( + &self, + pasteboard_type: &NSPasteboardType, + ) -> Option> { + if pasteboard_type == unsafe { NSPasteboardTypeFileURL } { + if let Some(out) = self.uri.clone().map(Into::into) { + return Some(out); + } + } + let pb_type = PasteboardType::from(pasteboard_type.retain()); + + let mut out = None; + + self.data.for_each_available_type(&mut |haystack| { + if haystack.matches(&pb_type) { + out = self.data.data_for_type(haystack); + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }); + + match out? { + // This should be handled separately + // TODO: Is there a better way to do this? + SendData::Uris(_) => None, + SendData::String(string) => Some(NSString::from_str(&string).into()), + SendData::Bytes(binary) => Some(NSData::from_vec(binary).into()), + } + } +} + +define_class!( + #[unsafe(super(NSObject))] + #[thread_kind = AnyThread] + #[name = "WinitPasteboardWriter"] + #[ivars = PasteboardWriterState] + pub(crate) struct PasteboardWriter; + + unsafe impl NSObjectProtocol for PasteboardWriter {} + + unsafe impl NSPasteboardWriting for PasteboardWriter { + #[unsafe(method_id(writableTypesForPasteboard:))] + fn writable_types_for_pasteboard( + &self, + pasteboard: &NSPasteboard, + ) -> Retained> { + let vars = self.ivars(); + vars.writeable_types.clone() + } + + #[unsafe(method(writingOptionsForType:pasteboard:))] + fn writing_options_for_type( + &self, + type_: &NSPasteboardType, + pasteboard: &NSPasteboard, + ) -> NSPasteboardWritingOptions { + let _ = type_; + let _ = pasteboard; + // TODO: Not necessarily ideal to always use `Promised`, but + // it's good enough for now. + NSPasteboardWritingOptions::empty() + } + + #[unsafe(method_id(pasteboardPropertyListForType:))] + fn pasteboard_property_list_for_type( + &self, + type_: &NSPasteboardType, + ) -> Option> { + let vars = self.ivars(); + vars.data_for_pasteboard_type(type_) + } + } +); diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 59b0bd6a31..582749759b 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -1,3 +1,4 @@ +use std::ffi::OsString; use std::fmt; use std::rc::Rc; use std::sync::Arc; @@ -5,13 +6,15 @@ use std::time::{Duration, Instant}; use objc2::rc::{Retained, autoreleasepool}; use objc2::runtime::ProtocolObject; -use objc2::{MainThreadMarker, available}; +use objc2::{AnyThread, ClassType, MainThreadMarker, available}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification, - NSApplicationWillTerminateNotification, NSView, NSWindow, + NSApplicationWillTerminateNotification, NSDraggingItem, NSWindow, }; -use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes}; -use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; +use objc2_core_foundation::{ + CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes, +}; +use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString}; use rwh_06::HasDisplayHandle; use tracing::debug_span; use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers}; @@ -19,7 +22,7 @@ use winit_common::foundation::create_observer; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; use winit_core::data_transfer::{ - DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData, + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, }; use winit_core::error::{EventLoopError, RequestError}; use winit_core::event_loop::pump_events::PumpStatus; @@ -37,7 +40,8 @@ use super::event::dummy_event; use super::monitor; use crate::ActivationPolicy; use crate::app_state::DragState; -use crate::dnd::DragOperation; +use crate::cursor::image_from_icon; +use crate::dnd::{DragOperation, PasteboardWriter}; use crate::window::Window; #[derive(Debug)] @@ -175,26 +179,95 @@ impl RootActiveEventLoop for ActiveEventLoop { fn start_drag( &self, source: WindowId, - _send_data: Box, + send_data: Box, _action_mask: &dyn DndActionMask, - _icon: Option, + icon: Option, ) -> Result { self.app_state - .with_window_delegate_on_main(source, |delegate| { - #[expect(unreachable_code)] - delegate - .view() - .downcast::() - .ok() - .unwrap() - .beginDraggingSessionWithItems_event_source(todo!(), todo!(), todo!()); + .with_window_delegate_on_main(source, move |delegate| { + let drag_image = icon.and_then(|icon| image_from_icon(&icon.icon).ok()); + + let mut uris = send_data + .data_for_type(&TypeHint::UriList) + .and_then(|file_uris| { + // TODO: Might not be ideal to do this + let ns_url_from_os_str = + |os_str: OsString| Some(NSString::from_str(&os_str.to_str()?)); + // Slightly complicated use of iterators in order to ensure that branches + // have the same opaque type + match file_uris { + SendData::Uris(os_strings) => Some( + None.into_iter() + .chain(os_strings.into_iter().filter_map(ns_url_from_os_str)), + ), + SendData::String(string) => Some( + Some(NSString::from_str(&string)) + .into_iter() + .chain(Vec::new().into_iter().filter_map(ns_url_from_os_str)), + ), + SendData::Bytes(_) => None, + } + }) + .into_iter() + .flatten(); + + let first_uri = uris.next(); + + let mut pasteboard_items = uris + .map(|ns_url| { + let dragging_item = NSDraggingItem::initWithPasteboardWriter( + NSDraggingItem::alloc(), + ProtocolObject::from_ref(&*ns_url), + ); + + unsafe { + dragging_item.setDraggingFrame_contents( + CGRect::new(CGPoint::ZERO, CGSize::new(16., 16.)), + drag_image.as_ref().map(AsRef::as_ref), + ) + }; + + dragging_item + }) + .collect::>(); + + let first_dragging_item = NSDraggingItem::initWithPasteboardWriter( + NSDraggingItem::alloc(), + ProtocolObject::from_ref(&*PasteboardWriter::new(send_data, first_uri)), + ); + + unsafe { + first_dragging_item.setDraggingFrame_contents( + CGRect::new(CGPoint::ZERO, CGSize::new(16., 16.)), + drag_image.as_ref().map(AsRef::as_ref), + ) + }; + + pasteboard_items.insert(0, first_dragging_item); + + let pasteboard_items = NSArray::from_retained_slice(&pasteboard_items); + + let view = delegate.view(); + let Some(event) = view.latest_event() else { + return Err(RequestError::Ignored); + }; + let session = view.as_super().beginDraggingSessionWithItems_event_source( + &pasteboard_items, + &event, + ProtocolObject::from_ref(&*delegate), + ); + + let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64); + + view.set_dragging_session(session); + + Ok(id) }) - .ok_or(RequestError::Ignored) + .ok_or(RequestError::Ignored)? } - fn cancel_drag(&self, id: DataTransferId) -> Result<(), RequestError> { - let _ = id; - todo!() + fn cancel_drag(&self, _id: DataTransferId) -> Result<(), RequestError> { + Ok(()) } } diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 818e460277..47972bf6f1 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -6,10 +6,10 @@ use std::rc::Rc; use dpi::{LogicalPosition, LogicalSize}; use objc2::rc::Retained; use objc2::runtime::{AnyObject, Sel}; -use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send}; +use objc2::{AnyThread, DefinedClass, MainThreadMarker, Message, define_class, msg_send}; use objc2_app_kit::{ - NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea, - NSTrackingAreaOptions, NSView, NSWindow, + NSApplication, NSCursor, NSDraggingSession, NSEvent, NSEventPhase, NSResponder, + NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, }; use objc2_core_foundation::CGRect; use objc2_foundation::{ @@ -114,6 +114,13 @@ pub struct ViewState { /// Strong reference to the global application state. app_state: Rc, + /// Initiating a drag requires passing the mouse event that initiated it. + /// Since we don't handle these events synchronously, we need to retain the + /// event internally. + latest_mouse_event: RefCell>>, + /// This is for a dragging session that we initiated + dragging_session: RefCell>>, + cursor_state: RefCell, ime_position: Cell, ime_size: Cell, @@ -780,6 +787,8 @@ impl WinitView { ) -> Retained { let this = mtm.alloc().set_ivars(ViewState { app_state: Rc::clone(app_state), + latest_mouse_event: Default::default(), + dragging_session: Default::default(), cursor_state: Default::default(), ime_position: Default::default(), ime_size: Default::default(), @@ -1073,7 +1082,28 @@ impl WinitView { self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); } + pub(crate) fn set_dragging_session(&self, drag: Retained) { + self.ivars().dragging_session.replace(Some(drag)); + } + + pub(crate) fn clear_dragging_session(&self) -> bool { + self.ivars().dragging_session.replace(None).is_some() + } + + fn update_latest_event(&self, event: &NSEvent) { + self.ivars().latest_mouse_event.replace(Some(event.retain())); + } + + pub(crate) fn latest_event(&self) -> Option> { + self.ivars().latest_mouse_event.borrow().clone() + } + fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { + // Initiating a drag requires us to pass a mouse down event. + if button_state == ElementState::Pressed { + self.update_latest_event(event); + } + let position = self.mouse_view_point(event).to_physical(self.scale_factor()); let button = mouse_button(event); diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index c9c1b46bbe..74a4a0bb17 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -19,12 +19,13 @@ use objc2::{ use objc2_app_kit::{ NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, - NSColor, NSDragOperation, NSDraggingDestination, NSDraggingInfo, NSPasteboardTypeFileURL, - NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, - NSPasteboardTypeTIFF, NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, - NSViewFrameDidChangeNotification, NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, - NSWindowOcclusionState, NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, - NSWindowTabbingMode, NSWindowTitleVisibility, NSWindowToolbarStyle, + NSColor, NSDragOperation, NSDraggingContext, NSDraggingDestination, NSDraggingInfo, + NSDraggingSession, NSDraggingSource, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, + NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, + NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification, + NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, + NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, + NSWindowTitleVisibility, NSWindowToolbarStyle, }; use objc2_core_foundation::{CGFloat, CGPoint}; use objc2_core_graphics::{ @@ -360,6 +361,18 @@ define_class!( } } + unsafe impl NSDraggingSource for WindowDelegate { + #[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))] + fn dragging_session_source_operation_mask( + &self, + _: &NSDraggingSession, + _: NSDraggingContext, + ) -> NSDragOperation { + // TODO: Set this from `start_drag` + NSDragOperation::all() + } + } + unsafe impl NSDraggingDestination for WindowDelegate { /// Invoked when the dragged image enters destination bounds or frame #[unsafe(method(draggingEntered:))] diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs index 57e76c994d..1e0dfbab5d 100644 --- a/winit-core/src/data_transfer.rs +++ b/winit-core/src/data_transfer.rs @@ -286,10 +286,10 @@ impl From> for SendData { /// See [`StartDrag`](crate::event_loop::StartDrag) for where this is used. To build an /// implementation of this trait dynamically in a cross-platform way, use /// [`DataTransferSendBuilder`]. -pub trait DataTransferSend: DataTransfer { +pub trait DataTransferSend: DataTransfer + Send { /// Get the data for the specified type, or `None` if this value does not supply the given data /// type. - fn data_for_type(&mut self, type_: &dyn TransferType) -> Option; + fn data_for_type(&self, type_: &dyn TransferType) -> Option; /// If `true`, this data transfer is only valid for the application sending the data. /// @@ -306,7 +306,7 @@ pub enum InternalTransferMarker {} /// Marker for a [`DataTransferSendBuilder`] which is external. pub enum ExternalTransferMarker {} -type SendDataCallback = Box Option>; +type SendDataCallback = Box Option + Send>; /// Dynamic builder for an implementation of [`DataTransferSend`]. /// @@ -322,7 +322,7 @@ type SendDataCallback = Box Option { state: T, - types: Vec<(Box, SendDataCallback)>, + types: Vec<(Box, SendDataCallback)>, _is_internal: PhantomData, } @@ -338,7 +338,7 @@ where impl DataTransfer for DataTransferSendBuilder where M: 'static, - T: fmt::Debug + 'static, + T: fmt::Debug + Send + 'static, { fn for_each_available_type<'this>( &'this self, @@ -350,9 +350,9 @@ where impl DataTransferSend for DataTransferSendBuilder where - T: fmt::Debug + 'static, + T: fmt::Debug + Send + 'static, { - fn data_for_type(&mut self, type_: &dyn TransferType) -> Option { + fn data_for_type(&self, type_: &dyn TransferType) -> Option { self.data_for_type(type_) } @@ -363,9 +363,9 @@ where impl DataTransferSend for DataTransferSendBuilder where - T: fmt::Debug + 'static, + T: fmt::Debug + Send + 'static, { - fn data_for_type(&mut self, type_: &dyn TransferType) -> Option { + fn data_for_type(&self, type_: &dyn TransferType) -> Option { self.data_for_type(type_) } @@ -391,18 +391,18 @@ impl DataTransferSendBuilder { } impl DataTransferSendBuilder { - fn data_for_type(&mut self, type_: &dyn TransferType) -> Option { + fn data_for_type(&self, type_: &dyn TransferType) -> Option { let (_, func) = self.types.iter().find(|(ty, _)| ty.matches(type_))?; - func(&mut self.state, type_) + func(&self.state, type_) } /// Add a callback which converts the builder's state to the given type. In /// most cases, `type_` will be [`TypeHint`]. pub fn add_type(&mut self, type_: Ty, func: F) -> &mut Self where - Ty: TransferType, - F: Fn(&mut T, &dyn TransferType) -> Option + 'static, + Ty: TransferType + Send, + F: Fn(&T, &dyn TransferType) -> Option + Send + 'static, O: Into, { self.types @@ -422,8 +422,8 @@ impl DataTransferSendBuilder { /// extension chosen by the receiving application. pub fn with_type(mut self, type_: Ty, func: F) -> Self where - Ty: TransferType, - F: Fn(&mut T, &dyn TransferType) -> Option + 'static, + Ty: TransferType + Send, + F: Fn(&T, &dyn TransferType) -> Option + Send + 'static, O: Into, { self.add_type(type_, func); diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index e07eb11a7a..b82cdad65f 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -869,7 +869,8 @@ impl RootActiveEventLoop for ActiveEventLoop { Ok(transfer_id) } - fn cancel_drag(&self, id: DataTransferId) -> Result<(), RequestError> { + fn cancel_drag(&self, _: DataTransferId) -> Result<(), RequestError> { + // TODO: Check the ID and choose between send/receive drag or nothing // Clearing the sent drag will drop the inner `WlDataSource`, which will // cancel the drag operation. if self.state.borrow_mut().dnd_state.clear_send_drag() { diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 5eb5598920..d4c2b3ef07 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -1,8 +1,10 @@ use std::error::Error; use std::ffi::OsString; use std::path::PathBuf; +use std::sync::Arc; use dpi::PhysicalPosition; +use image::RgbImage; use tracing::{error, info, warn}; use winit::application::ApplicationHandler; use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, TypeHint, TypedData}; @@ -32,6 +34,7 @@ struct Application { last_dnd_fetch: Option>, last_drag_start: Option, drag_icon: (Icon, PhysicalPosition), + drag_image_data: Arc, } const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png"); @@ -39,8 +42,14 @@ const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png"); impl Application { fn new() -> Self { let drag_icon = load_icon(DRAG_IMAGE); - - Self { window: None, last_dnd_fetch: None, last_drag_start: None, drag_icon } + let drag_image_data = Arc::new(image::load_from_memory(DRAG_IMAGE).unwrap().into_rgb8()); + Self { + window: None, + last_dnd_fetch: None, + last_drag_start: None, + drag_icon, + drag_image_data, + } } } @@ -83,13 +92,24 @@ impl ApplicationHandler for Application { let (icon, offset) = self.drag_icon.clone(); + // In a real application, you probably wouldn't advertise so many types. + // Depending on platform and destination application, different options may be + // chosen. let result = event_loop.start_drag( window_id, - DataTransferSendBuilder::new(()) - .with_type(TypeHint::Plaintext, |(), _| { + DataTransferSendBuilder::new(self.drag_image_data.clone()) + .with_type(TypeHint::UriList, |_, _| { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root = manifest_dir.parent().unwrap(); + let this_file = root.join(file!()); + let icon_file = this_file.parent().unwrap().join("data/icon.png"); + let icon_file = icon_file.display(); + Some(vec![OsString::from(format!("file://{icon_file}"))]) + }) + .with_type(TypeHint::Plaintext, |_, _| { Some("Winit example".to_string()) }) - .with_type(TypeHint::Html, |(), _| { + .with_type(TypeHint::Html, |_, _| { Some("Winit example".to_string()) }) // You can advertise a `TypeHint` that can match many types, and switch @@ -97,20 +117,14 @@ impl ApplicationHandler for Application { // This may be desirable on some platforms // which restrict the set of image types // that can be sent. - .with_type(TypeHint::Image { extension_hint: None }, |(), ty| { + .with_type(TypeHint::Image { extension_hint: None }, |image, ty| { let hint = ty.hint()?; match hint { - TypeHint::Image { extension_hint: None | Some("png") } => { - info!("Destination requested image as png"); - Some(DRAG_IMAGE.to_vec()) - }, - TypeHint::Image { extension_hint: Some(ext) } => { + TypeHint::Image { extension_hint } => { + let ext = extension_hint.unwrap_or("png"); info!( "Destination requested image as {ext}, converting..." ); - let image = image::load_from_memory(DRAG_IMAGE) - .unwrap() - .into_rgb8(); let format = image::ImageFormat::from_extension(ext)?; let mut out_buf = Vec::new(); let mut out_writer = std::io::Cursor::new(&mut out_buf); @@ -122,14 +136,6 @@ impl ApplicationHandler for Application { _ => None, } }) - .with_type(TypeHint::UriList, |(), _| { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let root = manifest_dir.parent().unwrap(); - let this_file = root.join(file!()); - let icon_file = this_file.parent().unwrap().join("data/icon.png"); - let icon_file = icon_file.display(); - Some(vec![OsString::from(format!("file://{icon_file}"))]) - }) .build(), &DndActions::All, Some(DragIcon { icon, offset }), From 098af43abdbc6403ba13cecc165df93313a62fa5 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 3 Jun 2026 14:08:31 +0200 Subject: [PATCH 64/87] Remove `cancel_drag` This should be handled by the OS and manually cancelling a drag is an advanced operation --- winit-appkit/src/event_loop.rs | 4 ---- winit-core/src/event_loop/mod.rs | 11 ----------- winit-wayland/src/event_loop/mod.rs | 11 ----------- winit/examples/dnd.rs | 4 ---- 4 files changed, 30 deletions(-) diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 582749759b..02eeedbfef 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -265,10 +265,6 @@ impl RootActiveEventLoop for ActiveEventLoop { }) .ok_or(RequestError::Ignored)? } - - fn cancel_drag(&self, _id: DataTransferId) -> Result<(), RequestError> { - Ok(()) - } } /// An operation was attempted on a data transfer ID, but that ID was invalid. diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index dc38736a7b..da5a5cfb38 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -195,17 +195,6 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, ))) } - - /// Cancel a drag-and-drop operation. - /// - /// This can be called on data transfers initiated by this application, as well as data - /// transfers received from an external application. - fn cancel_drag(&self, id: DataTransferId) -> Result<(), RequestError> { - let _ = id; - Err(RequestError::NotSupported(NotSupportedError::new( - DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, - ))) - } } const DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE: &str = { diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index b82cdad65f..ec719c7c66 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -868,17 +868,6 @@ impl RootActiveEventLoop for ActiveEventLoop { Ok(transfer_id) } - - fn cancel_drag(&self, _: DataTransferId) -> Result<(), RequestError> { - // TODO: Check the ID and choose between send/receive drag or nothing - // Clearing the sent drag will drop the inner `WlDataSource`, which will - // cancel the drag operation. - if self.state.borrow_mut().dnd_state.clear_send_drag() { - Ok(()) - } else { - Err(RequestError::Ignored) - } - } } /// An operation was attempted on a data transfer ID, but that ID was invalid. diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index d4c2b3ef07..e9d47deaaa 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -86,10 +86,6 @@ impl ApplicationHandler for Application { }; if button == MouseButton::Left && state.is_pressed() { - if let Some(last_drag) = self.last_drag_start.take() { - let _ = event_loop.cancel_drag(last_drag); - } - let (icon, offset) = self.drag_icon.clone(); // In a real application, you probably wouldn't advertise so many types. From b8e3eba246a2c536b35e25019277f45e90805938 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 3 Jun 2026 16:30:19 +0200 Subject: [PATCH 65/87] Fix buggy drag visuals --- winit-appkit/src/event_loop.rs | 40 +++++++++++++++++++---------- winit-appkit/src/view.rs | 20 +-------------- winit-appkit/src/window_delegate.rs | 18 ++++++++++--- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 02eeedbfef..c47d2e9181 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -4,6 +4,7 @@ use std::rc::Rc; use std::sync::Arc; use std::time::{Duration, Instant}; +use dpi::PhysicalPosition; use objc2::rc::{Retained, autoreleasepool}; use objc2::runtime::ProtocolObject; use objc2::{AnyThread, ClassType, MainThreadMarker, available}; @@ -14,7 +15,7 @@ use objc2_app_kit::{ use objc2_core_foundation::{ CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes, }; -use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString}; +use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSPoint, NSString}; use rwh_06::HasDisplayHandle; use tracing::debug_span; use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers}; @@ -185,8 +186,28 @@ impl RootActiveEventLoop for ActiveEventLoop { ) -> Result { self.app_state .with_window_delegate_on_main(source, move |delegate| { + let dragging_rect_offset = + icon.as_ref().map(|icon| icon.offset).unwrap_or_default(); let drag_image = icon.and_then(|icon| image_from_icon(&icon.icon).ok()); + let Some(event) = dbg!(delegate.window().currentEvent()) else { + return Err(RequestError::Ignored); + }; + + let dragging_rect_size = drag_image + .as_ref() + .map(|img| img.size()) + // Seemingly we need some kind of dragging rectangle even if no icon is + // supplied. + .unwrap_or(CGSize::new(16., 16.)); + + let event_location = event.locationInWindow(); + let dragging_rect_location = CGPoint::new( + event_location.x + dragging_rect_offset.x as f64, + event_location.y + dragging_rect_offset.y as f64, + ); + let dragging_rect = CGRect::new(dragging_rect_location, dragging_rect_size); + let mut uris = send_data .data_for_type(&TypeHint::UriList) .and_then(|file_uris| { @@ -220,12 +241,7 @@ impl RootActiveEventLoop for ActiveEventLoop { ProtocolObject::from_ref(&*ns_url), ); - unsafe { - dragging_item.setDraggingFrame_contents( - CGRect::new(CGPoint::ZERO, CGSize::new(16., 16.)), - drag_image.as_ref().map(AsRef::as_ref), - ) - }; + // No dragging frame/contents, icon only applies to the first item. dragging_item }) @@ -238,7 +254,7 @@ impl RootActiveEventLoop for ActiveEventLoop { unsafe { first_dragging_item.setDraggingFrame_contents( - CGRect::new(CGPoint::ZERO, CGSize::new(16., 16.)), + dragging_rect, drag_image.as_ref().map(AsRef::as_ref), ) }; @@ -247,11 +263,7 @@ impl RootActiveEventLoop for ActiveEventLoop { let pasteboard_items = NSArray::from_retained_slice(&pasteboard_items); - let view = delegate.view(); - let Some(event) = view.latest_event() else { - return Err(RequestError::Ignored); - }; - let session = view.as_super().beginDraggingSessionWithItems_event_source( + let session = delegate.window().beginDraggingSessionWithItems_event_source( &pasteboard_items, &event, ProtocolObject::from_ref(&*delegate), @@ -259,7 +271,7 @@ impl RootActiveEventLoop for ActiveEventLoop { let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64); - view.set_dragging_session(session); + delegate.view().set_dragging_session(session); Ok(id) }) diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 47972bf6f1..208f0df7ec 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -114,10 +114,6 @@ pub struct ViewState { /// Strong reference to the global application state. app_state: Rc, - /// Initiating a drag requires passing the mouse event that initiated it. - /// Since we don't handle these events synchronously, we need to retain the - /// event internally. - latest_mouse_event: RefCell>>, /// This is for a dragging session that we initiated dragging_session: RefCell>>, @@ -787,7 +783,6 @@ impl WinitView { ) -> Retained { let this = mtm.alloc().set_ivars(ViewState { app_state: Rc::clone(app_state), - latest_mouse_event: Default::default(), dragging_session: Default::default(), cursor_state: Default::default(), ime_position: Default::default(), @@ -863,7 +858,7 @@ impl WinitView { }); } - fn scale_factor(&self) -> f64 { + pub(crate) fn scale_factor(&self) -> f64 { self.window().backingScaleFactor() as f64 } @@ -1090,20 +1085,7 @@ impl WinitView { self.ivars().dragging_session.replace(None).is_some() } - fn update_latest_event(&self, event: &NSEvent) { - self.ivars().latest_mouse_event.replace(Some(event.retain())); - } - - pub(crate) fn latest_event(&self) -> Option> { - self.ivars().latest_mouse_event.borrow().clone() - } - fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { - // Initiating a drag requires us to pass a mouse down event. - if button_state == ElementState::Pressed { - self.update_latest_event(event); - } - let position = self.mouse_view_point(event).to_physical(self.scale_factor()); let button = mouse_button(event); diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 74a4a0bb17..f4621ff26a 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -400,7 +400,10 @@ define_class!( position: Some(position), }); - valid_operations + vars.app_state + .drag_state() + .get() + .map_or(NSDragOperation::empty(), |state| state.valid_operations.0) } #[unsafe(method(wantsPeriodicDraggingUpdates))] @@ -434,7 +437,10 @@ define_class!( self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); - valid_operations.0 + vars.app_state + .drag_state() + .get() + .map_or(NSDragOperation::empty(), |state| state.valid_operations.0) } /// Invoked when the image is released @@ -469,7 +475,13 @@ define_class!( self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); self.queue_event(WindowEvent::DragDropped { id: transfer_id }); - sender.draggingSourceOperationMask().intersects(valid_operations.0) + let valid_operations = vars + .app_state + .drag_state() + .get() + .map_or(NSDragOperation::empty(), |state| state.valid_operations.0); + + sender.draggingSourceOperationMask().intersects(valid_operations) } /// Invoked when the dragging operation is complete From ea394ea7aa41bad7a2b808f735349ae87d5a8157 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 3 Jun 2026 16:49:06 +0200 Subject: [PATCH 66/87] Clippy warnings --- winit-appkit/src/app_state.rs | 4 +--- winit-appkit/src/dnd.rs | 10 +++++----- winit-appkit/src/event_loop.rs | 9 ++++----- winit-appkit/src/view.rs | 9 ++++++--- winit-appkit/src/window_delegate.rs | 18 ++++++++++++------ 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index a0c7fb90f8..c20054defd 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -118,9 +118,7 @@ impl AppState { F: FnOnce(Retained) -> R + Send, R: Send, { - self.windows.borrow_mut().get(&id)?.get_on_main(move |delegate| { - if let Some(delegate) = delegate.load() { Some(func(delegate)) } else { None } - }) + self.windows.borrow_mut().get(&id)?.get_on_main(move |delegate| delegate.load().map(func)) } pub fn new_window(&self, window: &Retained, mtm: MainThreadMarker) { diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index 641d8bcf23..b5e07212aa 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -403,7 +403,7 @@ pub(crate) struct PasteboardWriterState { // we can really do is have the first element contain all the cross-platform items, and // any further items are file paths only. uri: Option>, - writeable_types: Retained>, + writable_types: Retained>, } impl PasteboardWriter { @@ -411,7 +411,7 @@ impl PasteboardWriter { value: Box, uri: Option>, ) -> Retained { - let mut writeable_types = Vec::>::new(); + let mut writable_types = Vec::>::new(); value.for_each_available_type(&mut |type_| { let Some(spec) = PasteboardTypeSpec::from_dyn(type_) else { return ControlFlow::Continue(()); @@ -421,7 +421,7 @@ impl PasteboardWriter { return ControlFlow::Continue(()); }; - writeable_types.push((**pb_type).clone()); + writable_types.push((**pb_type).clone()); ControlFlow::Continue(()) }); @@ -429,7 +429,7 @@ impl PasteboardWriter { let pb_writer = Self::alloc().set_ivars(PasteboardWriterState { data: value, uri, - writeable_types: NSArray::from_retained_slice(&writeable_types), + writable_types: NSArray::from_retained_slice(&writable_types), }); // Unsure if there's an easier way to do this, but this is how `WindowDelegate` does it. @@ -486,7 +486,7 @@ define_class!( pasteboard: &NSPasteboard, ) -> Retained> { let vars = self.ivars(); - vars.writeable_types.clone() + vars.writable_types.clone() } #[unsafe(method(writingOptionsForType:pasteboard:))] diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index c47d2e9181..281b63d482 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -4,10 +4,9 @@ use std::rc::Rc; use std::sync::Arc; use std::time::{Duration, Instant}; -use dpi::PhysicalPosition; use objc2::rc::{Retained, autoreleasepool}; use objc2::runtime::ProtocolObject; -use objc2::{AnyThread, ClassType, MainThreadMarker, available}; +use objc2::{AnyThread, MainThreadMarker, available}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification, NSApplicationWillTerminateNotification, NSDraggingItem, NSWindow, @@ -15,7 +14,7 @@ use objc2_app_kit::{ use objc2_core_foundation::{ CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes, }; -use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSPoint, NSString}; +use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString}; use rwh_06::HasDisplayHandle; use tracing::debug_span; use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers}; @@ -190,7 +189,7 @@ impl RootActiveEventLoop for ActiveEventLoop { icon.as_ref().map(|icon| icon.offset).unwrap_or_default(); let drag_image = icon.and_then(|icon| image_from_icon(&icon.icon).ok()); - let Some(event) = dbg!(delegate.window().currentEvent()) else { + let Some(event) = delegate.window().currentEvent() else { return Err(RequestError::Ignored); }; @@ -213,7 +212,7 @@ impl RootActiveEventLoop for ActiveEventLoop { .and_then(|file_uris| { // TODO: Might not be ideal to do this let ns_url_from_os_str = - |os_str: OsString| Some(NSString::from_str(&os_str.to_str()?)); + |os_str: OsString| Some(NSString::from_str(os_str.to_str()?)); // Slightly complicated use of iterators in order to ensure that branches // have the same opaque type match file_uris { diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 208f0df7ec..0f0bb2247b 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use dpi::{LogicalPosition, LogicalSize}; use objc2::rc::Retained; use objc2::runtime::{AnyObject, Sel}; -use objc2::{AnyThread, DefinedClass, MainThreadMarker, Message, define_class, msg_send}; +use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send}; use objc2_app_kit::{ NSApplication, NSCursor, NSDraggingSession, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, @@ -1081,8 +1081,11 @@ impl WinitView { self.ivars().dragging_session.replace(Some(drag)); } - pub(crate) fn clear_dragging_session(&self) -> bool { - self.ivars().dragging_session.replace(None).is_some() + pub(crate) fn clear_dragging_session(&self, drag: &NSDraggingSession) -> bool { + let mut dragging_session = self.ivars().dragging_session.borrow_mut(); + dragging_session + .take_if(|session| session.draggingSequenceNumber() == drag.draggingSequenceNumber()) + .is_some() } fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index f4621ff26a..7cf958423e 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -371,6 +371,16 @@ define_class!( // TODO: Set this from `start_drag` NSDragOperation::all() } + + #[unsafe(method(draggingSession:endedAtPoint:operation:))] + fn dragging_session_ended_at_point( + &self, + session: &NSDraggingSession, + _: NSPoint, + _: NSDragOperation, + ) { + self.view().clear_dragging_session(session); + } } unsafe impl NSDraggingDestination for WindowDelegate { @@ -420,9 +430,7 @@ define_class!( let vars = self.ivars(); - let Some(DragState { id: transfer_id, valid_operations }) = - vars.app_state.drag_state().get() - else { + let Some(DragState { id: transfer_id, .. }) = vars.app_state.drag_state().get() else { return NSDragOperation::empty(); }; @@ -457,9 +465,7 @@ define_class!( let vars = self.ivars(); - let Some(DragState { id: transfer_id, valid_operations }) = - vars.app_state.drag_state().get() - else { + let Some(DragState { id: transfer_id, .. }) = vars.app_state.drag_state().get() else { return false.into(); }; From fcababb645a9697682c780b8a647e16e03981b80 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 3 Jun 2026 16:57:29 +0200 Subject: [PATCH 67/87] Clippy warnings --- winit-appkit/src/dnd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index b5e07212aa..b38ef04e8b 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -483,7 +483,7 @@ define_class!( #[unsafe(method_id(writableTypesForPasteboard:))] fn writable_types_for_pasteboard( &self, - pasteboard: &NSPasteboard, + _: &NSPasteboard, ) -> Retained> { let vars = self.ivars(); vars.writable_types.clone() From 54f695ec6ee32f4b195a5c653bef4a49ca1be40a Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 3 Jun 2026 17:25:19 +0200 Subject: [PATCH 68/87] Fix comment formatting --- winit/examples/dnd.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index e9d47deaaa..7d7f7c2847 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -110,9 +110,8 @@ impl ApplicationHandler for Application { }) // You can advertise a `TypeHint` that can match many types, and switch // inside the callback. For example, this will match any image type. - // This may be desirable on some platforms - // which restrict the set of image types - // that can be sent. + // This may be desirable on some platforms which restrict the set of + // image types that can be sent. .with_type(TypeHint::Image { extension_hint: None }, |image, ty| { let hint = ty.hint()?; match hint { From 3a4bb3d111a1e9235bf05a5b8ad3e855b360c83d Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Wed, 3 Jun 2026 17:45:02 +0200 Subject: [PATCH 69/87] Fix `DndActionMask` on macOS --- winit-appkit/src/dnd.rs | 5 +++++ winit-appkit/src/event_loop.rs | 6 ++++-- winit-appkit/src/view.rs | 23 ++++++++++++++++++++--- winit-appkit/src/window_delegate.rs | 3 +-- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs index b38ef04e8b..aa0ce33797 100644 --- a/winit-appkit/src/dnd.rs +++ b/winit-appkit/src/dnd.rs @@ -193,6 +193,11 @@ impl PasteboardTypeSpec { pub struct DragOperation(pub NSDragOperation); impl DragOperation { + /// An empty set of drag operations + pub fn empty() -> Self { + Self(NSDragOperation::empty()) + } + pub(crate) fn from_dyn(actions: &dyn DndActionMask) -> Self { if let Some(op) = actions.cast_ref::() { *op diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 281b63d482..e46307a0f1 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -180,9 +180,11 @@ impl RootActiveEventLoop for ActiveEventLoop { &self, source: WindowId, send_data: Box, - _action_mask: &dyn DndActionMask, + action_mask: &dyn DndActionMask, icon: Option, ) -> Result { + let drag_operation = DragOperation::from_dyn(action_mask); + self.app_state .with_window_delegate_on_main(source, move |delegate| { let dragging_rect_offset = @@ -270,7 +272,7 @@ impl RootActiveEventLoop for ActiveEventLoop { let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64); - delegate.view().set_dragging_session(session); + delegate.view().set_dragging_session(session, drag_operation); Ok(id) }) diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 0f0bb2247b..e1bb425c56 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -32,6 +32,7 @@ use super::event::{ }; use super::window::window_id; use crate::OptionAsAlt; +use crate::dnd::DragOperation; #[derive(Debug)] struct CursorState { @@ -117,6 +118,9 @@ pub struct ViewState { /// This is for a dragging session that we initiated dragging_session: RefCell>>, + /// This is for a dragging session that we initiated + drag_operations: Cell, + cursor_state: RefCell, ime_position: Cell, ime_size: Cell, @@ -784,6 +788,7 @@ impl WinitView { let this = mtm.alloc().set_ivars(ViewState { app_state: Rc::clone(app_state), dragging_session: Default::default(), + drag_operations: Cell::new(DragOperation::empty()), cursor_state: Default::default(), ime_position: Default::default(), ime_size: Default::default(), @@ -1077,12 +1082,24 @@ impl WinitView { self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); } - pub(crate) fn set_dragging_session(&self, drag: Retained) { - self.ivars().dragging_session.replace(Some(drag)); + pub(crate) fn set_dragging_session( + &self, + drag: Retained, + action_mask: DragOperation, + ) { + let vars = self.ivars(); + vars.dragging_session.replace(Some(drag)); + vars.drag_operations.set(action_mask); + } + + pub(crate) fn drag_operations(&self) -> DragOperation { + self.ivars().drag_operations.get() } pub(crate) fn clear_dragging_session(&self, drag: &NSDraggingSession) -> bool { - let mut dragging_session = self.ivars().dragging_session.borrow_mut(); + let vars = self.ivars(); + vars.drag_operations.set(DragOperation::empty()); + let mut dragging_session = vars.dragging_session.borrow_mut(); dragging_session .take_if(|session| session.draggingSequenceNumber() == drag.draggingSequenceNumber()) .is_some() diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 7cf958423e..4c1ac6a8d9 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -368,8 +368,7 @@ define_class!( _: &NSDraggingSession, _: NSDraggingContext, ) -> NSDragOperation { - // TODO: Set this from `start_drag` - NSDragOperation::all() + self.view().drag_operations().0 } #[unsafe(method(draggingSession:endedAtPoint:operation:))] From 16f210261a6b78bc9c4cac28dda37792a8d63e63 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Tue, 2 Jun 2026 21:01:54 +0200 Subject: [PATCH 70/87] Initiate drags from win32 windows --- typos.toml | 1 + winit-win32/src/definitions.rs | 61 ++- winit-win32/src/dnd.rs | 781 ++++++++++++++++++++++++++- winit-win32/src/event_loop.rs | 81 ++- winit-win32/src/event_loop/runner.rs | 44 +- 5 files changed, 935 insertions(+), 33 deletions(-) diff --git a/typos.toml b/typos.toml index c454f23b9b..20ebeff8c3 100644 --- a/typos.toml +++ b/typos.toml @@ -5,6 +5,7 @@ TME_LEAVE = "TME_LEAVE" # From windows_sys::Win32::UI::Input::Keyboa XF86_Calculater = "XF86_Calculater" # From xkbcommon_dl::keysyms::XF86_Calculater ptd = "ptd" # From windows_sys::Win32::System::Com::FORMATETC { ptd, ..} requestor = "requestor" # From x11_dl::xlib::XSelectionEvent { requestor ..} +unknwn = "unknwn" # Windows SDK header filename `unknwn.h` [files] extend-exclude = ["*.drawio"] diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index c5225cfee9..25213bc599 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -37,7 +37,7 @@ pub struct IDataObjectVtbl { pformatetc: *const FORMATETC, pmedium: *mut STGMEDIUM, ) -> HRESULT, - QueryGetData: + pub QueryGetData: unsafe extern "system" fn(This: *mut IDataObject, pformatetc: *const FORMATETC) -> HRESULT, pub GetCanonicalFormatEtc: unsafe extern "system" fn( This: *mut IDataObject, @@ -69,6 +69,36 @@ pub struct IDataObjectVtbl { ) -> HRESULT, } +#[repr(C)] +pub struct IEnumFORMATETCVtbl { + pub parent: IUnknownVtbl, + pub Next: unsafe extern "system" fn( + This: *mut IEnumFORMATETC, + celt: u32, + rgelt: *mut FORMATETC, + pceltFetched: *mut u32, + ) -> HRESULT, + pub Skip: unsafe extern "system" fn(This: *mut IEnumFORMATETC, celt: u32) -> HRESULT, + pub Reset: unsafe extern "system" fn(This: *mut IEnumFORMATETC) -> HRESULT, + pub Clone: unsafe extern "system" fn( + This: *mut IEnumFORMATETC, + ppenum: *mut *mut IEnumFORMATETC, + ) -> HRESULT, +} + +pub type IDropSource = *mut c_void; + +#[repr(C)] +pub struct IDropSourceVtbl { + pub parent: IUnknownVtbl, + pub QueryContinueDrag: unsafe extern "system" fn( + This: *mut IDropSource, + fEscapePressed: BOOL, + grfKeyState: u32, + ) -> HRESULT, + pub GiveFeedback: unsafe extern "system" fn(This: *mut IDropSource, dwEffect: u32) -> HRESULT, +} + #[repr(C)] pub struct IDropTargetVtbl { pub parent: IUnknownVtbl, @@ -130,6 +160,35 @@ pub struct ITaskbarList2 { pub lpVtbl: *const ITaskbarList2Vtbl, } +// Well-known COM IIDs. Values from `unknwn.h`, `objidl.h`, `oleidl.h`. +pub const IID_IUnknown: GUID = GUID { + data1: 0x00000000, + data2: 0x0000, + data3: 0x0000, + data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], +}; + +pub const IID_IDataObject: GUID = GUID { + data1: 0x0000010e, + data2: 0x0000, + data3: 0x0000, + data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], +}; + +pub const IID_IDropSource: GUID = GUID { + data1: 0x00000121, + data2: 0x0000, + data3: 0x0000, + data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], +}; + +pub const IID_IEnumFORMATETC: GUID = GUID { + data1: 0x00000103, + data2: 0x0000, + data3: 0x0000, + data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], +}; + pub const CLSID_TaskbarList: GUID = GUID { data1: 0x56fdf344, data2: 0xfd6d, diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 7c8ff55c61..33ecbdde9f 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -1,29 +1,43 @@ +use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::ffi::{OsString, c_void}; use std::io; use std::ops::ControlFlow; -use std::os::windows::ffi::OsStringExt; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::rc::Rc; use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; use dpi::PhysicalPosition; -use windows_sys::Win32::Foundation::{E_ABORT, E_FAIL, HGLOBAL, HWND, POINT, POINTL, S_OK}; +use windows_sys::Win32::Foundation::{ + DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, DRAGDROP_S_USEDEFAULTCURSORS, DV_E_FORMATETC, E_ABORT, + E_FAIL, E_NOINTERFACE, E_NOTIMPL, E_UNEXPECTED, GlobalFree, HGLOBAL, HWND, + OLE_E_ADVISENOTSUPPORTED, POINT, POINTL, S_FALSE, S_OK, +}; use windows_sys::Win32::Graphics::Gdi::ScreenToClient; use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_HGLOBAL}; use windows_sys::Win32::System::DataExchange::RegisterClipboardFormatW; -use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock}; +use windows_sys::Win32::System::Memory::{ + GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, +}; use windows_sys::Win32::System::Ole::{ CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, ReleaseStgMedium, }; -use windows_sys::Win32::UI::Shell::{DragQueryFileW, HDROP}; -use windows_sys::core::{GUID, HRESULT}; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use windows_sys::Win32::System::SystemServices::{ + MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_XBUTTON1, MK_XBUTTON2, +}; +use windows_sys::Win32::UI::Shell::{DROPFILES, DragQueryFileW, HDROP}; +use windows_sys::core::{BOOL, GUID, HRESULT}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, +}; use winit_core::event::WindowEvent; use winit_core::event_loop::DndActions; use crate::definitions::{ - IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl, + IDataObject, IDataObjectVtbl, IDropSource, IDropSourceVtbl, IDropTarget, IDropTargetVtbl, + IEnumFORMATETC, IEnumFORMATETCVtbl, IID_IDataObject, IID_IDropSource, IID_IEnumFORMATETC, + IID_IUnknown, IUnknown, IUnknownVtbl, }; use crate::event_loop::EventLoopRunner; use crate::util; @@ -321,15 +335,19 @@ impl FileDropHandler { pt: POINTL, pdwEffect: *mut u32, ) -> HRESULT { - static DATA_TRANSFER_ID: AtomicI64 = AtomicI64::new(0); - let drop_handler = unsafe { Self::from_interface(this) }; - let data_transfer_id = - DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)); + // If this is a self-drop (we initiated the drag from this process), reuse the source's id + // and seed actions from the mask declared at `start_drag` - the app's `DragEntered` + // handler can't call `set_valid_actions` in time because it's buffered until `DoDragDrop` + // returns. + let (data_transfer_id, initial_actions) = match drop_handler.runner.source_drag.get() { + Some(info) => (info.id, info.allowed_actions), + None => (next_data_transfer_id(), DndActions::none()), + }; drop_handler.active_data_transfer_id = Some(data_transfer_id); let data = Rc::new(unsafe { DataObject::from_idataobject(pDataObj) }); - drop_handler.runner.register_data_transfer(data_transfer_id, data); + drop_handler.runner.register_data_transfer(data_transfer_id, data, initial_actions); let mut pt = POINT { x: pt.x, y: pt.y }; unsafe { @@ -421,9 +439,14 @@ impl FileDropHandler { *pdwEffect = pick_effect(actions, grfKeyState, source_allowed); } - // The application has had a chance to read the data while handling `DragDropped`; the - // transfer's lifecycle ends here. - drop_handler.runner.remove_data_transfer(data_transfer_id); + // External drop: the app's `DragDropped` handler dispatched synchronously above and has + // already read the data; safe to release the cache. Self-drop: the handler is buffered + // and hasn't run yet, so defer cleanup until after `dispatch_buffered_events` drains. + if drop_handler.runner.source_drag.get().is_some() { + drop_handler.runner.defer_source_drag_cleanup(data_transfer_id); + } else { + drop_handler.runner.remove_data_transfer(data_transfer_id); + } S_OK } @@ -453,21 +476,27 @@ static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { Drop: FileDropHandler::Drop, }; +/// Map the app's [`DndActions`] to the win32 `DROPEFFECT_*` bitmask. +pub(crate) fn actions_to_dropeffect_mask(actions: DndActions) -> u32 { + let mut mask = 0u32; + if actions.copy() { + mask |= DROPEFFECT_COPY; + } + if actions.move_() { + mask |= DROPEFFECT_MOVE; + } + if actions.link() { + mask |= DROPEFFECT_LINK; + } + mask +} + // Intersect the app's valid actions with the source's allowed effects, honoring Ctrl/Shift. fn pick_effect(actions: DndActions, key_state: u32, source_allowed: u32) -> u32 { const MK_SHIFT: u32 = 0x0004; const MK_CONTROL: u32 = 0x0008; - let mut allowed = 0u32; - if actions.copy() && (source_allowed & DROPEFFECT_COPY) != 0 { - allowed |= DROPEFFECT_COPY; - } - if actions.move_() && (source_allowed & DROPEFFECT_MOVE) != 0 { - allowed |= DROPEFFECT_MOVE; - } - if actions.link() && (source_allowed & DROPEFFECT_LINK) != 0 { - allowed |= DROPEFFECT_LINK; - } + let allowed = actions_to_dropeffect_mask(actions) & source_allowed; if allowed == 0 { return DROPEFFECT_NONE; } @@ -492,3 +521,705 @@ fn pick_effect(actions: DndActions, key_state: u32, source_allowed: u32) -> u32 DROPEFFECT_LINK } } + +// ============================================================================ +// Source side: providing data and controlling a `DoDragDrop` session. +// ============================================================================ + +/// Mint a unique [`DataTransferId`] for either a target-side `DragEnter` or a source-side +/// `start_drag` so the two never collide. +pub(crate) fn next_data_transfer_id() -> DataTransferId { + static COUNTER: AtomicI64 = AtomicI64::new(0); + DataTransferId::from_raw(COUNTER.fetch_add(1, Ordering::Relaxed)) +} + +fn guids_eq(a: &GUID, b: &GUID) -> bool { + a.data1 == b.data1 && a.data2 == b.data2 && a.data3 == b.data3 && a.data4 == b.data4 +} + +fn register_clipboard_format(name: &str) -> Option { + let wide = util::encode_wide(name); + let atom = unsafe { RegisterClipboardFormatW(wide.as_ptr()) }; + (atom != 0).then_some(atom as u16) +} + +// Returns the (cf_format, specialised hint) pairs we can lower `hint` to. +// +// Most hints map 1:1. Wildcard hints like `Image { extension_hint: None }` - +// the cross-platform way to say "any image format" - fan out to one entry per +// concrete format we support, each paired with the specialised hint so +// `data_for_type` is invoked with a concrete extension instead of `None`. +fn cf_formats_for_hint(hint: TypeHint) -> Vec<(u16, TypeHint)> { + fn one(cf: Option, hint: TypeHint) -> Vec<(u16, TypeHint)> { + cf.map(|cf| vec![(cf, hint)]).unwrap_or_default() + } + match hint { + TypeHint::Plaintext => vec![(CF_UNICODETEXT, hint)], + TypeHint::UriList => vec![(CF_HDROP, hint)], + TypeHint::Html => one(register_clipboard_format("HTML Format"), hint), + TypeHint::Image { extension_hint: Some("png") } => { + one(register_clipboard_format("PNG"), hint) + }, + TypeHint::Image { extension_hint: None } => { + // Fan out to every concrete image format we can produce. + let mut out = Vec::new(); + if let Some(cf) = register_clipboard_format("PNG") { + out.push((cf, TypeHint::Image { extension_hint: Some("png") })); + } + out + }, + _ => Vec::new(), + } +} + +unsafe fn alloc_hglobal_from(src: *const u8, len: usize) -> Option { + let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, len) }; + if hglobal.is_null() { + return None; + } + let dst = unsafe { GlobalLock(hglobal) }; + if dst.is_null() { + // `GlobalAlloc` succeeded but locking failed - free before bailing so we don't + // leak the moveable handle. + unsafe { GlobalFree(hglobal) }; + return None; + } + unsafe { std::ptr::copy_nonoverlapping(src, dst as *mut u8, len) }; + unsafe { GlobalUnlock(hglobal) }; + Some(hglobal) +} + +/// Build the [HTML Clipboard Format] wire bytes from an app-supplied HTML string. +/// +/// The format is a UTF-8 buffer with a small text header naming byte offsets into itself. Each +/// offset placeholder is exactly 10 zero-padded decimal digits, which fixes the header at a +/// known constant length and removes the chicken-and-egg between header length and offsets. +/// +/// If the input already contains `` we trust the caller's wrapping; +/// otherwise we wrap it in a minimal `` document with fragment markers. +/// +/// [HTML Clipboard Format]: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format +fn build_html_clipboard_format(html: &str) -> Vec { + use std::borrow::Cow; + + const START_MARKER: &str = ""; + const END_MARKER: &str = ""; + + let body: Cow<'_, str> = if html.contains(START_MARKER) { + Cow::Borrowed(html) + } else { + Cow::Owned(format!("\r\n{START_MARKER}{html}{END_MARKER}\r\n")) + }; + + const HEADER_LEN: usize = concat!( + "Version:0.9\r\n", + "StartHTML:0000000000\r\n", + "EndHTML:0000000000\r\n", + "StartFragment:0000000000\r\n", + "EndFragment:0000000000\r\n", + ) + .len(); + + let start_html = HEADER_LEN; + let end_html = HEADER_LEN + body.len(); + let start_fragment = HEADER_LEN + body.find(START_MARKER).unwrap() + START_MARKER.len(); + let end_fragment = HEADER_LEN + body.find(END_MARKER).unwrap(); + + let header = format!( + "Version:0.9\r\nStartHTML:{start_html:010}\r\nEndHTML:{end_html:010}\r\nStartFragment:\ + {start_fragment:010}\r\nEndFragment:{end_fragment:010}\r\n", + ); + debug_assert_eq!(header.len(), HEADER_LEN); + + let mut buf = Vec::with_capacity(header.len() + body.len()); + buf.extend_from_slice(header.as_bytes()); + buf.extend_from_slice(body.as_bytes()); + buf +} + +/// Convert app-supplied [`SendData`] into an `HGLOBAL`-backed [`STGMEDIUM`]. +/// +/// Callers must have already validated that the `SendData` variant matches the on-the-wire shape +/// the requested clipboard format expects (see `variant_matches_hint` at the `GetData` call site). +unsafe fn send_data_to_stgmedium(data: SendData, hint: TypeHint) -> Option { + let hglobal = match data { + SendData::String(s) if matches!(hint, TypeHint::Html) => { + // HTML Clipboard Format: UTF-8 with a Version/StartHTML/EndHTML/StartFragment/ + // EndFragment header. Targets parse the header before reading the wrapped HTML. + let bytes = build_html_clipboard_format(&s); + unsafe { alloc_hglobal_from(bytes.as_ptr(), bytes.len()) }? + }, + SendData::String(s) => { + // UTF-16 + NUL - used for `CF_UNICODETEXT` and other text-ish registered formats. + let utf16 = util::encode_wide(&s); + unsafe { alloc_hglobal_from(utf16.as_ptr() as *const u8, utf16.len() * 2) }? + }, + SendData::Uris(paths) => { + // CF_HDROP: `DROPFILES` header + double-NUL-terminated UTF-16 path list. + let mut wide: Vec = Vec::new(); + for path in &paths { + wide.extend(path.encode_wide()); + wide.push(0); + } + wide.push(0); + let header = DROPFILES { + pFiles: std::mem::size_of::() as u32, + pt: POINT { x: 0, y: 0 }, + fNC: 0, + fWide: 1, + }; + let total = std::mem::size_of::() + wide.len() * 2; + let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, total) }; + if hglobal.is_null() { + return None; + } + let dst = unsafe { GlobalLock(hglobal) }; + if dst.is_null() { + unsafe { GlobalFree(hglobal) }; + return None; + } + unsafe { + std::ptr::write_unaligned(dst as *mut DROPFILES, header); + let paths_dst = (dst as *mut u8).add(std::mem::size_of::()); + std::ptr::copy_nonoverlapping( + wide.as_ptr() as *const u8, + paths_dst, + wide.len() * 2, + ); + GlobalUnlock(hglobal); + } + hglobal + }, + SendData::Bytes(b) => unsafe { alloc_hglobal_from(b.as_ptr(), b.len()) }?, + }; + + let mut medium = unsafe { std::mem::zeroed::() }; + medium.tymed = TYMED_HGLOBAL as u32; + medium.u.hGlobal = hglobal; + Some(medium) +} + +/// True if the `SendData` variant carries the on-the-wire shape OLE expects for `hint`'s mapped +/// clipboard format. Mismatches (e.g. `SendData::String` for a `UriList` hint) would otherwise +/// produce malformed `HGLOBAL` payloads (e.g. UTF-16 text mislabeled as `CF_HDROP`), which the +/// target would parse as wild offsets - memory corruption in the receiving process. +fn variant_matches_hint(hint: TypeHint, data: &SendData) -> bool { + matches!( + (hint, data), + (TypeHint::UriList, SendData::Uris(_)) + | (TypeHint::Plaintext | TypeHint::Html | TypeHint::Rtf, SendData::String(_)) + | (TypeHint::Image { .. } | TypeHint::Audio { .. }, SendData::Bytes(_)) + ) +} + +// ---- IEnumFORMATETC: a cursor over a precomputed `Vec`. ----------- + +#[repr(C)] +#[allow(non_snake_case)] +struct IEnumFORMATETCInterface { + lpVtbl: *const IEnumFORMATETCVtbl, +} + +#[repr(C)] +struct SourceFormatEnumerator { + interface: IEnumFORMATETCInterface, + refcount: AtomicUsize, + formats: Vec, + cursor: Cell, +} + +#[allow(non_snake_case)] +impl SourceFormatEnumerator { + fn new_boxed(formats: Vec) -> *mut Self { + Box::into_raw(Box::new(Self { + interface: IEnumFORMATETCInterface { + lpVtbl: &SOURCE_ENUM_FORMATETC_VTBL as *const IEnumFORMATETCVtbl, + }, + refcount: AtomicUsize::new(1), + formats, + cursor: Cell::new(0), + })) + } + + unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut SourceFormatEnumerator { + unsafe { &mut *(this as *mut _) } + } + + unsafe extern "system" fn QueryInterface( + this: *mut IUnknown, + riid: *const GUID, + ppv: *mut *mut c_void, + ) -> HRESULT { + let riid = unsafe { &*riid }; + if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, &IID_IEnumFORMATETC) { + unsafe { *ppv = this as *mut c_void }; + unsafe { Self::AddRef(this) }; + S_OK + } else { + unsafe { *ppv = std::ptr::null_mut() }; + E_NOINTERFACE + } + } + + unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + me.refcount.fetch_add(1, Ordering::Release) as u32 + 1 + } + + unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + drop(unsafe { Box::from_raw(me as *mut Self) }); + } + count as u32 + } + + unsafe extern "system" fn Next( + this: *mut IEnumFORMATETC, + celt: u32, + rgelt: *mut FORMATETC, + pcelt_fetched: *mut u32, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let cursor = me.cursor.get(); + let to_copy = (celt as usize).min(me.formats.len().saturating_sub(cursor)); + for i in 0..to_copy { + unsafe { *rgelt.add(i) = me.formats[cursor + i] }; + } + me.cursor.set(cursor + to_copy); + if !pcelt_fetched.is_null() { + unsafe { *pcelt_fetched = to_copy as u32 }; + } + if to_copy < celt as usize { S_FALSE } else { S_OK } + } + + unsafe extern "system" fn Skip(this: *mut IEnumFORMATETC, celt: u32) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let new_cursor = me.cursor.get().saturating_add(celt as usize); + if new_cursor > me.formats.len() { + me.cursor.set(me.formats.len()); + S_FALSE + } else { + me.cursor.set(new_cursor); + S_OK + } + } + + unsafe extern "system" fn Reset(this: *mut IEnumFORMATETC) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + me.cursor.set(0); + S_OK + } + + unsafe extern "system" fn Clone( + this: *mut IEnumFORMATETC, + ppenum: *mut *mut IEnumFORMATETC, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let cloned = SourceFormatEnumerator::new_boxed(me.formats.clone()); + unsafe { (*cloned).cursor.set(me.cursor.get()) }; + unsafe { *ppenum = cloned as *mut IEnumFORMATETC }; + S_OK + } +} + +static SOURCE_ENUM_FORMATETC_VTBL: IEnumFORMATETCVtbl = IEnumFORMATETCVtbl { + parent: IUnknownVtbl { + QueryInterface: SourceFormatEnumerator::QueryInterface, + AddRef: SourceFormatEnumerator::AddRef, + Release: SourceFormatEnumerator::Release, + }, + Next: SourceFormatEnumerator::Next, + Skip: SourceFormatEnumerator::Skip, + Reset: SourceFormatEnumerator::Reset, + Clone: SourceFormatEnumerator::Clone, +}; + +// ---- IDataObject (source) --------------------------------------------------- + +#[repr(C)] +#[allow(non_snake_case)] +struct IDataObjectInterface { + lpVtbl: *const IDataObjectVtbl, +} + +#[repr(C)] +struct SourceDataObjectData { + interface: IDataObjectInterface, + refcount: AtomicUsize, + send_data: RefCell>, + // (cf_format, hint) pairs we advertise to the target. + formats: Vec<(u16, TypeHint)>, +} + +#[allow(non_snake_case)] +impl SourceDataObjectData { + fn new_boxed(send_data: Box) -> *mut Self { + let mut formats: Vec<(u16, TypeHint)> = Vec::new(); + send_data.for_each_available_type(&mut |ty| { + if let Some(hint) = ty.hint() { + for (cf, specialised) in cf_formats_for_hint(hint) { + if !formats.iter().any(|(c, _)| *c == cf) { + formats.push((cf, specialised)); + } + } + } + ControlFlow::Continue(()) + }); + + Box::into_raw(Box::new(Self { + interface: IDataObjectInterface { + lpVtbl: &SOURCE_DATA_OBJECT_VTBL as *const IDataObjectVtbl, + }, + refcount: AtomicUsize::new(1), + send_data: RefCell::new(send_data), + formats, + })) + } + + unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut SourceDataObjectData { + unsafe { &mut *(this as *mut _) } + } + + unsafe extern "system" fn QueryInterface( + this: *mut IUnknown, + riid: *const GUID, + ppv: *mut *mut c_void, + ) -> HRESULT { + let riid = unsafe { &*riid }; + if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, &IID_IDataObject) { + unsafe { *ppv = this as *mut c_void }; + unsafe { Self::AddRef(this) }; + S_OK + } else { + unsafe { *ppv = std::ptr::null_mut() }; + E_NOINTERFACE + } + } + + unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + me.refcount.fetch_add(1, Ordering::Release) as u32 + 1 + } + + unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + drop(unsafe { Box::from_raw(me as *mut Self) }); + } + count as u32 + } + + unsafe extern "system" fn GetData( + this: *mut IDataObject, + pformatetc_in: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let format = unsafe { &*pformatetc_in }; + if (format.tymed & TYMED_HGLOBAL as u32) == 0 { + return DV_E_FORMATETC; + } + let Some(&(_, hint)) = me.formats.iter().find(|&&(cf, _)| cf == format.cfFormat) else { + return DV_E_FORMATETC; + }; + // `try_borrow_mut` rather than `borrow_mut`: `data_for_type` is the app's callback and + // may itself reach back into this `IDataObject`. A panic across the `extern "system"` + // boundary would be UB; return `E_UNEXPECTED` instead. + let data = { + let Ok(send_data) = me.send_data.try_borrow_mut() else { + return E_UNEXPECTED; + }; + send_data.data_for_type(&hint) + }; + let Some(data) = data else { + return DV_E_FORMATETC; + }; + if !variant_matches_hint(hint, &data) { + return DV_E_FORMATETC; + } + let Some(medium) = (unsafe { send_data_to_stgmedium(data, hint) }) else { + return E_FAIL; + }; + unsafe { *pmedium = medium }; + S_OK + } + + unsafe extern "system" fn GetDataHere( + _this: *mut IDataObject, + _pformatetc: *const FORMATETC, + _pmedium: *mut STGMEDIUM, + ) -> HRESULT { + E_NOTIMPL + } + + unsafe extern "system" fn QueryGetData( + this: *mut IDataObject, + pformatetc: *const FORMATETC, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let format = unsafe { &*pformatetc }; + if (format.tymed & TYMED_HGLOBAL as u32) == 0 { + return DV_E_FORMATETC; + } + if me.formats.iter().any(|&(cf, _)| cf == format.cfFormat) { S_OK } else { S_FALSE } + } + + unsafe extern "system" fn GetCanonicalFormatEtc( + _this: *mut IDataObject, + _pformatetc_in: *const FORMATETC, + _pformatetc_out: *mut FORMATETC, + ) -> HRESULT { + E_NOTIMPL + } + + unsafe extern "system" fn SetData( + _this: *mut IDataObject, + _pformatetc: *const FORMATETC, + _pformatetc_out: *const FORMATETC, + _f_release: BOOL, + ) -> HRESULT { + E_NOTIMPL + } + + unsafe extern "system" fn EnumFormatEtc( + this: *mut IDataObject, + dw_direction: u32, + ppenum: *mut *mut IEnumFORMATETC, + ) -> HRESULT { + const DATADIR_GET: u32 = 1; + if dw_direction != DATADIR_GET { + return E_NOTIMPL; + } + let me = unsafe { Self::from_interface(this) }; + let formats: Vec = me + .formats + .iter() + .map(|&(cf, _)| FORMATETC { + cfFormat: cf, + ptd: std::ptr::null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL as u32, + }) + .collect(); + let enumerator = SourceFormatEnumerator::new_boxed(formats); + unsafe { *ppenum = enumerator as *mut IEnumFORMATETC }; + S_OK + } + + unsafe extern "system" fn DAdvise( + _this: *mut IDataObject, + _pformatetc: *const FORMATETC, + _advf: u32, + _adv_sink: *const crate::definitions::IAdviseSink, + _pdw_connection: *mut u32, + ) -> HRESULT { + OLE_E_ADVISENOTSUPPORTED + } + + unsafe extern "system" fn DUnadvise(_this: *mut IDataObject, _connection: u32) -> HRESULT { + OLE_E_ADVISENOTSUPPORTED + } + + unsafe extern "system" fn EnumDAdvise( + _this: *mut IDataObject, + _ppenum_advise: *const *const crate::definitions::IEnumSTATDATA, + ) -> HRESULT { + OLE_E_ADVISENOTSUPPORTED + } +} + +static SOURCE_DATA_OBJECT_VTBL: IDataObjectVtbl = IDataObjectVtbl { + parent: IUnknownVtbl { + QueryInterface: SourceDataObjectData::QueryInterface, + AddRef: SourceDataObjectData::AddRef, + Release: SourceDataObjectData::Release, + }, + GetData: SourceDataObjectData::GetData, + GetDataHere: SourceDataObjectData::GetDataHere, + QueryGetData: SourceDataObjectData::QueryGetData, + GetCanonicalFormatEtc: SourceDataObjectData::GetCanonicalFormatEtc, + SetData: SourceDataObjectData::SetData, + EnumFormatEtc: SourceDataObjectData::EnumFormatEtc, + DAdvise: SourceDataObjectData::DAdvise, + DUnadvise: SourceDataObjectData::DUnadvise, + EnumDAdvise: SourceDataObjectData::EnumDAdvise, +}; + +pub(crate) struct SourceDataObject { + data: *mut SourceDataObjectData, +} + +impl SourceDataObject { + pub(crate) fn new(send_data: Box) -> Self { + Self { data: SourceDataObjectData::new_boxed(send_data) } + } + + pub(crate) fn interface_ptr(&self) -> *mut c_void { + self.data as *mut c_void + } +} + +impl Drop for SourceDataObject { + fn drop(&mut self) { + unsafe { SourceDataObjectData::Release(self.data as *mut IUnknown) }; + } +} + +// ---- IDropSource ------------------------------------------------------------ + +#[repr(C)] +#[allow(non_snake_case)] +struct IDropSourceInterface { + lpVtbl: *const IDropSourceVtbl, +} + +#[repr(C)] +struct DropSourceData { + interface: IDropSourceInterface, + refcount: AtomicUsize, +} + +#[allow(non_snake_case)] +impl DropSourceData { + fn new_boxed() -> *mut Self { + Box::into_raw(Box::new(Self { + interface: IDropSourceInterface { lpVtbl: &DROP_SOURCE_VTBL as *const IDropSourceVtbl }, + refcount: AtomicUsize::new(1), + })) + } + + unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut DropSourceData { + unsafe { &mut *(this as *mut _) } + } + + unsafe extern "system" fn QueryInterface( + this: *mut IUnknown, + riid: *const GUID, + ppv: *mut *mut c_void, + ) -> HRESULT { + let riid = unsafe { &*riid }; + if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, &IID_IDropSource) { + unsafe { *ppv = this as *mut c_void }; + unsafe { Self::AddRef(this) }; + S_OK + } else { + unsafe { *ppv = std::ptr::null_mut() }; + E_NOINTERFACE + } + } + + unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + me.refcount.fetch_add(1, Ordering::Release) as u32 + 1 + } + + unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + drop(unsafe { Box::from_raw(me as *mut Self) }); + } + count as u32 + } + + unsafe extern "system" fn QueryContinueDrag( + _this: *mut IDropSource, + escape_pressed: BOOL, + grf_key_state: u32, + ) -> HRESULT { + // Drop when no mouse button is pressed - matches Microsoft's documented + // `IDropSource` example and covers right-button / middle-button drags as + // well as the common left-button case. Hardcoding `MK_LBUTTON` would make + // a right-drag (e.g. context-menu drag) never terminate by mouse release. + const ANY_MOUSE_BUTTON: u32 = + MK_LBUTTON | MK_RBUTTON | MK_MBUTTON | MK_XBUTTON1 | MK_XBUTTON2; + if escape_pressed != 0 { + return DRAGDROP_S_CANCEL; + } + if (grf_key_state & ANY_MOUSE_BUTTON) == 0 { + return DRAGDROP_S_DROP; + } + S_OK + } + + unsafe extern "system" fn GiveFeedback(_this: *mut IDropSource, _dw_effect: u32) -> HRESULT { + DRAGDROP_S_USEDEFAULTCURSORS + } +} + +static DROP_SOURCE_VTBL: IDropSourceVtbl = IDropSourceVtbl { + parent: IUnknownVtbl { + QueryInterface: DropSourceData::QueryInterface, + AddRef: DropSourceData::AddRef, + Release: DropSourceData::Release, + }, + QueryContinueDrag: DropSourceData::QueryContinueDrag, + GiveFeedback: DropSourceData::GiveFeedback, +}; + +pub(crate) struct DropSource { + data: *mut DropSourceData, +} + +impl DropSource { + pub(crate) fn new() -> Self { + Self { data: DropSourceData::new_boxed() } + } + + pub(crate) fn interface_ptr(&self) -> *mut c_void { + self.data as *mut c_void + } +} + +impl Drop for DropSource { + fn drop(&mut self) { + unsafe { DropSourceData::Release(self.data as *mut IUnknown) }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_offset(buf: &[u8], name: &str) -> usize { + let prefix = format!("{name}:"); + let head = std::str::from_utf8(buf).unwrap(); + let pos = head.find(&prefix).unwrap() + prefix.len(); + head[pos..pos + 10].parse().unwrap() + } + + #[test] + fn html_clipboard_format_brackets_user_html() { + let html = "Winit example"; + let buf = build_html_clipboard_format(html); + + assert!(buf.starts_with(b"Version:0.9\r\n")); + + let start_html = parse_offset(&buf, "StartHTML"); + let end_html = parse_offset(&buf, "EndHTML"); + let doc = std::str::from_utf8(&buf[start_html..end_html]).unwrap(); + assert!(doc.starts_with("")); + assert!(doc.ends_with("")); + + let start_fragment = parse_offset(&buf, "StartFragment"); + let end_fragment = parse_offset(&buf, "EndFragment"); + let fragment = std::str::from_utf8(&buf[start_fragment..end_fragment]).unwrap(); + assert_eq!(fragment, html); + } + + #[test] + fn html_clipboard_format_preserves_pre_wrapped() { + let pre_wrapped = + "

hi

"; + let buf = build_html_clipboard_format(pre_wrapped); + + let start_fragment = parse_offset(&buf, "StartFragment"); + let end_fragment = parse_offset(&buf, "EndFragment"); + let fragment = std::str::from_utf8(&buf[start_fragment..end_fragment]).unwrap(); + assert_eq!(fragment, "

hi

"); + } +} diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index bff0ba3cec..50ed367fc3 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -13,13 +13,16 @@ use std::{fmt, mem, panic, ptr}; use dpi::{PhysicalPosition, PhysicalSize}; use windows_sys::Win32::Foundation::{ - FALSE, GetLastError, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WAIT_FAILED, WPARAM, + DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, FALSE, GetLastError, HANDLE, HWND, LPARAM, LRESULT, POINT, + RECT, WAIT_FAILED, WPARAM, }; use windows_sys::Win32::Graphics::Gdi::{ GetMonitorInfoW, MONITOR_DEFAULTTONULL, MONITORINFO, MonitorFromRect, MonitorFromWindow, RDW_INTERNALPAINT, RedrawWindow, SC_SCREENSAVE, ScreenToClient, ValidateRect, }; -use windows_sys::Win32::System::Ole::RevokeDragDrop; +use windows_sys::Win32::System::Ole::{ + DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DoDragDrop, RevokeDragDrop, +}; use windows_sys::Win32::System::Threading::{ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, CreateWaitableTimerExW, GetCurrentThreadId, INFINITE, SetWaitableTimer, TIMER_ALL_ACCESS, @@ -63,7 +66,9 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ }; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor, CustomCursorSource}; -use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypedData}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData, +}; use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::event::{ DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton, @@ -71,7 +76,7 @@ use winit_core::event::{ }; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, DragIcon, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; @@ -83,8 +88,9 @@ pub(super) use self::runner::{Event, EventLoopRunner}; use super::SelectedCursor; use super::window::set_skip_taskbar; use crate::dark_mode::try_theme; -use crate::dnd::{FileDropHandler, WinDataTransfer, WinTypedData}; +use crate::dnd::{DropSource, FileDropHandler, SourceDataObject, WinDataTransfer, WinTypedData}; use crate::dpi::{become_dpi_aware, dpi_to_scale_factor}; +use crate::event_loop::runner::SourceDrag; use crate::icon::WinCursor; use crate::ime::ImeContext; use crate::keyboard::KeyEventBuilder; @@ -513,6 +519,63 @@ impl RootActiveEventLoop for ActiveEventLoop { state.actions = actions.hint(); Ok(()) } + + fn start_drag( + &self, + _source: WindowId, + send_data: Box, + action_mask: &dyn DndActionMask, + _icon: Option, + ) -> Result { + let allowed_actions = action_mask.hint(); + let allowed_effects = crate::dnd::actions_to_dropeffect_mask(allowed_actions); + // Win32 would happily run a modal `DoDragDrop` with `allowed_effects == 0`, but every + // target would see "no action allowed" and the drag would end in a guaranteed cancel + // after burning a full modal pump. Fail fast instead - the caller asked for a drag + // they explicitly refuse to allow. + if allowed_effects == 0 { + return Err(NotSupportedError::new( + "start_drag called with an empty action mask (DndActions::none())", + ) + .into()); + } + + let id = crate::dnd::next_data_transfer_id(); + let data_object = SourceDataObject::new(send_data); + let drop_source = DropSource::new(); + + // Make the drag visible to our own target-side `IDropTarget` so it can recognize + // self-drops and reuse this id + action mask without going through the (buffered) app + // handler. The guard ensures the flag is cleared on any exit path - if anything between + // here and `DoDragDrop`'s return panics, the stale flag would otherwise permanently + // disable `WM_PAINT` dispatch and misclassify all future external drags as self-drops. + struct ClearOnDrop<'a>(&'a Cell>); + impl Drop for ClearOnDrop<'_> { + fn drop(&mut self) { + self.0.set(None); + } + } + self.0.source_drag.set(Some(SourceDrag { id, allowed_actions })); + let _guard = ClearOnDrop(&self.0.source_drag); + + let mut effect_out: u32 = 0; + let hr = unsafe { + DoDragDrop( + data_object.interface_ptr(), + drop_source.interface_ptr(), + allowed_effects, + &mut effect_out, + ) + }; + + // Both `DRAGDROP_S_DROP` and `DRAGDROP_S_CANCEL` are success codes for us - the app + // will hear about the outcome via the buffered `DragDropped`/`DragLeft` events. + if hr == DRAGDROP_S_DROP || hr == DRAGDROP_S_CANCEL { + Ok(id) + } else { + Err(os_error!(std::io::Error::other(format!("DoDragDrop failed: 0x{hr:08x}"))).into()) + } + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { @@ -1225,6 +1288,14 @@ unsafe fn public_window_callback_inner( result = ProcResult::Value(0); }, + WM_PAINT if userdata.event_loop_runner.source_drag.get().is_some() => { + // While a source-side drag is in flight, the app handler is on the stack (we're + // inside `start_drag` -> `DoDragDrop`), so we can neither dispatch `RedrawRequested` + // nor keep re-arming via `RDW_INTERNALPAINT` (that would spin in OLE's modal loop). + // Let `DefWindowProcW` validate the region and show stale content for the duration of + // the drag; the next real paint happens once `DoDragDrop` returns. + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + }, WM_PAINT => { userdata.window_state_lock().redraw_requested = userdata.event_loop_runner.should_buffer(); diff --git a/winit-win32/src/event_loop/runner.rs b/winit-win32/src/event_loop/runner.rs index 2fc7dbd8d1..51bcb973c4 100644 --- a/winit-win32/src/event_loop/runner.rs +++ b/winit-win32/src/event_loop/runner.rs @@ -29,6 +29,15 @@ pub(super) struct DragState { pub(super) actions: DndActions, } +/// Set while `DoDragDrop` is on the call stack - i.e., this process is the source of an active +/// drag. The target-side `IDropTarget` checks this to recognize self-drops and reuse the source's +/// id + allowed actions instead of waiting for the (buffered) app `DragEntered` handler. +#[derive(Copy, Clone)] +pub(crate) struct SourceDrag { + pub(crate) id: DataTransferId, + pub(crate) allowed_actions: DndActions, +} + pub(crate) struct EventLoopRunner { pub(super) thread_id: u32, @@ -51,6 +60,17 @@ pub(crate) struct EventLoopRunner { /// `DragLeft`/`DragDropped`. pub(super) drag_state: RefCell>, + /// `Some(_)` while `start_drag` has `DoDragDrop` on the call stack. + pub(crate) source_drag: Cell>, + + /// For self-drops, target-side `IDropTarget::Drop` can't release the cached `DragState` + /// before its `DragDropped` `WindowEvent` is delivered - the event is buffered (the outer app + /// handler holds `event_handler` for the duration of `DoDragDrop`) and `data_transfer(id)` + /// would return `UnknownDataTransfer` if cleanup ran synchronously. So we stash the id here + /// and drain it at the end of `dispatch_buffered_events`, after the app's buffered handler + /// has had its chance to read the data. + pending_source_drag_cleanup: Cell>, + panic_error: Cell>, } @@ -102,11 +122,22 @@ impl EventLoopRunner { event_handler: Rc::new(Cell::new(None)), event_buffer: RefCell::new(VecDeque::new()), drag_state: RefCell::new(None), + source_drag: Cell::new(None), + pending_source_drag_cleanup: Cell::new(None), } } - pub(crate) fn register_data_transfer(&self, id: DataTransferId, data: Rc) { - *self.drag_state.borrow_mut() = Some(DragState { id, data, actions: DndActions::none() }); + pub(crate) fn defer_source_drag_cleanup(&self, id: DataTransferId) { + self.pending_source_drag_cleanup.set(Some(id)); + } + + pub(crate) fn register_data_transfer( + &self, + id: DataTransferId, + data: Rc, + actions: DndActions, + ) { + *self.drag_state.borrow_mut() = Some(DragState { id, data, actions }); } pub(crate) fn remove_data_transfer(&self, id: DataTransferId) { @@ -179,6 +210,8 @@ impl EventLoopRunner { event_handler, event_buffer: _, drag_state, + source_drag, + pending_source_drag_cleanup, } = self; interrupt_msg_dispatch.set(false); runner_state.set(RunnerState::Uninitialized); @@ -186,6 +219,8 @@ impl EventLoopRunner { exit.set(None); event_handler.set(None); *drag_state.borrow_mut() = None; + source_drag.set(None); + pending_source_drag_cleanup.set(None); } } @@ -327,6 +362,11 @@ impl EventLoopRunner { None => break, } } + // The app's buffered `DragDropped` handler (if any) has now had its chance to call + // `data_transfer(id)`; safe to release the cached `DragState` for a deferred self-drop. + if let Some(id) = self.pending_source_drag_cleanup.take() { + self.remove_data_transfer(id); + } } /// Dispatch control flow events (`NewEvents`, `AboutToWait`, and From 1e1c579d91a7b81bf29f833b867453193cfe15bc Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Tue, 2 Jun 2026 22:00:05 +0200 Subject: [PATCH 71/87] Emit DragLeft when the drop was rejected Drop emitted DragDropped before computing the effect, so a target rejection via set_valid_actions(none()) left the source seeing DROPEFFECT_NONE while the target app had already seen DragDropped. Compute the effect first and route DROPEFFECT_NONE to DragLeft. Also trace effect_out on the source - cross-process drops have no in-process target callback to observe it through. --- winit-win32/src/dnd.rs | 21 ++++++++++++++++----- winit-win32/src/event_loop.rs | 13 ++++++++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 33ecbdde9f..a880590878 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -432,16 +432,27 @@ impl FileDropHandler { ScreenToClient(drop_handler.window, &mut pt); } + // Negotiate the effect first so we can pick the right outgoing event. If the app + // rejected the drop (e.g. via `set_valid_actions(none())`), `pick_effect` returns + // `DROPEFFECT_NONE`; in that case OLE reports back `effect_out == DROPEFFECT_NONE` to + // the source, so the matching observer-facing event is `DragLeft`, not `DragDropped` - + // otherwise target and source see contradictory outcomes. + let effect = pick_effect(actions, grfKeyState, source_allowed); let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); (drop_handler.send_event)(WindowEvent::DragPosition { id: data_transfer_id, position }); - (drop_handler.send_event)(WindowEvent::DragDropped { id: data_transfer_id }); + let event = if effect == DROPEFFECT_NONE { + WindowEvent::DragLeft { id: data_transfer_id } + } else { + WindowEvent::DragDropped { id: data_transfer_id } + }; + (drop_handler.send_event)(event); unsafe { - *pdwEffect = pick_effect(actions, grfKeyState, source_allowed); + *pdwEffect = effect; } - // External drop: the app's `DragDropped` handler dispatched synchronously above and has - // already read the data; safe to release the cache. Self-drop: the handler is buffered - // and hasn't run yet, so defer cleanup until after `dispatch_buffered_events` drains. + // External drop: the app's handler dispatched synchronously above and has already read + // the data; safe to release the cache. Self-drop: the handler is buffered and hasn't + // run yet, so defer cleanup until after `dispatch_buffered_events` drains. if drop_handler.runner.source_drag.get().is_some() { drop_handler.runner.defer_source_drag_cleanup(data_transfer_id); } else { diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 50ed367fc3..251b7f7dd3 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -21,7 +21,7 @@ use windows_sys::Win32::Graphics::Gdi::{ RDW_INTERNALPAINT, RedrawWindow, SC_SCREENSAVE, ScreenToClient, ValidateRect, }; use windows_sys::Win32::System::Ole::{ - DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DoDragDrop, RevokeDragDrop, + DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, DoDragDrop, RevokeDragDrop, }; use windows_sys::Win32::System::Threading::{ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, CreateWaitableTimerExW, GetCurrentThreadId, INFINITE, @@ -558,7 +558,7 @@ impl RootActiveEventLoop for ActiveEventLoop { self.0.source_drag.set(Some(SourceDrag { id, allowed_actions })); let _guard = ClearOnDrop(&self.0.source_drag); - let mut effect_out: u32 = 0; + let mut effect_out: u32 = DROPEFFECT_NONE; let hr = unsafe { DoDragDrop( data_object.interface_ptr(), @@ -569,8 +569,15 @@ impl RootActiveEventLoop for ActiveEventLoop { }; // Both `DRAGDROP_S_DROP` and `DRAGDROP_S_CANCEL` are success codes for us - the app - // will hear about the outcome via the buffered `DragDropped`/`DragLeft` events. + // will hear about the outcome via the buffered `DragDropped`/`DragLeft` events + // (target-side translates `effect_out == DROPEFFECT_NONE` to `DragLeft`). + // Log the negotiated effect so cross-process drops, which have no target-side event + // in this process, leave a debuggable trace of what action the remote target performed. if hr == DRAGDROP_S_DROP || hr == DRAGDROP_S_CANCEL { + tracing::trace!( + "DoDragDrop completed: hr=0x{hr:08x} effect_out={effect_out} \ + (COPY={DROPEFFECT_COPY}, MOVE={DROPEFFECT_MOVE}, LINK={DROPEFFECT_LINK})", + ); Ok(id) } else { Err(os_error!(std::io::Error::other(format!("DoDragDrop failed: 0x{hr:08x}"))).into()) From 353fb870bf2398ce8e9225e2a8117b73c4b1b7b7 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Tue, 2 Jun 2026 22:01:41 +0200 Subject: [PATCH 72/87] Pair COM Release with an Acquire fence before drop Standard Arc pattern: Release on decrement publishes, but the dropping thread needs an Acquire fence on the zero transition to see writes another thread made through the object. AddRef drops to Relaxed - a bump on a count you already reference doesn't synchronize anything. Latent today since our COM objects stay in the STA. --- winit-win32/src/dnd.rs | 170 ++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 104 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index a880590878..2763eadcb1 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -5,7 +5,7 @@ use std::io; use std::ops::ControlFlow; use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::rc::Rc; -use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; +use std::sync::atomic::{self, AtomicI64, AtomicUsize, Ordering}; use dpi::PhysicalPosition; use windows_sys::Win32::Foundation::{ @@ -309,14 +309,17 @@ impl FileDropHandler { unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { let drop_handler_data = unsafe { Self::from_interface(this) }; - let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; + let count = drop_handler_data.refcount.fetch_add(1, Ordering::Relaxed) + 1; count as u32 } unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { let drop_handler = unsafe { Self::from_interface(this) }; + // See the SourceDataObject Release for why we use Release on decrement and an + // Acquire fence on the zero-transition (standard Arc pattern). let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; if count == 0 { + atomic::fence(Ordering::Acquire); // Drop any transfer still in flight (e.g. the window was destroyed mid-drag, so no // `DragLeave`/`Drop` ever arrived to clean it up). if let Some(id) = drop_handler.active_data_transfer_id.take() { @@ -731,6 +734,61 @@ struct IEnumFORMATETCInterface { lpVtbl: *const IEnumFORMATETCVtbl, } +/// Generates the shared `IUnknown` boilerplate for our hand-rolled COM source-side objects. +/// +/// Each such object is a `Box`-allocated `#[repr(C)]` struct whose first field is its COM +/// interface and which carries an `AtomicUsize` `refcount`. The thunks are identical across +/// objects apart from the concrete type and the one extra IID accepted by `QueryInterface`, so +/// the subtle refcount memory ordering lives in exactly one place. +macro_rules! com_iunknown_impl { + ($ty:ty, $extra_iid:expr) => { + #[allow(non_snake_case)] + impl $ty { + unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut Self { + unsafe { &mut *(this as *mut _) } + } + + unsafe extern "system" fn QueryInterface( + this: *mut IUnknown, + riid: *const GUID, + ppv: *mut *mut c_void, + ) -> HRESULT { + let riid = unsafe { &*riid }; + if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, $extra_iid) { + unsafe { *ppv = this as *mut c_void }; + unsafe { Self::AddRef(this) }; + S_OK + } else { + unsafe { *ppv = std::ptr::null_mut() }; + E_NOINTERFACE + } + } + + unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + // Mere refcount bump - Relaxed is enough; the caller already holds a synchronized + // reference to the object. + me.refcount.fetch_add(1, Ordering::Relaxed) as u32 + 1 + } + + unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + // Release on decrement publishes any writes made through this reference before the + // count is observed by other threads. When we hit zero, fence with Acquire so the + // destructor sees all writes from prior Releases on other threads - the standard + // Arc pattern. Without the fence, dropping the box could race with reads done by + // the last releasing thread on another core. + let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + atomic::fence(Ordering::Acquire); + drop(unsafe { Box::from_raw(me as *mut Self) }); + } + count as u32 + } + } + }; +} + #[repr(C)] struct SourceFormatEnumerator { interface: IEnumFORMATETCInterface, @@ -739,6 +797,8 @@ struct SourceFormatEnumerator { cursor: Cell, } +com_iunknown_impl!(SourceFormatEnumerator, &IID_IEnumFORMATETC); + #[allow(non_snake_case)] impl SourceFormatEnumerator { fn new_boxed(formats: Vec) -> *mut Self { @@ -752,40 +812,6 @@ impl SourceFormatEnumerator { })) } - unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut SourceFormatEnumerator { - unsafe { &mut *(this as *mut _) } - } - - unsafe extern "system" fn QueryInterface( - this: *mut IUnknown, - riid: *const GUID, - ppv: *mut *mut c_void, - ) -> HRESULT { - let riid = unsafe { &*riid }; - if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, &IID_IEnumFORMATETC) { - unsafe { *ppv = this as *mut c_void }; - unsafe { Self::AddRef(this) }; - S_OK - } else { - unsafe { *ppv = std::ptr::null_mut() }; - E_NOINTERFACE - } - } - - unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { - let me = unsafe { Self::from_interface(this) }; - me.refcount.fetch_add(1, Ordering::Release) as u32 + 1 - } - - unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { - let me = unsafe { Self::from_interface(this) }; - let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; - if count == 0 { - drop(unsafe { Box::from_raw(me as *mut Self) }); - } - count as u32 - } - unsafe extern "system" fn Next( this: *mut IEnumFORMATETC, celt: u32, @@ -864,6 +890,8 @@ struct SourceDataObjectData { formats: Vec<(u16, TypeHint)>, } +com_iunknown_impl!(SourceDataObjectData, &IID_IDataObject); + #[allow(non_snake_case)] impl SourceDataObjectData { fn new_boxed(send_data: Box) -> *mut Self { @@ -889,40 +917,6 @@ impl SourceDataObjectData { })) } - unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut SourceDataObjectData { - unsafe { &mut *(this as *mut _) } - } - - unsafe extern "system" fn QueryInterface( - this: *mut IUnknown, - riid: *const GUID, - ppv: *mut *mut c_void, - ) -> HRESULT { - let riid = unsafe { &*riid }; - if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, &IID_IDataObject) { - unsafe { *ppv = this as *mut c_void }; - unsafe { Self::AddRef(this) }; - S_OK - } else { - unsafe { *ppv = std::ptr::null_mut() }; - E_NOINTERFACE - } - } - - unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { - let me = unsafe { Self::from_interface(this) }; - me.refcount.fetch_add(1, Ordering::Release) as u32 + 1 - } - - unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { - let me = unsafe { Self::from_interface(this) }; - let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; - if count == 0 { - drop(unsafe { Box::from_raw(me as *mut Self) }); - } - count as u32 - } - unsafe extern "system" fn GetData( this: *mut IDataObject, pformatetc_in: *const FORMATETC, @@ -1094,6 +1088,8 @@ struct DropSourceData { refcount: AtomicUsize, } +com_iunknown_impl!(DropSourceData, &IID_IDropSource); + #[allow(non_snake_case)] impl DropSourceData { fn new_boxed() -> *mut Self { @@ -1103,40 +1099,6 @@ impl DropSourceData { })) } - unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut DropSourceData { - unsafe { &mut *(this as *mut _) } - } - - unsafe extern "system" fn QueryInterface( - this: *mut IUnknown, - riid: *const GUID, - ppv: *mut *mut c_void, - ) -> HRESULT { - let riid = unsafe { &*riid }; - if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, &IID_IDropSource) { - unsafe { *ppv = this as *mut c_void }; - unsafe { Self::AddRef(this) }; - S_OK - } else { - unsafe { *ppv = std::ptr::null_mut() }; - E_NOINTERFACE - } - } - - unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { - let me = unsafe { Self::from_interface(this) }; - me.refcount.fetch_add(1, Ordering::Release) as u32 + 1 - } - - unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { - let me = unsafe { Self::from_interface(this) }; - let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; - if count == 0 { - drop(unsafe { Box::from_raw(me as *mut Self) }); - } - count as u32 - } - unsafe extern "system" fn QueryContinueDrag( _this: *mut IDropSource, escape_pressed: BOOL, From dfd05ae5e1b98a4270be8d2c94f56859e34ce600 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Tue, 2 Jun 2026 22:06:46 +0200 Subject: [PATCH 73/87] Implement DragIcon on win32 Plumb the icon through IDragSourceHelper::InitializeFromBitmap. The shell composites in premultiplied alpha, so convert RGBA to premultiplied BGRA in a top-down DIB or translucent edges get a halo. Helper failures are non-fatal. --- winit-win32/src/definitions.rs | 66 +++++- winit-win32/src/dnd.rs | 360 +++++++++++++++++++++++++++++++-- winit-win32/src/event_loop.rs | 23 ++- 3 files changed, 425 insertions(+), 24 deletions(-) diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index 25213bc599..801fc41533 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -3,8 +3,9 @@ use std::ffi::c_void; -use windows_sys::Win32::Foundation::{HWND, POINTL}; +use windows_sys::Win32::Foundation::{HWND, POINT, POINTL}; use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM}; +use windows_sys::Win32::UI::Shell::SHDRAGIMAGE; use windows_sys::core::{BOOL, GUID, HRESULT}; pub type IUnknown = *mut c_void; @@ -47,7 +48,7 @@ pub struct IDataObjectVtbl { pub SetData: unsafe extern "system" fn( This: *mut IDataObject, pformatetc: *const FORMATETC, - pformatetcOut: *const FORMATETC, + pmedium: *const STGMEDIUM, fRelease: BOOL, ) -> HRESULT, pub EnumFormatEtc: unsafe extern "system" fn( @@ -86,6 +87,51 @@ pub struct IEnumFORMATETCVtbl { ) -> HRESULT, } +pub type IDragSourceHelper = *mut c_void; + +#[repr(C)] +pub struct IDragSourceHelperVtbl { + pub parent: IUnknownVtbl, + pub InitializeFromBitmap: unsafe extern "system" fn( + This: *mut IDragSourceHelper, + pshdi: *const SHDRAGIMAGE, + pDataObject: *mut IDataObject, + ) -> HRESULT, + pub InitializeFromWindow: unsafe extern "system" fn( + This: *mut IDragSourceHelper, + hwnd: HWND, + ppt: *const POINT, + pDataObject: *mut IDataObject, + ) -> HRESULT, +} + +pub type IDropTargetHelper = *mut c_void; + +#[repr(C)] +pub struct IDropTargetHelperVtbl { + pub parent: IUnknownVtbl, + pub DragEnter: unsafe extern "system" fn( + This: *mut IDropTargetHelper, + hwndTarget: HWND, + pDataObject: *mut IDataObject, + ppt: *const POINT, + dwEffect: u32, + ) -> HRESULT, + pub DragLeave: unsafe extern "system" fn(This: *mut IDropTargetHelper) -> HRESULT, + pub DragOver: unsafe extern "system" fn( + This: *mut IDropTargetHelper, + ppt: *const POINT, + dwEffect: u32, + ) -> HRESULT, + pub Drop: unsafe extern "system" fn( + This: *mut IDropTargetHelper, + pDataObject: *mut IDataObject, + ppt: *const POINT, + dwEffect: u32, + ) -> HRESULT, + pub Show: unsafe extern "system" fn(This: *mut IDropTargetHelper, fShow: BOOL) -> HRESULT, +} + pub type IDropSource = *mut c_void; #[repr(C)] @@ -189,6 +235,22 @@ pub const IID_IEnumFORMATETC: GUID = GUID { data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], }; +// {DE5BF786-477A-11D2-839D-00C04FD918D0} +pub const IID_IDragSourceHelper: GUID = GUID { + data1: 0xde5bf786, + data2: 0x477a, + data3: 0x11d2, + data4: [0x83, 0x9d, 0x00, 0xc0, 0x4f, 0xd9, 0x18, 0xd0], +}; + +// {4657278B-411B-11D2-839A-00C04FD918D0} +pub const IID_IDropTargetHelper: GUID = GUID { + data1: 0x4657278b, + data2: 0x411b, + data3: 0x11d2, + data4: [0x83, 0x9a, 0x00, 0xc0, 0x4f, 0xd9, 0x18, 0xd0], +}; + pub const CLSID_TaskbarList: GUID = GUID { data1: 0x56fdf344, data2: 0xfd6d, diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 2763eadcb1..ff6d9e255e 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -21,7 +21,7 @@ use windows_sys::Win32::System::Memory::{ }; use windows_sys::Win32::System::Ole::{ CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, - ReleaseStgMedium, + OleDuplicateData, ReleaseStgMedium, }; use windows_sys::Win32::System::SystemServices::{ MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_XBUTTON1, MK_XBUTTON2, @@ -35,9 +35,10 @@ use winit_core::event::WindowEvent; use winit_core::event_loop::DndActions; use crate::definitions::{ - IDataObject, IDataObjectVtbl, IDropSource, IDropSourceVtbl, IDropTarget, IDropTargetVtbl, - IEnumFORMATETC, IEnumFORMATETCVtbl, IID_IDataObject, IID_IDropSource, IID_IEnumFORMATETC, - IID_IUnknown, IUnknown, IUnknownVtbl, + IDataObject, IDataObjectVtbl, IDropSource, IDropSourceVtbl, IDropTarget, IDropTargetHelper, + IDropTargetHelperVtbl, IDropTargetVtbl, IEnumFORMATETC, IEnumFORMATETCVtbl, IID_IDataObject, + IID_IDropSource, IID_IDropTargetHelper, IID_IEnumFORMATETC, IID_IUnknown, IUnknown, + IUnknownVtbl, }; use crate::event_loop::EventLoopRunner; use crate::util; @@ -266,6 +267,11 @@ pub struct FileDropHandlerData { runner: Rc, send_event: Box, active_data_transfer_id: Option, + // Shell drop-target helper. Lazy-init on first DragEnter; null means "not yet created" or + // "creation failed and we're running without a drag image". Forwarding to this is what + // makes the source's `IDragSourceHelper` bitmap actually render under the cursor over our + // own window and any other helper-aware target. + drop_target_helper: *mut IDropTargetHelper, } pub struct FileDropHandler { @@ -286,10 +292,41 @@ impl FileDropHandler { runner, send_event, active_data_transfer_id: None, + drop_target_helper: std::ptr::null_mut(), }); FileDropHandler { data: Box::into_raw(data) } } + /// Lazy-create the shell drop-target helper. Returns null if creation failed; callers should + /// treat that as "no drag image" and continue silently - failure is purely cosmetic. + unsafe fn ensure_drop_target_helper(data: &mut FileDropHandlerData) -> *mut IDropTargetHelper { + use windows_sys::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance}; + use windows_sys::Win32::UI::Shell::CLSID_DragDropHelper; + + if !data.drop_target_helper.is_null() { + return data.drop_target_helper; + } + let mut helper: *mut IDropTargetHelper = std::ptr::null_mut(); + let hr = unsafe { + CoCreateInstance( + &CLSID_DragDropHelper, + std::ptr::null_mut(), + CLSCTX_ALL, + &IID_IDropTargetHelper, + &mut helper as *mut _ as *mut _, + ) + }; + if hr < 0 { + return std::ptr::null_mut(); + } + data.drop_target_helper = helper; + helper + } + + unsafe fn helper_vtbl(helper: *mut IDropTargetHelper) -> &'static IDropTargetHelperVtbl { + unsafe { &*(*(helper as *mut *const IDropTargetHelperVtbl)) } + } + pub(crate) unsafe fn interface_unchecked_mut(&mut self) -> &mut IDropTarget { unsafe { &mut (*self.data).interface } } @@ -325,6 +362,13 @@ impl FileDropHandler { if let Some(id) = drop_handler.active_data_transfer_id.take() { drop_handler.runner.remove_data_transfer(id); } + // Release the shell drop-target helper if we created one. + if !drop_handler.drop_target_helper.is_null() { + let vtbl = unsafe { Self::helper_vtbl(drop_handler.drop_target_helper) }; + unsafe { + (vtbl.parent.Release)(drop_handler.drop_target_helper as *mut IUnknown); + } + } // Destroy the underlying data drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); } @@ -352,11 +396,12 @@ impl FileDropHandler { let data = Rc::new(unsafe { DataObject::from_idataobject(pDataObj) }); drop_handler.runner.register_data_transfer(data_transfer_id, data, initial_actions); - let mut pt = POINT { x: pt.x, y: pt.y }; + let pt_screen = POINT { x: pt.x, y: pt.y }; + let mut pt_client = pt_screen; unsafe { - ScreenToClient(drop_handler.window, &mut pt); + ScreenToClient(drop_handler.window, &mut pt_client); } - let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); + let position = PhysicalPosition::new(pt_client.x as f64, pt_client.y as f64); (drop_handler.send_event)(WindowEvent::DragEntered { id: data_transfer_id, position: Some(position), @@ -365,6 +410,22 @@ impl FileDropHandler { *pdwEffect = DROPEFFECT_NONE; } + // Forward to the shell drop-target helper so any drag image attached by the source's + // IDragSourceHelper renders the bitmap under the cursor while it's over our window. + let helper = unsafe { Self::ensure_drop_target_helper(drop_handler) }; + if !helper.is_null() { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { + (vtbl.DragEnter)( + helper, + drop_handler.window, + pDataObj as *mut IDataObject, + &pt_screen, + *pdwEffect, + ); + } + } + S_OK } @@ -386,14 +447,22 @@ impl FileDropHandler { let actions = drop_handler.runner.current_drag_actions(data_transfer_id); let source_allowed = unsafe { *pdwEffect }; - let mut pt = POINT { x: pt.x, y: pt.y }; + let pt_screen = POINT { x: pt.x, y: pt.y }; + let mut pt_client = pt_screen; unsafe { - ScreenToClient(drop_handler.window, &mut pt); + ScreenToClient(drop_handler.window, &mut pt_client); } - let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); + let position = PhysicalPosition::new(pt_client.x as f64, pt_client.y as f64); (drop_handler.send_event)(WindowEvent::DragPosition { id: data_transfer_id, position }); + let new_effect = pick_effect(actions, grfKeyState, source_allowed); unsafe { - *pdwEffect = pick_effect(actions, grfKeyState, source_allowed); + *pdwEffect = new_effect; + } + + let helper = drop_handler.drop_target_helper; + if !helper.is_null() { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { (vtbl.DragOver)(helper, &pt_screen, new_effect) }; } S_OK @@ -408,12 +477,18 @@ impl FileDropHandler { (drop_handler.send_event)(WindowEvent::DragLeft { id: data_transfer_id }); drop_handler.runner.remove_data_transfer(data_transfer_id); + let helper = drop_handler.drop_target_helper; + if !helper.is_null() { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { (vtbl.DragLeave)(helper) }; + } + S_OK } unsafe extern "system" fn Drop( this: *mut IDropTarget, - _pDataObj: *const IDataObject, + pDataObj: *const IDataObject, grfKeyState: u32, pt: POINTL, pdwEffect: *mut u32, @@ -430,10 +505,12 @@ impl FileDropHandler { let actions = drop_handler.runner.current_drag_actions(data_transfer_id); let source_allowed = unsafe { *pdwEffect }; - let mut pt = POINT { x: pt.x, y: pt.y }; + let pt_screen = POINT { x: pt.x, y: pt.y }; + let mut pt_client = pt_screen; unsafe { - ScreenToClient(drop_handler.window, &mut pt); + ScreenToClient(drop_handler.window, &mut pt_client); } + let pt = pt_client; // Negotiate the effect first so we can pick the right outgoing event. If the app // rejected the drop (e.g. via `set_valid_actions(none())`), `pick_effect` returns @@ -453,6 +530,14 @@ impl FileDropHandler { *pdwEffect = effect; } + let helper = drop_handler.drop_target_helper; + if !helper.is_null() { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { + (vtbl.Drop)(helper, pDataObj as *mut IDataObject, &pt_screen, effect); + } + } + // External drop: the app's handler dispatched synchronously above and has already read // the data; safe to release the cache. Self-drop: the handler is buffered and hasn't // run yet, so defer cleanup until after `dispatch_buffered_events` drains. @@ -586,6 +671,44 @@ fn cf_formats_for_hint(hint: TypeHint) -> Vec<(u16, TypeHint)> { } } +/// Duplicate a `STGMEDIUM` for handing out via `GetData` without losing the original. +/// +/// Delegates to `OleDuplicateData` which knows how to clone HGLOBAL, HBITMAP, HENHMETAFILE, +/// HMETAFILEPICT and file-name mediums. Returns `None` for tymeds the shell helper doesn't use +/// (interface-based mediums like IStream / IStorage) so we never hand out an aliased pointer +/// the caller would later `Release` once we also drop ours. +unsafe fn duplicate_stgmedium(src: &STGMEDIUM, cf_format: u16) -> Option { + use windows_sys::Win32::Foundation::HANDLE; + + let handle: HANDLE = unsafe { + match src.tymed { + t if t == TYMED_HGLOBAL as u32 => src.u.hGlobal as HANDLE, + // Other handle-typed tymeds; the shell drag helper doesn't currently use these but + // OleDuplicateData supports them so we'd rather forward than refuse. + 1 /* TYMED_FILE */ => src.u.lpszFileName as HANDLE, + 32 /* TYMED_GDI / HBITMAP */ => src.u.hBitmap as HANDLE, + 64 /* TYMED_MFPICT */ => src.u.hMetaFilePict as HANDLE, + 128 /* TYMED_ENHMF */ => src.u.hEnhMetaFile as HANDLE, + _ => return None, + } + }; + let dup = unsafe { OleDuplicateData(handle, cf_format, 0) }; + if dup.is_null() { + return None; + } + let mut out: STGMEDIUM = unsafe { std::mem::zeroed() }; + out.tymed = src.tymed; + match src.tymed { + t if t == TYMED_HGLOBAL as u32 => out.u.hGlobal = dup as _, + 1 => out.u.lpszFileName = dup as _, + 32 => out.u.hBitmap = dup as _, + 64 => out.u.hMetaFilePict = dup as _, + 128 => out.u.hEnhMetaFile = dup as _, + _ => unreachable!(), + } + Some(out) +} + unsafe fn alloc_hglobal_from(src: *const u8, len: usize) -> Option { let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, len) }; if hglobal.is_null() { @@ -881,6 +1004,15 @@ struct IDataObjectInterface { lpVtbl: *const IDataObjectVtbl, } +/// Owning wrapper around a `STGMEDIUM` that releases the underlying handle on drop. +struct OwnedStgMedium(STGMEDIUM); + +impl Drop for OwnedStgMedium { + fn drop(&mut self) { + unsafe { ReleaseStgMedium(&mut self.0) }; + } +} + #[repr(C)] struct SourceDataObjectData { interface: IDataObjectInterface, @@ -888,6 +1020,10 @@ struct SourceDataObjectData { send_data: RefCell>, // (cf_format, hint) pairs we advertise to the target. formats: Vec<(u16, TypeHint)>, + // Formats injected via `SetData` - primarily by `IDragSourceHelper::InitializeFromBitmap`, + // which stores the drag image bits (CFSTR_DRAGIMAGEBITS) and related shell formats here so + // the target-side `IDropTargetHelper` can read them back via `GetData`. + extras: RefCell>, } com_iunknown_impl!(SourceDataObjectData, &IID_IDataObject); @@ -914,6 +1050,7 @@ impl SourceDataObjectData { refcount: AtomicUsize::new(1), send_data: RefCell::new(send_data), formats, + extras: RefCell::new(Vec::new()), })) } @@ -927,6 +1064,21 @@ impl SourceDataObjectData { if (format.tymed & TYMED_HGLOBAL as u32) == 0 { return DV_E_FORMATETC; } + // Shell-helper-injected formats live in `extras`; serve those first by duplicating the + // stored HGLOBAL so the caller can `ReleaseStgMedium` independently of our storage. + if let Ok(extras) = me.extras.try_borrow() { + if let Some((stored_fmt, stored)) = + extras.iter().find(|(f, _)| f.cfFormat == format.cfFormat) + { + if (stored_fmt.tymed & format.tymed) != 0 { + if let Some(dup) = unsafe { duplicate_stgmedium(&stored.0, format.cfFormat) } { + unsafe { *pmedium = dup }; + return S_OK; + } + return E_FAIL; + } + } + } let Some(&(_, hint)) = me.formats.iter().find(|&&(cf, _)| cf == format.cfFormat) else { return DV_E_FORMATETC; }; @@ -969,7 +1121,15 @@ impl SourceDataObjectData { if (format.tymed & TYMED_HGLOBAL as u32) == 0 { return DV_E_FORMATETC; } - if me.formats.iter().any(|&(cf, _)| cf == format.cfFormat) { S_OK } else { S_FALSE } + if me.formats.iter().any(|&(cf, _)| cf == format.cfFormat) { + return S_OK; + } + if let Ok(extras) = me.extras.try_borrow() { + if extras.iter().any(|(f, _)| f.cfFormat == format.cfFormat) { + return S_OK; + } + } + S_FALSE } unsafe extern "system" fn GetCanonicalFormatEtc( @@ -981,12 +1141,47 @@ impl SourceDataObjectData { } unsafe extern "system" fn SetData( - _this: *mut IDataObject, - _pformatetc: *const FORMATETC, - _pformatetc_out: *const FORMATETC, - _f_release: BOOL, + this: *mut IDataObject, + pformatetc: *const FORMATETC, + pmedium: *const STGMEDIUM, + f_release: BOOL, ) -> HRESULT { - E_NOTIMPL + // Primary caller is `IDragSourceHelper::InitializeFromBitmap`, which attaches the drag + // image bits and related shell formats to our data object. We don't interpret them - + // just hold them so `GetData` can hand them back to `IDropTargetHelper`. + let me = unsafe { Self::from_interface(this) }; + if pformatetc.is_null() || pmedium.is_null() { + return E_FAIL; + } + let format = unsafe { *pformatetc }; + let medium = if f_release != 0 { + // We take ownership of the passed-in medium as-is. + unsafe { *pmedium } + } else { + // Caller retains ownership; we must duplicate. + let Some(dup) = (unsafe { duplicate_stgmedium(&*pmedium, format.cfFormat) }) else { + return E_FAIL; + }; + dup + }; + let Ok(mut extras) = me.extras.try_borrow_mut() else { + if f_release != 0 { + // We promised to take ownership but can't store - release immediately so we + // don't leak. + let mut m = medium; + unsafe { ReleaseStgMedium(&mut m) }; + } + return E_UNEXPECTED; + }; + // Replace any earlier entry with the same format - last-write-wins matches what real + // shell apps do and avoids growing the vec unboundedly on repeated SetData calls. + if let Some(slot) = extras.iter_mut().find(|(f, _)| f.cfFormat == format.cfFormat) { + slot.0 = format; + slot.1 = OwnedStgMedium(medium); + } else { + extras.push((format, OwnedStgMedium(medium))); + } + S_OK } unsafe extern "system" fn EnumFormatEtc( @@ -999,7 +1194,7 @@ impl SourceDataObjectData { return E_NOTIMPL; } let me = unsafe { Self::from_interface(this) }; - let formats: Vec = me + let mut formats: Vec = me .formats .iter() .map(|&(cf, _)| FORMATETC { @@ -1010,6 +1205,13 @@ impl SourceDataObjectData { tymed: TYMED_HGLOBAL as u32, }) .collect(); + if let Ok(extras) = me.extras.try_borrow() { + for (fmt, _) in extras.iter() { + if !formats.iter().any(|f| f.cfFormat == fmt.cfFormat) { + formats.push(*fmt); + } + } + } let enumerator = SourceFormatEnumerator::new_boxed(formats); unsafe { *ppenum = enumerator as *mut IEnumFORMATETC }; S_OK @@ -1154,6 +1356,122 @@ impl Drop for DropSource { } } +/// Attach a drag image to `data_object` so the shell renders the app's icon under the cursor +/// during the drag instead of the default no-image cursor. +/// +/// `rgba` is the icon's pixel buffer in straight RGBA8 with `width * height * 4` bytes; +/// `hot_offset` is the icon-relative offset of the cursor hot spot (cross-platform sign: +/// `(-w/2, -h/2)` centres the icon on the cursor). +/// +/// Returns `Ok(())` on success. On failure the caller's data object is left untouched and the +/// drag still runs - just without a custom image. We never propagate the error: a missing drag +/// preview is purely cosmetic and shouldn't fail the whole `start_drag`. +pub(crate) unsafe fn apply_drag_image( + data_object: *mut IDataObject, + width: u32, + height: u32, + rgba: &[u8], + hot_offset: dpi::PhysicalPosition, +) -> Result<(), HRESULT> { + use windows_sys::Win32::Graphics::Gdi::{ + BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateDIBSection, DIB_RGB_COLORS, DeleteObject, HDC, + }; + use windows_sys::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance}; + use windows_sys::Win32::UI::Shell::{CLSID_DragDropHelper, SHDRAGIMAGE}; + + use crate::definitions::{IDragSourceHelper, IDragSourceHelperVtbl, IID_IDragSourceHelper}; + + if width == 0 || height == 0 || rgba.len() != (width as usize) * (height as usize) * 4 { + return Err(E_FAIL); + } + + // Build a top-down 32bpp BGRA DIB. Top-down means negative biHeight; the shell helper + // expects BGRA byte order (B, G, R, A) with premultiplied alpha for smooth compositing. + let mut header: BITMAPINFO = unsafe { std::mem::zeroed() }; + header.bmiHeader = BITMAPINFOHEADER { + biSize: std::mem::size_of::() as u32, + biWidth: width as i32, + biHeight: -(height as i32), + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB, + biSizeImage: width * height * 4, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }; + + let mut bits_ptr: *mut c_void = std::ptr::null_mut(); + let hbitmap = unsafe { + CreateDIBSection( + std::ptr::null_mut::() as HDC, + &header, + DIB_RGB_COLORS, + &mut bits_ptr, + std::ptr::null_mut(), + 0, + ) + }; + if hbitmap.is_null() || bits_ptr.is_null() { + return Err(E_FAIL); + } + + // Copy RGBA -> premultiplied BGRA so the shell can render the image with smooth alpha + // edges. Without premultiplication, the cursor preview shows a halo on every translucent + // pixel. + let dst = unsafe { std::slice::from_raw_parts_mut(bits_ptr as *mut u8, rgba.len()) }; + for (src_px, dst_px) in rgba.chunks_exact(4).zip(dst.chunks_exact_mut(4)) { + let (r, g, b, a) = (src_px[0] as u32, src_px[1] as u32, src_px[2] as u32, src_px[3]); + dst_px[0] = ((b * a as u32) / 255) as u8; + dst_px[1] = ((g * a as u32) / 255) as u8; + dst_px[2] = ((r * a as u32) / 255) as u8; + dst_px[3] = a; + } + + let mut helper: *mut IDragSourceHelper = std::ptr::null_mut(); + let hr = unsafe { + CoCreateInstance( + &CLSID_DragDropHelper, + std::ptr::null_mut(), + CLSCTX_ALL, + &IID_IDragSourceHelper, + &mut helper as *mut _ as *mut _, + ) + }; + if hr < 0 || helper.is_null() { + unsafe { DeleteObject(hbitmap as _) }; + return Err(hr); + } + + // The helper API takes `ptOffset` as the cursor's position inside the image (positive + // values into the image), but cross-platform `DragIcon::offset` is the icon-relative + // offset where the cursor sits (negative values mean the icon extends up/left of the + // cursor). Negate to translate between the two conventions. + let sdi = SHDRAGIMAGE { + sizeDragImage: windows_sys::Win32::Foundation::SIZE { cx: width as i32, cy: height as i32 }, + ptOffset: POINT { x: -hot_offset.x, y: -hot_offset.y }, + hbmpDragImage: hbitmap, + crColorKey: 0xffff_ffff, // CLR_NONE - use the alpha channel + }; + + let vtbl = unsafe { + &*((*(helper as *mut *mut IDragSourceHelperVtbl)) as *const IDragSourceHelperVtbl) + }; + let init_hr = unsafe { (vtbl.InitializeFromBitmap)(helper, &sdi, data_object) }; + let release = vtbl.parent.Release; + unsafe { release(helper as *mut IUnknown) }; + + if init_hr < 0 { + // On failure the helper did not take ownership of the bitmap - we still own it. + unsafe { DeleteObject(hbitmap as _) }; + return Err(init_hr); + } + // On success the helper stores the bitmap on the data object and will delete it later; + // do NOT call DeleteObject here. + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 251b7f7dd3..54b565e0fa 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -525,7 +525,7 @@ impl RootActiveEventLoop for ActiveEventLoop { _source: WindowId, send_data: Box, action_mask: &dyn DndActionMask, - _icon: Option, + icon: Option, ) -> Result { let allowed_actions = action_mask.hint(); let allowed_effects = crate::dnd::actions_to_dropeffect_mask(allowed_actions); @@ -544,6 +544,27 @@ impl RootActiveEventLoop for ActiveEventLoop { let data_object = SourceDataObject::new(send_data); let drop_source = DropSource::new(); + // Attach a drag preview if the app supplied one. Cosmetic failures must not abort the + // drag - the gesture still works, just without a custom image - so log and move on. + if let Some(icon) = icon { + if let Some(rgba) = icon.icon.cast_ref::() { + let result = unsafe { + crate::dnd::apply_drag_image( + data_object.interface_ptr() as *mut _, + rgba.width(), + rgba.height(), + rgba.buffer(), + icon.offset, + ) + }; + if let Err(hr) = result { + tracing::warn!("Failed to attach drag image: hr=0x{hr:08x}"); + } + } else { + tracing::warn!("DragIcon::icon must be an RgbaIcon on win32; ignoring"); + } + } + // Make the drag visible to our own target-side `IDropTarget` so it can recognize // self-drops and reuse this id + action mask without going through the (buffered) app // handler. The guard ensures the flag is cleared on any exit path - if anything between From 2c6d5c5e72a0521882030122f5f5508204c5359b Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 11:50:37 +0200 Subject: [PATCH 74/87] fixup! Implement DragIcon on win32 --- winit-win32/src/definitions.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index 801fc41533..a07b0dc5e1 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -206,7 +206,7 @@ pub struct ITaskbarList2 { pub lpVtbl: *const ITaskbarList2Vtbl, } -// Well-known COM IIDs. Values from `unknwn.h`, `objidl.h`, `oleidl.h`. +/// Defined in `unknwn.h`. pub const IID_IUnknown: GUID = GUID { data1: 0x00000000, data2: 0x0000, @@ -214,6 +214,7 @@ pub const IID_IUnknown: GUID = GUID { data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], }; +/// Defined in `objidl.h`. pub const IID_IDataObject: GUID = GUID { data1: 0x0000010e, data2: 0x0000, @@ -221,6 +222,7 @@ pub const IID_IDataObject: GUID = GUID { data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], }; +/// Defined in `oleidl.h`. pub const IID_IDropSource: GUID = GUID { data1: 0x00000121, data2: 0x0000, @@ -228,6 +230,7 @@ pub const IID_IDropSource: GUID = GUID { data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], }; +/// Defined in `objidl.h`. pub const IID_IEnumFORMATETC: GUID = GUID { data1: 0x00000103, data2: 0x0000, @@ -235,7 +238,7 @@ pub const IID_IEnumFORMATETC: GUID = GUID { data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], }; -// {DE5BF786-477A-11D2-839D-00C04FD918D0} +/// Defined in `shobjidl_core.h`. `{DE5BF786-477A-11D2-839D-00C04FD918D0}`. pub const IID_IDragSourceHelper: GUID = GUID { data1: 0xde5bf786, data2: 0x477a, @@ -243,7 +246,7 @@ pub const IID_IDragSourceHelper: GUID = GUID { data4: [0x83, 0x9d, 0x00, 0xc0, 0x4f, 0xd9, 0x18, 0xd0], }; -// {4657278B-411B-11D2-839A-00C04FD918D0} +/// Defined in `shobjidl_core.h`. `{4657278B-411B-11D2-839A-00C04FD918D0}`. pub const IID_IDropTargetHelper: GUID = GUID { data1: 0x4657278b, data2: 0x411b, @@ -251,6 +254,7 @@ pub const IID_IDropTargetHelper: GUID = GUID { data4: [0x83, 0x9a, 0x00, 0xc0, 0x4f, 0xd9, 0x18, 0xd0], }; +/// Defined in `shobjidl_core.h`. pub const CLSID_TaskbarList: GUID = GUID { data1: 0x56fdf344, data2: 0xfd6d, @@ -258,6 +262,7 @@ pub const CLSID_TaskbarList: GUID = GUID { data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], }; +/// Defined in `shobjidl_core.h`. pub const IID_ITaskbarList: GUID = GUID { data1: 0x56fdf342, data2: 0xfd6d, @@ -265,6 +270,7 @@ pub const IID_ITaskbarList: GUID = GUID { data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], }; +/// Defined in `shobjidl_core.h`. pub const IID_ITaskbarList2: GUID = GUID { data1: 0x602d4995, data2: 0xb13a, From e4f341d2a835d1d64bde07cd28604913ded48479 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 13:03:34 +0200 Subject: [PATCH 75/87] fixup! Implement DragIcon on win32 The TYMED branch values in `duplicate_stgmedium` were each off by one bit shift: TYMED_FILE was 1 (= HGLOBAL) instead of 2, GDI was 32 instead of 16, MFPICT 64 instead of 32, ENHMF 128 instead of 64. Latent because the shell drag helper only uses HGLOBAL, but a HANDLE-bearing SetData of any other tymed would have either fallen through or aliased the wrong union arm. Use the named TYMED_* constants from windows-sys so we can't drift again. --- winit-win32/src/dnd.rs | 50 ++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index ff6d9e255e..c69a004bc5 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -14,7 +14,10 @@ use windows_sys::Win32::Foundation::{ OLE_E_ADVISENOTSUPPORTED, POINT, POINTL, S_FALSE, S_OK, }; use windows_sys::Win32::Graphics::Gdi::ScreenToClient; -use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_HGLOBAL}; +use windows_sys::Win32::System::Com::{ + DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_ENHMF, TYMED_FILE, TYMED_GDI, TYMED_HGLOBAL, + TYMED_MFPICT, +}; use windows_sys::Win32::System::DataExchange::RegisterClipboardFormatW; use windows_sys::Win32::System::Memory::{ GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, @@ -680,16 +683,24 @@ fn cf_formats_for_hint(hint: TypeHint) -> Vec<(u16, TypeHint)> { unsafe fn duplicate_stgmedium(src: &STGMEDIUM, cf_format: u16) -> Option { use windows_sys::Win32::Foundation::HANDLE; + // Pull the handle out of the union by tymed. The shell drag helper only uses HGLOBAL today, + // but forwarding every handle-typed tymed `OleDuplicateData` understands is essentially + // free, so do it. Interface-based tymeds (IStream / IStorage) deliberately fall through - + // duplicating them properly requires AddRef, not OleDuplicateData. + let tymed = src.tymed as i32; let handle: HANDLE = unsafe { - match src.tymed { - t if t == TYMED_HGLOBAL as u32 => src.u.hGlobal as HANDLE, - // Other handle-typed tymeds; the shell drag helper doesn't currently use these but - // OleDuplicateData supports them so we'd rather forward than refuse. - 1 /* TYMED_FILE */ => src.u.lpszFileName as HANDLE, - 32 /* TYMED_GDI / HBITMAP */ => src.u.hBitmap as HANDLE, - 64 /* TYMED_MFPICT */ => src.u.hMetaFilePict as HANDLE, - 128 /* TYMED_ENHMF */ => src.u.hEnhMetaFile as HANDLE, - _ => return None, + if tymed == TYMED_HGLOBAL { + src.u.hGlobal as HANDLE + } else if tymed == TYMED_FILE { + src.u.lpszFileName as HANDLE + } else if tymed == TYMED_GDI { + src.u.hBitmap as HANDLE + } else if tymed == TYMED_MFPICT { + src.u.hMetaFilePict as HANDLE + } else if tymed == TYMED_ENHMF { + src.u.hEnhMetaFile as HANDLE + } else { + return None; } }; let dup = unsafe { OleDuplicateData(handle, cf_format, 0) }; @@ -698,13 +709,18 @@ unsafe fn duplicate_stgmedium(src: &STGMEDIUM, cf_format: u16) -> Option out.u.hGlobal = dup as _, - 1 => out.u.lpszFileName = dup as _, - 32 => out.u.hBitmap = dup as _, - 64 => out.u.hMetaFilePict = dup as _, - 128 => out.u.hEnhMetaFile = dup as _, - _ => unreachable!(), + if tymed == TYMED_HGLOBAL { + out.u.hGlobal = dup as _; + } else if tymed == TYMED_FILE { + out.u.lpszFileName = dup as _; + } else if tymed == TYMED_GDI { + out.u.hBitmap = dup as _; + } else if tymed == TYMED_MFPICT { + out.u.hMetaFilePict = dup as _; + } else if tymed == TYMED_ENHMF { + out.u.hEnhMetaFile = dup as _; + } else { + unreachable!(); } Some(out) } From 463d72ffc94ae17105e8fe098e65ee05abcad775 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 13:04:18 +0200 Subject: [PATCH 76/87] fixup! Implement DragIcon on win32 --- winit-win32/src/dnd.rs | 56 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index c69a004bc5..e6cc4fd8e9 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -4,6 +4,7 @@ use std::ffi::{OsString, c_void}; use std::io; use std::ops::ControlFlow; use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::ptr::NonNull; use std::rc::Rc; use std::sync::atomic::{self, AtomicI64, AtomicUsize, Ordering}; @@ -270,11 +271,11 @@ pub struct FileDropHandlerData { runner: Rc, send_event: Box, active_data_transfer_id: Option, - // Shell drop-target helper. Lazy-init on first DragEnter; null means "not yet created" or + // Shell drop-target helper. Lazy-init on first DragEnter; `None` means "not yet created" or // "creation failed and we're running without a drag image". Forwarding to this is what // makes the source's `IDragSourceHelper` bitmap actually render under the cursor over our // own window and any other helper-aware target. - drop_target_helper: *mut IDropTargetHelper, + drop_target_helper: Option>, } pub struct FileDropHandler { @@ -295,19 +296,21 @@ impl FileDropHandler { runner, send_event, active_data_transfer_id: None, - drop_target_helper: std::ptr::null_mut(), + drop_target_helper: None, }); FileDropHandler { data: Box::into_raw(data) } } - /// Lazy-create the shell drop-target helper. Returns null if creation failed; callers should - /// treat that as "no drag image" and continue silently - failure is purely cosmetic. - unsafe fn ensure_drop_target_helper(data: &mut FileDropHandlerData) -> *mut IDropTargetHelper { + /// Lazy-create the shell drop-target helper. Returns `None` if creation failed; callers + /// should treat that as "no drag image" and continue silently - failure is purely cosmetic. + unsafe fn ensure_drop_target_helper( + data: &mut FileDropHandlerData, + ) -> Option> { use windows_sys::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance}; use windows_sys::Win32::UI::Shell::CLSID_DragDropHelper; - if !data.drop_target_helper.is_null() { - return data.drop_target_helper; + if let Some(helper) = data.drop_target_helper { + return Some(helper); } let mut helper: *mut IDropTargetHelper = std::ptr::null_mut(); let hr = unsafe { @@ -320,14 +323,15 @@ impl FileDropHandler { ) }; if hr < 0 { - return std::ptr::null_mut(); + return None; } - data.drop_target_helper = helper; - helper + let helper = NonNull::new(helper)?; + data.drop_target_helper = Some(helper); + Some(helper) } - unsafe fn helper_vtbl(helper: *mut IDropTargetHelper) -> &'static IDropTargetHelperVtbl { - unsafe { &*(*(helper as *mut *const IDropTargetHelperVtbl)) } + unsafe fn helper_vtbl(helper: NonNull) -> &'static IDropTargetHelperVtbl { + unsafe { &*(*(helper.as_ptr() as *mut *const IDropTargetHelperVtbl)) } } pub(crate) unsafe fn interface_unchecked_mut(&mut self) -> &mut IDropTarget { @@ -366,10 +370,10 @@ impl FileDropHandler { drop_handler.runner.remove_data_transfer(id); } // Release the shell drop-target helper if we created one. - if !drop_handler.drop_target_helper.is_null() { - let vtbl = unsafe { Self::helper_vtbl(drop_handler.drop_target_helper) }; + if let Some(helper) = drop_handler.drop_target_helper { + let vtbl = unsafe { Self::helper_vtbl(helper) }; unsafe { - (vtbl.parent.Release)(drop_handler.drop_target_helper as *mut IUnknown); + (vtbl.parent.Release)(helper.as_ptr() as *mut IUnknown); } } // Destroy the underlying data @@ -415,12 +419,11 @@ impl FileDropHandler { // Forward to the shell drop-target helper so any drag image attached by the source's // IDragSourceHelper renders the bitmap under the cursor while it's over our window. - let helper = unsafe { Self::ensure_drop_target_helper(drop_handler) }; - if !helper.is_null() { + if let Some(helper) = unsafe { Self::ensure_drop_target_helper(drop_handler) } { let vtbl = unsafe { Self::helper_vtbl(helper) }; unsafe { (vtbl.DragEnter)( - helper, + helper.as_ptr(), drop_handler.window, pDataObj as *mut IDataObject, &pt_screen, @@ -462,10 +465,9 @@ impl FileDropHandler { *pdwEffect = new_effect; } - let helper = drop_handler.drop_target_helper; - if !helper.is_null() { + if let Some(helper) = drop_handler.drop_target_helper { let vtbl = unsafe { Self::helper_vtbl(helper) }; - unsafe { (vtbl.DragOver)(helper, &pt_screen, new_effect) }; + unsafe { (vtbl.DragOver)(helper.as_ptr(), &pt_screen, new_effect) }; } S_OK @@ -480,10 +482,9 @@ impl FileDropHandler { (drop_handler.send_event)(WindowEvent::DragLeft { id: data_transfer_id }); drop_handler.runner.remove_data_transfer(data_transfer_id); - let helper = drop_handler.drop_target_helper; - if !helper.is_null() { + if let Some(helper) = drop_handler.drop_target_helper { let vtbl = unsafe { Self::helper_vtbl(helper) }; - unsafe { (vtbl.DragLeave)(helper) }; + unsafe { (vtbl.DragLeave)(helper.as_ptr()) }; } S_OK @@ -533,11 +534,10 @@ impl FileDropHandler { *pdwEffect = effect; } - let helper = drop_handler.drop_target_helper; - if !helper.is_null() { + if let Some(helper) = drop_handler.drop_target_helper { let vtbl = unsafe { Self::helper_vtbl(helper) }; unsafe { - (vtbl.Drop)(helper, pDataObj as *mut IDataObject, &pt_screen, effect); + (vtbl.Drop)(helper.as_ptr(), pDataObj as *mut IDataObject, &pt_screen, effect); } } From f0589574be5f13d68f26d496db49a2a7179e72f8 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 13:16:26 +0200 Subject: [PATCH 77/87] fixup! Pair COM Release with an Acquire fence before drop --- winit-win32/src/dnd.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index e6cc4fd8e9..25485a80db 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -359,8 +359,11 @@ impl FileDropHandler { unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { let drop_handler = unsafe { Self::from_interface(this) }; - // See the SourceDataObject Release for why we use Release on decrement and an - // Acquire fence on the zero-transition (standard Arc pattern). + // Release on decrement publishes any writes made through this reference before the + // count is observed by other threads. When we hit zero, fence with Acquire so the + // destructor sees all writes from prior Releases on other threads - the standard + // Arc pattern. Without the fence, dropping the box could race with reads done by + // the last releasing thread on another core. let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; if count == 0 { atomic::fence(Ordering::Acquire); From bf5bcc10ff4572e80d0a181bec627c47634d5281 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 13:43:05 +0200 Subject: [PATCH 78/87] fixup! Implement DragIcon on win32 --- winit-win32/src/dnd.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 25485a80db..d30c08ade1 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -1379,8 +1379,10 @@ impl Drop for DropSource { /// during the drag instead of the default no-image cursor. /// /// `rgba` is the icon's pixel buffer in straight RGBA8 with `width * height * 4` bytes; -/// `hot_offset` is the icon-relative offset of the cursor hot spot (cross-platform sign: -/// `(-w/2, -h/2)` centres the icon on the cursor). +/// `offset` matches `DragIcon::offset` - `(0, 0)` sits the cursor at the icon's top-left, +/// `(-w/2, -h/2)` centres the icon on the cursor. Windows expresses this as the offset from +/// the icon's upper-left to the cursor hot spot (sign inverted), so we negate when filling +/// in `SHDRAGIMAGE::ptOffset`. /// /// Returns `Ok(())` on success. On failure the caller's data object is left untouched and the /// drag still runs - just without a custom image. We never propagate the error: a missing drag @@ -1390,7 +1392,7 @@ pub(crate) unsafe fn apply_drag_image( width: u32, height: u32, rgba: &[u8], - hot_offset: dpi::PhysicalPosition, + offset: dpi::PhysicalPosition, ) -> Result<(), HRESULT> { use windows_sys::Win32::Graphics::Gdi::{ BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateDIBSection, DIB_RGB_COLORS, DeleteObject, HDC, @@ -1469,7 +1471,7 @@ pub(crate) unsafe fn apply_drag_image( // cursor). Negate to translate between the two conventions. let sdi = SHDRAGIMAGE { sizeDragImage: windows_sys::Win32::Foundation::SIZE { cx: width as i32, cy: height as i32 }, - ptOffset: POINT { x: -hot_offset.x, y: -hot_offset.y }, + ptOffset: POINT { x: -offset.x, y: -offset.y }, hbmpDragImage: hbitmap, crColorKey: 0xffff_ffff, // CLR_NONE - use the alpha channel }; From e35894d067d3f5ee7e74db9fc13c3f134d51ea6b Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 13:44:19 +0200 Subject: [PATCH 79/87] fixup! Initiate drags from win32 windows --- winit-win32/src/event_loop.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 54b565e0fa..76adf8fbf1 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -534,10 +534,9 @@ impl RootActiveEventLoop for ActiveEventLoop { // after burning a full modal pump. Fail fast instead - the caller asked for a drag // they explicitly refuse to allow. if allowed_effects == 0 { - return Err(NotSupportedError::new( - "start_drag called with an empty action mask (DndActions::none())", - ) - .into()); + return Err( + NotSupportedError::new("start_drag called with an empty action mask").into() + ); } let id = crate::dnd::next_data_transfer_id(); From b8ebe98f5706a7fa8623b8b8c940f7ff0ecdf74c Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 14:11:40 +0200 Subject: [PATCH 80/87] fixup! Initiate drags from win32 windows --- winit-win32/src/definitions.rs | 8 -------- winit-win32/src/dnd.rs | 5 ++--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index a07b0dc5e1..d68aabf52a 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -206,14 +206,6 @@ pub struct ITaskbarList2 { pub lpVtbl: *const ITaskbarList2Vtbl, } -/// Defined in `unknwn.h`. -pub const IID_IUnknown: GUID = GUID { - data1: 0x00000000, - data2: 0x0000, - data3: 0x0000, - data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], -}; - /// Defined in `objidl.h`. pub const IID_IDataObject: GUID = GUID { data1: 0x0000010e, diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index d30c08ade1..77484bfa69 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -31,7 +31,7 @@ use windows_sys::Win32::System::SystemServices::{ MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_XBUTTON1, MK_XBUTTON2, }; use windows_sys::Win32::UI::Shell::{DROPFILES, DragQueryFileW, HDROP}; -use windows_sys::core::{BOOL, GUID, HRESULT}; +use windows_sys::core::{BOOL, GUID, HRESULT, IID_IUnknown}; use winit_core::data_transfer::{ DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, }; @@ -41,8 +41,7 @@ use winit_core::event_loop::DndActions; use crate::definitions::{ IDataObject, IDataObjectVtbl, IDropSource, IDropSourceVtbl, IDropTarget, IDropTargetHelper, IDropTargetHelperVtbl, IDropTargetVtbl, IEnumFORMATETC, IEnumFORMATETCVtbl, IID_IDataObject, - IID_IDropSource, IID_IDropTargetHelper, IID_IEnumFORMATETC, IID_IUnknown, IUnknown, - IUnknownVtbl, + IID_IDropSource, IID_IDropTargetHelper, IID_IEnumFORMATETC, IUnknown, IUnknownVtbl, }; use crate::event_loop::EventLoopRunner; use crate::util; From 0b5b9b30d7ff6b8be7891099794c0c17ef5849e6 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 14:12:23 +0200 Subject: [PATCH 81/87] fixup! Initiate drags from win32 windows --- winit-win32/src/definitions.rs | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index d68aabf52a..0287ead7ad 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -207,28 +207,13 @@ pub struct ITaskbarList2 { } /// Defined in `objidl.h`. -pub const IID_IDataObject: GUID = GUID { - data1: 0x0000010e, - data2: 0x0000, - data3: 0x0000, - data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], -}; +pub const IID_IDataObject: GUID = GUID::from_u128(0x0000010e_0000_0000_c000_000000000046); /// Defined in `oleidl.h`. -pub const IID_IDropSource: GUID = GUID { - data1: 0x00000121, - data2: 0x0000, - data3: 0x0000, - data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], -}; +pub const IID_IDropSource: GUID = GUID::from_u128(0x00000121_0000_0000_c000_000000000046); /// Defined in `objidl.h`. -pub const IID_IEnumFORMATETC: GUID = GUID { - data1: 0x00000103, - data2: 0x0000, - data3: 0x0000, - data4: [0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46], -}; +pub const IID_IEnumFORMATETC: GUID = GUID::from_u128(0x00000103_0000_0000_c000_000000000046); /// Defined in `shobjidl_core.h`. `{DE5BF786-477A-11D2-839D-00C04FD918D0}`. pub const IID_IDragSourceHelper: GUID = GUID { From d8b2babdf422c0ac6f091ac576f8d6838b85ae7e Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 14:13:20 +0200 Subject: [PATCH 82/87] fixup! Implement DragIcon on win32 --- winit-win32/src/definitions.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index 0287ead7ad..9bf87f4b8f 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -215,21 +215,11 @@ pub const IID_IDropSource: GUID = GUID::from_u128(0x00000121_0000_0000_c000_0000 /// Defined in `objidl.h`. pub const IID_IEnumFORMATETC: GUID = GUID::from_u128(0x00000103_0000_0000_c000_000000000046); -/// Defined in `shobjidl_core.h`. `{DE5BF786-477A-11D2-839D-00C04FD918D0}`. -pub const IID_IDragSourceHelper: GUID = GUID { - data1: 0xde5bf786, - data2: 0x477a, - data3: 0x11d2, - data4: [0x83, 0x9d, 0x00, 0xc0, 0x4f, 0xd9, 0x18, 0xd0], -}; +/// Defined in `shobjidl_core.h`. +pub const IID_IDragSourceHelper: GUID = GUID::from_u128(0xde5bf786_477a_11d2_839d_00c04fd918d0); -/// Defined in `shobjidl_core.h`. `{4657278B-411B-11D2-839A-00C04FD918D0}`. -pub const IID_IDropTargetHelper: GUID = GUID { - data1: 0x4657278b, - data2: 0x411b, - data3: 0x11d2, - data4: [0x83, 0x9a, 0x00, 0xc0, 0x4f, 0xd9, 0x18, 0xd0], -}; +/// Defined in `shobjidl_core.h`. +pub const IID_IDropTargetHelper: GUID = GUID::from_u128(0x4657278b_411b_11d2_839a_00c04fd918d0); /// Defined in `shobjidl_core.h`. pub const CLSID_TaskbarList: GUID = GUID { From e1293a16533566c1d61b0308890140b146cd771b Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 4 Jun 2026 17:28:04 +0200 Subject: [PATCH 83/87] Plaintext is UTF-8 on Wayland --- winit-wayland/src/dnd.rs | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs index 2f99029309..a7d7124def 100644 --- a/winit-wayland/src/dnd.rs +++ b/winit-wayland/src/dnd.rs @@ -89,19 +89,17 @@ impl DataSourceHandler for WinitState { } } }, - SendData::String(str) => { - match mime.parse_charset().or(mime.default_charset()).unwrap_or_default() { - Charset::Utf8 => { - let _ = fd.write_all(str.as_bytes()); - }, - Charset::Utf16 => { - let utf16_binary = str - .encode_utf16() - .flat_map(|uint16| uint16.to_ne_bytes()) - .collect::>(); - let _ = fd.write_all(&utf16_binary); - }, - } + SendData::String(str) => match mime.parse_charset().unwrap_or(mime.default_charset()) { + Charset::Utf8 => { + let _ = fd.write_all(str.as_bytes()); + }, + Charset::Utf16 => { + let utf16_binary = str + .encode_utf16() + .flat_map(|uint16| uint16.to_ne_bytes()) + .collect::>(); + let _ = fd.write_all(&utf16_binary); + }, }, SendData::Bytes(binary) => { let _ = fd.write_all(&binary); @@ -270,11 +268,10 @@ impl MimeType { } } - fn default_charset(&self) -> Option { - match self.hint? { - TypeHint::Plaintext => Some(Charset::Utf8), - TypeHint::Html => Some(Charset::Utf16), - _ => None, + fn default_charset(&self) -> Charset { + match self.hint { + Some(TypeHint::Html) => Charset::Utf16, + _ => Charset::Utf8, } } @@ -389,8 +386,7 @@ impl TypedData for MimeData { )); }; - // Default charset is UTF-16 for some reason - let charset = self.mime_type.parse_charset().unwrap_or(Charset::Utf16); + let charset = self.mime_type.parse_charset().unwrap_or(self.mime_type.default_charset()); match charset { Charset::Utf8 => { From 2884d7449d0382e487b7be7f4de2c4f06e070f63 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 18:03:08 +0200 Subject: [PATCH 84/87] fixup! Initiate drags from win32 windows --- winit-win32/src/dnd.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 77484bfa69..def59ede6f 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -727,8 +727,8 @@ unsafe fn duplicate_stgmedium(src: &STGMEDIUM, cf_format: u16) -> Option Option { - let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, len) }; +fn alloc_hglobal_from(src: &[u8]) -> Option { + let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, src.len()) }; if hglobal.is_null() { return None; } @@ -739,7 +739,7 @@ unsafe fn alloc_hglobal_from(src: *const u8, len: usize) -> Option { unsafe { GlobalFree(hglobal) }; return None; } - unsafe { std::ptr::copy_nonoverlapping(src, dst as *mut u8, len) }; + unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), dst as *mut u8, src.len()) }; unsafe { GlobalUnlock(hglobal) }; Some(hglobal) } @@ -802,12 +802,15 @@ unsafe fn send_data_to_stgmedium(data: SendData, hint: TypeHint) -> Option { // UTF-16 + NUL - used for `CF_UNICODETEXT` and other text-ish registered formats. let utf16 = util::encode_wide(&s); - unsafe { alloc_hglobal_from(utf16.as_ptr() as *const u8, utf16.len() * 2) }? + let utf16_bytes = unsafe { + std::slice::from_raw_parts(utf16.as_ptr() as *const u8, utf16.len() * 2) + }; + alloc_hglobal_from(utf16_bytes)? }, SendData::Uris(paths) => { // CF_HDROP: `DROPFILES` header + double-NUL-terminated UTF-16 path list. @@ -845,7 +848,7 @@ unsafe fn send_data_to_stgmedium(data: SendData, hint: TypeHint) -> Option unsafe { alloc_hglobal_from(b.as_ptr(), b.len()) }?, + SendData::Bytes(b) => alloc_hglobal_from(&b)?, }; let mut medium = unsafe { std::mem::zeroed::() }; From 58ca72b11a779e63ffb01a1add6e4129670c86d0 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 4 Jun 2026 18:06:26 +0200 Subject: [PATCH 85/87] fixup! Implement DragIcon on win32 --- winit-win32/src/dnd.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index def59ede6f..69b7d334e8 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -722,7 +722,10 @@ unsafe fn duplicate_stgmedium(src: &STGMEDIUM, cf_format: u16) -> Option Date: Thu, 4 Jun 2026 18:20:36 +0200 Subject: [PATCH 86/87] Fix dragging file paths on Windows --- winit-win32/src/dnd.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index 69b7d334e8..a032accfbf 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -818,7 +818,23 @@ unsafe fn send_data_to_stgmedium(data: SendData, hint: TypeHint) -> Option { // CF_HDROP: `DROPFILES` header + double-NUL-terminated UTF-16 path list. let mut wide: Vec = Vec::new(); - for path in &paths { + for path in paths { + let path = 'uri_to_path: { + if let Some(path_str) = path.to_str() { + // There's no `strip_prefix` etc on `OsStr` so we need to go via `str` + // Windows is the only platform that sends raw file paths instead of URIs + let Some(path_str) = path_str.strip_prefix("file://") else { + break 'uri_to_path path; + }; + + // Even though "/" is theoretically a valid path separator on Windows, it + // doesn't seem to work for drag-and-drop specifically. + OsString::from(path_str.replace("/", "\\")) + } else { + path + } + }; + wide.extend(path.encode_wide()); wide.push(0); } From b5535e24c3c77df21714b63af5def25edd7f7b28 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Thu, 4 Jun 2026 18:22:39 +0200 Subject: [PATCH 87/87] Format --- winit-win32/src/dnd.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs index a032accfbf..005377fb9b 100644 --- a/winit-win32/src/dnd.rs +++ b/winit-win32/src/dnd.rs @@ -810,9 +810,8 @@ unsafe fn send_data_to_stgmedium(data: SendData, hint: TypeHint) -> Option { // UTF-16 + NUL - used for `CF_UNICODETEXT` and other text-ish registered formats. let utf16 = util::encode_wide(&s); - let utf16_bytes = unsafe { - std::slice::from_raw_parts(utf16.as_ptr() as *const u8, utf16.len() * 2) - }; + let utf16_bytes = + unsafe { std::slice::from_raw_parts(utf16.as_ptr() as *const u8, utf16.len() * 2) }; alloc_hglobal_from(utf16_bytes)? }, SendData::Uris(paths) => {