diff --git a/src/x11/drag_n_drop.rs b/src/x11/drag_n_drop.rs index 91c71c8..8e8a8fa 100644 --- a/src/x11/drag_n_drop.rs +++ b/src/x11/drag_n_drop.rs @@ -1,223 +1,596 @@ -/* -The code in this file was derived from the Winit project (https://github.com/rust-windowing/winit). -The original, unmodified code file this work is derived from can be found here: - -https://github.com/rust-windowing/winit/blob/44aabdddcc9f720aec860c1f83c1041082c28560/src/platform_impl/linux/x11/dnd.rs - -The original code this is based on is licensed under the following terms: -*/ - -/* -Copyright 2024 "The Winit contributors". - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* -The full licensing terms of the original source code, at the time of writing, can also be found at: -https://github.com/rust-windowing/winit/blob/44aabdddcc9f720aec860c1f83c1041082c28560/LICENSE . - -The Derived Work present in this file contains modifications made to the original source code, is -Copyright (c) 2024 "The Baseview contributors", -and is licensed under either the Apache License, Version 2.0; or The MIT license, at your option. - -Copies of those licenses can be respectively found at: -* https://github.com/RustAudio/baseview/blob/master/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0 ; -* https://github.com/RustAudio/baseview/blob/master/LICENSE-MIT. - -*/ - +use keyboard_types::Modifiers; +use std::error::Error; +use std::fmt::{Display, Formatter}; use std::{ - io, - os::raw::*, + io, mem, path::{Path, PathBuf}, str::Utf8Error, }; use percent_encoding::percent_decode; use x11rb::connection::Connection; -use x11rb::protocol::xproto::Timestamp; +use x11rb::errors::ReplyError; +use x11rb::protocol::xproto::{ClientMessageEvent, SelectionNotifyEvent, Timestamp}; use x11rb::{ errors::ConnectionError, protocol::xproto::{self, ConnectionExt}, x11_utils::Serialize, }; -use crate::{DropData, Point}; +use super::xcb_connection::GetPropertyError; +use crate::x11::{Window, WindowInner}; +use crate::{DropData, Event, MouseEvent, PhyPoint, WindowHandler}; +use DragNDropState::*; + +/// The Drag-N-Drop session state of a `baseview` X11 window, for which it is the target. +/// +/// For more information about what the heck is going on here, see the +/// [XDND (X Drag-n-Drop) specification](https://www.freedesktop.org/wiki/Specifications/XDND/). +pub(crate) enum DragNDropState { + /// There is no active XDND session for this window. + NoCurrentSession, + /// At some point in this session's lifetime, we have decided we couldn't possibly handle the + /// source's Drop data. Every request from this source window from now on will be rejected, + /// until either a Leave or Drop event is received. + PermanentlyRejected { + /// The source window the rejected drag session originated from. + source_window: xproto::Window, + }, + /// We have registered a new session (after receiving an Enter event), and are now waiting + /// for a position event. + WaitingForPosition { + /// The protocol version used in this session. + protocol_version: u8, + /// The source window the current drag session originates from. + source_window: xproto::Window, + }, + /// We have performed a request for data (via `XConvertSelection`), and are now waiting for a + /// reply. + /// + /// More position events can still be received to further update the position data. + WaitingForData { + /// The source window the current drag session originates from. + source_window: xproto::Window, + /// The current position of the pointer, from the last received position event. + position: PhyPoint, + /// The timestamp of the event we made the selection request from. + /// + /// This is either from the first position event, or from the drop event if it arrived first. + /// + /// In very old versions of the protocol (v0), this timestamp isn't provided. In that case, + /// this will be `None`. + requested_at: Option, + /// This will be true if we received a drop event *before* we managed to fetch the data. + /// + /// If this is true, this means we must complete the drop upon receiving the data, instead + /// of just going to [`Ready`]. + dropped: bool, + }, + /// We have completed our quest for the drop data. All fields are populated, and the + /// [`WindowHandler`] has been notified about the drop session. + /// + /// We are now waiting for the user to either drop the file, or leave the window. + /// + /// More position events can still be received to further update the position data. + Ready { + /// The source window the current drag session originates from. + source_window: xproto::Window, + position: PhyPoint, + data: DropData, + }, +} + +impl DragNDropState { + pub fn handle_enter_event( + &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, + event: &ClientMessageEvent, + ) -> Result<(), GetPropertyError> { + let data = event.data.as_data32(); + + let source_window = data[0] as xproto::Window; + let [protocol_version, _, _, flags] = data[1].to_le_bytes(); -use super::xcb_connection::{GetPropertyError, XcbConnection}; + // Fetch the list of supported data types. It can be either stored inline in the event, or + // in a separate property on the source window. + const FLAG_HAS_MORE_TYPES: u8 = 1 << 0; + let has_more_types = (FLAG_HAS_MORE_TYPES & flags) == FLAG_HAS_MORE_TYPES; -pub(crate) struct DragNDrop { - // Populated by XdndEnter event handler - pub version: Option, + let extra_types; + let supported_types = if !has_more_types { + &data[2..5] + } else { + extra_types = window.xcb_connection.get_property( + source_window, + window.xcb_connection.atoms.XdndTypeList, + xproto::Atom::from(xproto::AtomEnum::ATOM), + )?; - pub type_list: Option>, + &extra_types + }; - // Populated by XdndPosition event handler - pub source_window: Option, + // We only support the TextUriList type + let data_type_supported = + supported_types.contains(&window.xcb_connection.atoms.TextUriList); - // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) - pub data: DropData, - pub data_requested_at: Option, + // If there was an active drag session that we informed the handler about, we need to + // generate the matching DragLeft before cancelling the previous session. + let interrupted_active_drag = matches!(self, Ready { .. }); - pub logical_pos: Point, -} + // Clear any previous state, and mark the new session as started if we can handle the drop. + *self = if data_type_supported { + WaitingForPosition { source_window, protocol_version } + } else { + // Permanently reject the drop if the data isn't supported. + PermanentlyRejected { source_window } + }; -impl DragNDrop { - pub fn new() -> Self { - Self { - version: None, - type_list: None, - source_window: None, - data: DropData::None, - data_requested_at: None, - logical_pos: Point::new(0.0, 0.0), + // Do this at the end, in case the handler panics, so that it doesn't poison our internal state. + if interrupted_active_drag { + handler.on_event( + &mut crate::Window::new(Window { inner: window }), + Event::Mouse(MouseEvent::DragLeft), + ); } + + Ok(()) } - pub fn reset(&mut self) { - self.version = None; - self.type_list = None; - self.source_window = None; - self.data = DropData::None; - self.data_requested_at = None; - self.logical_pos = Point::new(0.0, 0.0); + pub fn handle_position_event( + &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, + event: &ClientMessageEvent, + ) -> Result<(), ReplyError> { + let data = event.data.as_data32(); + + let event_source_window = data[0] as xproto::Window; + let (event_x, event_y) = decode_xy(data[2]); + + match self { + // Someone sent us a position event without first sending an enter event. + // Weird, but we'll still politely tell them we reject the drop. + NoCurrentSession => Ok(send_status_event(event_source_window, window, false)?), + + // The current session's source window does not match the given event. + // This means it can either be from a stale session, or a misbehaving app. + // In any case, we ignore the event but still tell the source we reject the drop. + WaitingForPosition { source_window, .. } + | PermanentlyRejected { source_window, .. } + | WaitingForData { source_window, .. } + | Ready { source_window, .. } + if *source_window != event_source_window => + { + Ok(send_status_event(event_source_window, window, false)?) + } + + // We decided to permanently reject this drop. + // This means the WindowHandler can't do anything with the data, so we reject the drop. + PermanentlyRejected { .. } => { + Ok(send_status_event(event_source_window, window, false)?) + } + + // This is the position event we were waiting for. Now we can request the selection data. + // The code above already checks that source_window == event_source_window. + WaitingForPosition { protocol_version, source_window: _ } => { + // In version 0, time isn't specified + let timestamp = (*protocol_version >= 1).then_some(data[3] as Timestamp); + + request_convert_selection(window, timestamp)?; + + let position = translate_root_coordinates(window, event_x, event_y)?; + + *self = WaitingForData { + requested_at: timestamp, + source_window: event_source_window, + position, + dropped: false, + }; + + Ok(()) + } + + // We are still waiting for the data. So we'll just update the position in the meantime. + WaitingForData { position, .. } => { + *position = translate_root_coordinates(window, event_x, event_y)?; + + Ok(()) + } + + // We have already received the data. We can + Ready { position, data, .. } => { + *position = translate_root_coordinates(window, event_x, event_y)?; + + // Inform the source that we are still accepting the drop. + send_status_event(event_source_window, window, true)?; + + handler.on_event( + &mut crate::Window::new(Window { inner: window }), + Event::Mouse(MouseEvent::DragMoved { + position: position.to_logical(&window.window_info), + data: data.clone(), + // We don't get modifiers for drag n drop events. + modifiers: Modifiers::empty(), + }), + ); + + Ok(()) + } + } } - pub fn send_status( - &self, this_window: xproto::Window, target_window: xproto::Window, accepted: bool, - conn: &XcbConnection, - ) -> Result<(), ConnectionError> { - let (accepted, action) = - if accepted { (1, conn.atoms.XdndActionPrivate) } else { (0, conn.atoms.None) }; - - let event = xproto::ClientMessageEvent { - response_type: xproto::CLIENT_MESSAGE_EVENT, - window: target_window, - format: 32, - data: [this_window, accepted, 0, 0, action as _].into(), - sequence: 0, - type_: conn.atoms.XdndStatus as _, + pub fn handle_leave_event( + &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, + event: &ClientMessageEvent, + ) { + let data = event.data.as_data32(); + let event_source_window = data[0] as xproto::Window; + + let current_source_window = match self { + NoCurrentSession => return, + WaitingForPosition { source_window, .. } + | PermanentlyRejected { source_window, .. } + | WaitingForData { source_window, .. } + | Ready { source_window, .. } => *source_window, }; - conn.conn.send_event( - false, - target_window, - xproto::EventMask::NO_EVENT, - event.serialize(), - )?; + // Only accept the leave event if it comes from the source window of the current drag session. + if event_source_window != current_source_window { + return; + } + + // If there was an active drag session that we informed the handler about, we need to + // generate the matching DragLeft before cancelling the previous session. + let left_active_drag = matches!(self, Ready { .. }); - conn.conn.flush() + // Clear everything. + *self = NoCurrentSession; + + // Do this at the end, in case the handler panics, so that it doesn't poison our internal state. + if left_active_drag { + handler.on_event( + &mut crate::Window::new(Window { inner: window }), + Event::Mouse(MouseEvent::DragLeft), + ); + } } - pub fn send_finished( - &self, this_window: xproto::Window, target_window: xproto::Window, accepted: bool, - conn: &XcbConnection, + pub fn handle_drop_event( + &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, + event: &ClientMessageEvent, ) -> Result<(), ConnectionError> { - let (accepted, action) = - if accepted { (1, conn.atoms.XdndFinished) } else { (0, conn.atoms.None) }; - - let event = xproto::ClientMessageEvent { - response_type: xproto::CLIENT_MESSAGE_EVENT, - window: target_window, - format: 32, - data: [this_window, accepted, action as _, 0, 0].into(), - sequence: 0, - type_: conn.atoms.XdndStatus as _, - }; + let data = event.data.as_data32(); + + let event_source_window = data[0] as xproto::Window; + + match self { + // Someone sent us a position event without first sending an enter event. + // Weird, but we'll still politely tell them we reject the drop. + NoCurrentSession => Ok(send_finished_event(event_source_window, window, false)?), + + // The current session's source window does not match the given event. + // This means it can either be from a stale session, or a misbehaving app. + // In any case, we ignore the event but still tell the source we reject the drop. + WaitingForPosition { source_window, .. } + | PermanentlyRejected { source_window, .. } + | WaitingForData { source_window, .. } + | Ready { source_window, .. } + if *source_window != event_source_window => + { + Ok(send_finished_event(event_source_window, window, false)?) + } - conn.conn - .send_event(false, target_window, xproto::EventMask::NO_EVENT, event.serialize()) - .map(|_| ()) - } + // We decided to permanently reject this drop. + // This means the WindowHandler can't do anything with the data, so we reject the drop. + PermanentlyRejected { .. } => { + send_finished_event(event_source_window, window, false)?; + + *self = NoCurrentSession; + + Ok(()) + } + + // We received a drop event without any position event. That's very weird, but not + // irrecoverable: the drop event provides enough data as it is. + // The code above already checks that source_window == event_source_window. + WaitingForPosition { protocol_version, source_window: _ } => { + // In version 0, time isn't specified + let timestamp = (*protocol_version >= 1).then_some(data[2] as Timestamp); + + // We have the timestamp, we can use it to request to convert the selection, + // even in this state. + + request_convert_selection(window, timestamp)?; + + *self = WaitingForData { + requested_at: timestamp, + source_window: event_source_window, + // We don't have usable position data. Maybe we'll receive a position later, + // but otherwise this will have to do. + position: PhyPoint::new(0, 0), + dropped: true, + }; - pub fn get_type_list( - &self, source_window: xproto::Window, conn: &XcbConnection, - ) -> Result, GetPropertyError> { - conn.get_property( - source_window, - conn.atoms.XdndTypeList, - xproto::Atom::from(xproto::AtomEnum::ATOM), - ) + Ok(()) + } + + // We are still waiting to receive the data. + // In that case, we'll wait to receive all of it before finalizing the drop. + WaitingForData { dropped, requested_at, .. } => { + // If we have a timestamp, that means this is version >= 1. + if let Some(requested_at) = *requested_at { + let event_timestamp = data[2] as Timestamp; + + // Just in case, check if this drop event isn't stale + if requested_at > event_timestamp { + return Ok(()); + } + } + + // Indicate to the selection_notified handler that the user has performed the drop. + // Now it should complete the drop instead of just waiting for more events. + *dropped = true; + + Ok(()) + } + + // The normal case. + Ready { .. } => { + send_finished_event(event_source_window, window, true)?; + + let Ready { data, position, .. } = mem::replace(self, NoCurrentSession) else { + unreachable!() + }; + + handler.on_event( + &mut crate::Window::new(Window { inner: window }), + Event::Mouse(MouseEvent::DragDropped { + position: position.to_logical(&window.window_info), + data, + // We don't get modifiers for drag n drop events. + modifiers: Modifiers::empty(), + }), + ); + + Ok(()) + } + } } - pub fn convert_selection( - &self, window: xproto::Window, time: xproto::Timestamp, conn: &XcbConnection, + pub fn handle_selection_notify_event( + &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, + event: &SelectionNotifyEvent, ) -> Result<(), ConnectionError> { - conn.conn - .convert_selection( - window, - conn.atoms.XdndSelection, - conn.atoms.TextUriList, - conn.atoms.XdndSelection, - time, - ) - .map(|_| ()) + // Ignore the event if we weren't actually waiting for a selection notify event + let WaitingForData { source_window, requested_at, position, dropped } = *self else { + return Ok(()); + }; + + // Ignore if this was meant for another window (?) + if event.requestor != window.window_id { + return Ok(()); + } + + // Ignore if this is stale selection data. + if let Some(requested_at) = requested_at { + if requested_at != event.time { + return Ok(()); + } + } + + // The sender should have set the data on our window, let's fetch it. + match fetch_dnd_data(window) { + Err(_e) => { + *self = PermanentlyRejected { source_window }; + + if dropped { + send_finished_event(source_window, window, false) + } else { + send_status_event(source_window, window, false) + } + + // TODO: Log warning + } + Ok(data) => { + let logical_position = position.to_logical(&window.window_info); + + // Inform the source that we are (still) accepting the drop. + + // Handle the case where the user already dropped, but we received the data only later. + if dropped { + *self = NoCurrentSession; + + send_finished_event(source_window, window, true)?; + + // Now that we have actual drop data, we can inform the handler about the drag AND drop events. + handler.on_event( + &mut crate::Window::new(Window { inner: window }), + Event::Mouse(MouseEvent::DragEntered { + position: logical_position, + data: data.clone(), + // We don't get modifiers for drag n drop events. + modifiers: Modifiers::empty(), + }), + ); + + handler.on_event( + &mut crate::Window::new(Window { inner: window }), + Event::Mouse(MouseEvent::DragDropped { + position: logical_position, + data: data.clone(), + // We don't get modifiers for drag n drop events. + modifiers: Modifiers::empty(), + }), + ); + } else { + // Save the data, now that we finally have it! + *self = Ready { data: data.clone(), source_window, position }; + + send_status_event(source_window, window, true)?; + + // Now that we have actual drop data, we can inform the handler about the drag event. + handler.on_event( + &mut crate::Window::new(Window { inner: window }), + Event::Mouse(MouseEvent::DragEntered { + position: logical_position, + data, + // We don't get modifiers for drag n drop events. + modifiers: Modifiers::empty(), + }), + ); + } + + Ok(()) + } + } } +} + +fn send_status_event( + source_window: xproto::Window, window: &WindowInner, accepted: bool, +) -> Result<(), ConnectionError> { + let conn = &window.xcb_connection; + let (accepted, action) = + if accepted { (1, conn.atoms.XdndActionPrivate) } else { (0, conn.atoms.None) }; + + let event = ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + window: source_window, + format: 32, + data: [window.window_id, accepted, 0, 0, action as _].into(), + sequence: 0, + type_: conn.atoms.XdndStatus, + }; + + conn.conn.send_event(false, source_window, xproto::EventMask::NO_EVENT, event.serialize())?; + + conn.conn.flush() +} + +pub fn send_finished_event( + source_window: xproto::Window, window: &WindowInner, accepted: bool, +) -> Result<(), ConnectionError> { + let conn = &window.xcb_connection; + let (accepted, action) = + if accepted { (1, conn.atoms.XdndFinished) } else { (0, conn.atoms.None) }; + + let event = ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + window: source_window, + format: 32, + data: [window.window_id, accepted, action as _, 0, 0].into(), + sequence: 0, + type_: conn.atoms.XdndStatus as _, + }; + + conn.conn.send_event(false, source_window, xproto::EventMask::NO_EVENT, event.serialize())?; + + conn.conn.flush() +} - pub fn read_data( - &self, window: xproto::Window, conn: &XcbConnection, - ) -> Result, GetPropertyError> { - conn.get_property(window, conn.atoms.XdndSelection, conn.atoms.TextUriList) +fn request_convert_selection( + window: &WindowInner, timestamp: Option, +) -> Result<(), ConnectionError> { + let conn = &window.xcb_connection; + + conn.conn.convert_selection( + window.window_id, + conn.atoms.XdndSelection, + conn.atoms.TextUriList, + conn.atoms.XdndSelection, + timestamp.unwrap_or(x11rb::CURRENT_TIME), + )?; + + conn.conn.flush() +} + +fn decode_xy(data: u32) -> (u16, u16) { + ((data >> 16) as u16, data as u16) +} + +fn translate_root_coordinates( + window: &WindowInner, x: u16, y: u16, +) -> Result { + let root_id = window.xcb_connection.screen().root; + if root_id == window.window_id { + return Ok(PhyPoint::new(x as i32, y as i32)); } - 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 reply = window + .xcb_connection + .conn + .translate_coordinates(root_id, window.window_id, x as i16, y as i16)? + .reply()?; + + Ok(PhyPoint::new(reply.dst_x as i32, reply.dst_y as i32)) +} + +fn fetch_dnd_data(window: &WindowInner) -> Result> { + let conn = &window.xcb_connection; + + let data: Vec = + conn.get_property(window.window_id, conn.atoms.XdndSelection, conn.atoms.TextUriList)?; + + let path_list = parse_data(&data)?; - let path = Path::new(&path_str).canonicalize()?; - path_list.push(path); + Ok(DropData::Files(path_list)) +} + +// See: https://edeproject.org/spec/file-uri-spec.txt +// TL;DR: format is "file:///", hostname is optional and can be "localhost" +fn parse_data(data: &[u8]) -> Result, ParseError> { + if data.is_empty() { + return Err(ParseError::EmptyData); + } + + let decoded = percent_decode(data).decode_utf8().map_err(ParseError::InvalidUtf8)?; + + let mut path_list = Vec::new(); + for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { + // We only support the file:// protocol + let Some(mut uri) = uri.strip_prefix("file://") else { + return Err(ParseError::UnsupportedProtocol(uri.into())); + }; + + if !uri.starts_with('/') { + // Try (and hope) to see if it's just localhost + if let Some(stripped) = uri.strip_prefix("localhost") { + if !stripped.starts_with('/') { + // There is something else after "localhost" but before '/' + return Err(ParseError::UnsupportedHostname(uri.into())); + } + + uri = stripped; + } else { + // We don't support hostnames. + return Err(ParseError::UnsupportedHostname(uri.into())); } - Ok(path_list) - } else { - Err(DndDataParseError::EmptyData) } + + let path = Path::new(uri).canonicalize().map_err(ParseError::CanonicalizeError)?; + path_list.push(path); } + Ok(path_list) } #[derive(Debug)] -pub enum DndDataParseError { +enum ParseError { EmptyData, - InvalidUtf8(#[allow(dead_code)] Utf8Error), - HostnameSpecified(#[allow(dead_code)] String), - UnexpectedProtocol(#[allow(dead_code)] String), - UnresolvablePath(#[allow(dead_code)] io::Error), + InvalidUtf8(Utf8Error), + UnsupportedHostname(String), + UnsupportedProtocol(String), + CanonicalizeError(io::Error), } -impl From for DndDataParseError { - fn from(e: Utf8Error) -> Self { - DndDataParseError::InvalidUtf8(e) - } -} +impl Display for ParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to parse Drag-n-Drop data: ")?; -impl From for DndDataParseError { - fn from(e: io::Error) -> Self { - DndDataParseError::UnresolvablePath(e) + match self { + ParseError::EmptyData => f.write_str("data is empty"), + ParseError::InvalidUtf8(e) => e.fmt(f), + ParseError::UnsupportedHostname(uri) => write!(f, "unsupported hostname in URI: {uri}"), + ParseError::UnsupportedProtocol(uri) => write!(f, "unsupported protocol in URI: {uri}"), + ParseError::CanonicalizeError(e) => write!(f, "unable to resolve path: {e}"), + } } } + +impl Error for ParseError {} diff --git a/src/x11/event_loop.rs b/src/x11/event_loop.rs index 3a3f0bb..efdd84c 100644 --- a/src/x11/event_loop.rs +++ b/src/x11/event_loop.rs @@ -1,16 +1,14 @@ -use crate::x11::drag_n_drop::DragNDrop; +use crate::x11::drag_n_drop::DragNDropState; use crate::x11::keyboard::{convert_key_press_event, convert_key_release_event, key_mods}; use crate::x11::{ParentHandle, Window, WindowInner}; use crate::{ - DropData, Event, MouseButton, MouseEvent, PhyPoint, PhySize, Point, ScrollDelta, WindowEvent, - WindowHandler, WindowInfo, + Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, WindowEvent, WindowHandler, + WindowInfo, }; -use keyboard_types::Modifiers; use std::error::Error; use std::os::fd::AsRawFd; use std::time::{Duration, Instant}; use x11rb::connection::Connection; -use x11rb::protocol::xproto::{Atom, ConnectionExt, Timestamp, Window as XWindow}; use x11rb::protocol::Event as XEvent; pub(super) struct EventLoop { @@ -22,7 +20,7 @@ pub(super) struct EventLoop { frame_interval: Duration, event_loop_running: bool, - drag_n_drop: DragNDrop, + drag_n_drop: DragNDropState, } impl EventLoop { @@ -37,7 +35,7 @@ impl EventLoop { frame_interval: Duration::from_millis(15), event_loop_running: false, new_physical_size: None, - drag_n_drop: DragNDrop::new(), + drag_n_drop: DragNDropState::NoCurrentSession, } } @@ -174,199 +172,40 @@ impl EventLoop { // drag n drop //// if event.type_ == self.window.xcb_connection.atoms.XdndEnter { - self.drag_n_drop.reset(); - let data = event.data.as_data32(); - - let source_window = data[0] as XWindow; - let flags = data[1]; - let version = flags >> 24; - - self.drag_n_drop.version = Some(version); - - let has_more_types = flags - (flags & (u32::max_value() - 1)) == 1; - if !has_more_types { - let type_list = vec![data[2] as Atom, data[3] as Atom, data[4] as Atom]; - self.drag_n_drop.type_list = Some(type_list); - } else if let Ok(more_types) = - self.drag_n_drop.get_type_list(source_window, &self.window.xcb_connection) - { - self.drag_n_drop.type_list = Some(more_types); + if let Err(_e) = self.drag_n_drop.handle_enter_event( + &self.window, + &mut *self.handler, + &event, + ) { + // TODO: log warning } - - self.handler.on_event( - &mut crate::Window::new(Window { inner: &self.window }), - Event::Mouse(MouseEvent::DragEntered { - // We don't get the position until we get an `XdndPosition` event. - position: Point::new(0.0, 0.0), - // We don't get modifiers for drag n drop events. - modifiers: Modifiers::empty(), - // We don't get data until we get an `XdndPosition` event. - data: DropData::None, - }), - ); } else if event.type_ == self.window.xcb_connection.atoms.XdndPosition { - let data = event.data.as_data32(); - - let source_window = data[0] as XWindow; - - // By our own state flow, `version` should never be `None` at this point. - let version = self.drag_n_drop.version.unwrap_or(5); - - let accepted = if let Some(ref type_list) = self.drag_n_drop.type_list { - type_list.contains(&self.window.xcb_connection.atoms.TextUriList) - } else { - false - }; - - if !accepted { - if let Err(_e) = self.drag_n_drop.send_status( - self.window.window_id, - source_window, - false, - &self.window.xcb_connection, - ) { - // TODO: log warning - } - - self.drag_n_drop.reset(); - return; - } - - self.drag_n_drop.source_window = Some(source_window); - - let packed_coordinates = data[2]; - let x = packed_coordinates >> 16; - let y = packed_coordinates & !(x << 16); - let mut physical_pos = PhyPoint::new(x as i32, y as i32); - - // The coordinates are relative to the root window, not our window >:( - let root_id = self.window.xcb_connection.screen().root; - if root_id != self.window.window_id { - if let Ok(r) = self - .window - .xcb_connection - .conn - .translate_coordinates( - root_id, - self.window.window_id, - physical_pos.x as i16, - physical_pos.y as i16, - ) - .unwrap() - .reply() - { - physical_pos = PhyPoint::new(r.dst_x as i32, r.dst_y as i32); - } - } - - self.drag_n_drop.logical_pos = - physical_pos.to_logical(&self.window.window_info); - - let ev = Event::Mouse(MouseEvent::DragMoved { - position: self.drag_n_drop.logical_pos, - // We don't get modifiers for drag n drop events. - modifiers: Modifiers::empty(), - data: self.drag_n_drop.data.clone(), - }); - self.handler - .on_event(&mut crate::Window::new(Window { inner: &self.window }), ev); - - if self.drag_n_drop.data_requested_at.is_none() { - let time = if version >= 1 { - data[3] as Timestamp - } else { - // In version 0, time isn't specified - x11rb::CURRENT_TIME - }; - - // This results in the `SelectionNotify` event below - if let Err(_e) = self.drag_n_drop.convert_selection( - self.window.window_id, - time, - &self.window.xcb_connection, - ) { - // TODO: log warning - } else { - self.drag_n_drop.data_requested_at = Some(time); - self.window.xcb_connection.conn.flush().unwrap(); - } - } - - if let Err(_e) = self.drag_n_drop.send_status( - self.window.window_id, - source_window, - true, - &self.window.xcb_connection, + if let Err(_e) = self.drag_n_drop.handle_position_event( + &self.window, + &mut *self.handler, + &event, ) { // TODO: log warning } } else if event.type_ == self.window.xcb_connection.atoms.XdndDrop { - let (source_window, accepted) = if let Some(source_window) = - self.drag_n_drop.source_window + if let Err(_e) = + self.drag_n_drop.handle_drop_event(&self.window, &mut *self.handler, &event) { - let ev = Event::Mouse(MouseEvent::DragDropped { - position: self.drag_n_drop.logical_pos, - // We don't get modifiers for drag n drop events. - modifiers: Modifiers::empty(), - data: self.drag_n_drop.data.clone(), - }); - self.handler - .on_event(&mut crate::Window::new(Window { inner: &self.window }), ev); - - (source_window, true) - } else { - // `source_window` won't be part of our DND state if we already rejected the drop in our - // `XdndPosition` handler. - let source_window = event.data.as_data32()[0] as XWindow; - (source_window, false) - }; - - if let Err(_e) = self.drag_n_drop.send_finished( - self.window.window_id, - source_window, - accepted, - &self.window.xcb_connection, - ) { // TODO: log warning } - - self.drag_n_drop.reset(); } else if event.type_ == self.window.xcb_connection.atoms.XdndLeave { - self.drag_n_drop.reset(); - - self.handler.on_event( - &mut crate::Window::new(Window { inner: &self.window }), - Event::Mouse(MouseEvent::DragLeft), - ); + self.drag_n_drop.handle_leave_event(&self.window, &mut *self.handler, &event); } } XEvent::SelectionNotify(event) => { if event.property == self.window.xcb_connection.atoms.XdndSelection { - let Some(requested_time) = self.drag_n_drop.data_requested_at else { - // eprintln!("Received DnD selection data, but we didn't request any. Weird. Skipping."); - return; - }; - - if event.time != requested_time { - // eprintln!("Received stale DnD selection data ({}), expected data is {requested_time}. Skipping.", event.time); - return; - } - - if let Ok(mut data) = self - .drag_n_drop - .read_data(self.window.window_id, &self.window.xcb_connection) - { - match self.drag_n_drop.parse_data(&mut data) { - Ok(path_list) => { - self.drag_n_drop.data = DropData::Files(path_list); - } - Err(_e) => { - self.drag_n_drop.data = DropData::None; - - // TODO: Log warning - } - } + if let Err(_e) = self.drag_n_drop.handle_selection_notify_event( + &self.window, + &mut *self.handler, + &event, + ) { + // TODO: Log warning } } }