diff --git a/Cargo.toml b/Cargo.toml index 2d65e46..275f45d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ raw-window-handle = "0.5" x11rb = { version = "0.13.0", features = ["cursor", "resource_manager", "allow-unsafe-code"] } x11 = { version = "2.21", features = ["xlib", "xlib_xcb"] } nix = "0.22.0" +percent-encoding = "2.3.1" +bytemuck = "1.15.0" [target.'cfg(target_os="windows")'.dependencies] winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi", "ole2", "oleidl", "shellapi", "winerror"] } diff --git a/examples/render_femtovg/src/main.rs b/examples/render_femtovg/src/main.rs index 0f5504b..397145c 100644 --- a/examples/render_femtovg/src/main.rs +++ b/examples/render_femtovg/src/main.rs @@ -83,7 +83,12 @@ impl WindowHandler for FemtovgExample { self.canvas.set_size(phy_size.width, phy_size.height, size.scale() as f32); self.damaged = true; } - Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { + Event::Mouse( + MouseEvent::CursorMoved { position, .. } + | MouseEvent::DragEntered { position, .. } + | MouseEvent::DragMoved { position, .. } + | MouseEvent::DragDropped { position, .. }, + ) => { self.current_mouse_position = position.to_physical(&self.current_size); self.damaged = true; } diff --git a/src/x11/drag_n_drop.rs b/src/x11/drag_n_drop.rs new file mode 100644 index 0000000..540b9bd --- /dev/null +++ b/src/x11/drag_n_drop.rs @@ -0,0 +1,615 @@ +use keyboard_types::Modifiers; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::{ + io, mem, + path::{Path, PathBuf}, + str::Utf8Error, +}; + +use percent_encoding::percent_decode; +use x11rb::connection::Connection; +use x11rb::errors::ReplyError; +use x11rb::protocol::xproto::{ClientMessageEvent, SelectionNotifyEvent, Timestamp}; +use x11rb::{ + errors::ConnectionError, + protocol::xproto::{self, ConnectionExt}, + x11_utils::Serialize, +}; + +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(); + + // 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; + + 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), + )?; + + &extra_types + }; + + // We only support the TextUriList type + let data_type_supported = + supported_types.contains(&window.xcb_connection.atoms.TextUriList); + + // 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 { .. }); + + // 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 } + }; + + // 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 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)?; + + // We set our state before translating position data, in case that fails. + *self = WaitingForData { + requested_at: timestamp, + source_window: event_source_window, + position: PhyPoint::new(0, 0), + dropped: false, + }; + + let WaitingForData { position, .. } = self else { unreachable!() }; + *position = translate_root_coordinates(window, event_x, event_y)?; + + 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 update the position and notify the handler + Ready { position, data, .. } => { + // Inform the source that we are still accepting the drop. + // Do this first, in case translate_root_coordinates fails, or the handler panics. + // Do not return right away on failure though, we can still inform the handler about + // the new position. + let status_result = send_status_event(event_source_window, window, true); + + *position = translate_root_coordinates(window, event_x, event_y)?; + + 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(), + }), + ); + + status_result?; + Ok(()) + } + } + } + + 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, + }; + + // 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 { .. }); + + // 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 handle_drop_event( + &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, + event: &ClientMessageEvent, + ) -> Result<(), ConnectionError> { + 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 => 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 => + { + send_finished_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 { .. } => { + *self = NoCurrentSession; + + send_finished_event(event_source_window, window, false) + } + + // 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. + + // If we fail to send the request when the drop has completed, we can't do anything. + // Just cancel the drop. + if let Err(e) = request_convert_selection(window, timestamp) { + *self = NoCurrentSession; + + // Try to inform the source that we ended up rejecting the drop. + // If the initial request failed, this is likely to fail too, so we'll ignore + // it if it errors, so we can focus on the original error. + let _ = send_finished_event(event_source_window, window, false); + + return Err(e); + }; + + *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, + }; + + 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 { .. } => { + let Ready { data, position, .. } = mem::replace(self, NoCurrentSession) else { + unreachable!() + }; + + // Don't return immediately if sending the reply fails, we can still notify the window + // handler about the drop. + let reply_result = send_finished_event(event_source_window, window, true); + + 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(), + }), + ); + + reply_result + } + } + } + + pub fn handle_selection_notify_event( + &mut self, window: &WindowInner, handler: &mut dyn WindowHandler, + event: &SelectionNotifyEvent, + ) -> Result<(), ConnectionError> { + // 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 only received the data later. + if dropped { + *self = NoCurrentSession; + + let reply_result = 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(), + }), + ); + + reply_result + } else { + // Save the data, now that we finally have it! + *self = Ready { data: data.clone(), source_window, position }; + + let reply_result = 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(), + }), + ); + + reply_result + } + } + } + } +} + +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() +} + +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)); + } + + 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)?; + + 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())); + } + } + + let path = Path::new(uri).canonicalize().map_err(ParseError::CanonicalizeError)?; + path_list.push(path); + } + Ok(path_list) +} + +#[derive(Debug)] +enum ParseError { + EmptyData, + InvalidUtf8(Utf8Error), + UnsupportedHostname(String), + UnsupportedProtocol(String), + CanonicalizeError(io::Error), +} + +impl Display for ParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("Failed to parse Drag-n-Drop data: ")?; + + 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 6b6ecd3..efdd84c 100644 --- a/src/x11/event_loop.rs +++ b/src/x11/event_loop.rs @@ -1,3 +1,4 @@ +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::{ @@ -18,6 +19,8 @@ pub(super) struct EventLoop { new_physical_size: Option, frame_interval: Duration, event_loop_running: bool, + + drag_n_drop: DragNDropState, } impl EventLoop { @@ -32,6 +35,7 @@ impl EventLoop { frame_interval: Duration::from_millis(15), event_loop_running: false, new_physical_size: None, + drag_n_drop: DragNDropState::NoCurrentSession, } } @@ -155,14 +159,60 @@ impl EventLoop { // window //// XEvent::ClientMessage(event) => { - if event.format == 32 - && event.data.as_data32()[0] - == self.window.xcb_connection.atoms.WM_DELETE_WINDOW - { + if event.format != 32 { + return; + } + + if event.data.as_data32()[0] == self.window.xcb_connection.atoms.WM_DELETE_WINDOW { self.handle_close_requested(); + return; + } + + //// + // drag n drop + //// + if event.type_ == self.window.xcb_connection.atoms.XdndEnter { + if let Err(_e) = self.drag_n_drop.handle_enter_event( + &self.window, + &mut *self.handler, + &event, + ) { + // TODO: log warning + } + } else if event.type_ == self.window.xcb_connection.atoms.XdndPosition { + 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 { + if let Err(_e) = + self.drag_n_drop.handle_drop_event(&self.window, &mut *self.handler, &event) + { + // TODO: log warning + } + } else if event.type_ == self.window.xcb_connection.atoms.XdndLeave { + 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 { + if let Err(_e) = self.drag_n_drop.handle_selection_notify_event( + &self.window, + &mut *self.handler, + &event, + ) { + // TODO: Log warning + } + } + } + + //// + // window resize + //// XEvent::ConfigureNotify(event) => { let new_physical_size = PhySize::new(event.width as u32, event.height as u32); diff --git a/src/x11/mod.rs b/src/x11/mod.rs index 149df0b..7ab486a 100644 --- a/src/x11/mod.rs +++ b/src/x11/mod.rs @@ -5,6 +5,7 @@ mod window; pub use window::*; mod cursor; +mod drag_n_drop; mod event_loop; mod keyboard; mod visual_info; diff --git a/src/x11/window.rs b/src/x11/window.rs index 5b801ec..56eb4a5 100644 --- a/src/x11/window.rs +++ b/src/x11/window.rs @@ -13,7 +13,7 @@ use raw_window_handle::{ use x11rb::connection::Connection; use x11rb::protocol::xproto::{ - AtomEnum, ChangeWindowAttributesAux, ConfigureWindowAux, ConnectionExt as _, CreateGCAux, + AtomEnum, ChangeWindowAttributesAux, ConfigureWindowAux, ConnectionExt, CreateGCAux, CreateWindowAux, EventMask, PropMode, Visualid, Window as XWindow, WindowClass, }; use x11rb::wrapper::ConnectionExt as _; @@ -95,7 +95,7 @@ impl Drop for ParentHandle { pub(crate) struct WindowInner { pub(crate) xcb_connection: XcbConnection, - window_id: XWindow, + pub(crate) window_id: XWindow, pub(crate) window_info: WindowInfo, visual_id: Visualid, mouse_cursor: Cell, @@ -253,6 +253,15 @@ impl<'a> Window<'a> { &[xcb_connection.atoms.WM_DELETE_WINDOW], )?; + // Enable drag and drop (TODO: Make this toggleable?) + xcb_connection.conn.change_property32( + PropMode::REPLACE, + window_id, + xcb_connection.atoms.XdndAware, + AtomEnum::ATOM, + &[5u32], // Latest version; hasn't changed since 2002 + )?; + xcb_connection.conn.flush()?; // TODO: These APIs could use a couple tweaks now that everything is internal and there is diff --git a/src/x11/xcb_connection.rs b/src/x11/xcb_connection.rs index a5ea06d..e280e8d 100644 --- a/src/x11/xcb_connection.rs +++ b/src/x11/xcb_connection.rs @@ -6,7 +6,7 @@ use x11::{xlib, xlib::Display, xlib_xcb}; use x11rb::connection::Connection; use x11rb::cursor::Handle as CursorHandle; -use x11rb::protocol::xproto::{Cursor, Screen}; +use x11rb::protocol::xproto::{self, Cursor, Screen}; use x11rb::resource_manager; use x11rb::xcb_ffi::XCBConnection; @@ -14,10 +14,27 @@ use crate::MouseCursor; use super::cursor; +mod get_property; +pub use get_property::GetPropertyError; + x11rb::atom_manager! { pub Atoms: AtomsCookie { WM_PROTOCOLS, WM_DELETE_WINDOW, + + // Drag-N-Drop Atoms + XdndAware, + XdndEnter, + XdndLeave, + XdndDrop, + XdndPosition, + XdndStatus, + XdndActionPrivate, + XdndSelection, + XdndFinished, + XdndTypeList, + TextUriList: b"text/uri-list", + None: b"None", } } @@ -121,6 +138,12 @@ impl XcbConnection { pub fn screen(&self) -> &Screen { &self.conn.setup().roots[self.screen] } + + pub fn get_property( + &self, window: xproto::Window, property: xproto::Atom, property_type: xproto::Atom, + ) -> Result, GetPropertyError> { + self::get_property::get_property(window, property, property_type, &self.conn) + } } impl Drop for XcbConnection { diff --git a/src/x11/xcb_connection/get_property.rs b/src/x11/xcb_connection/get_property.rs new file mode 100644 index 0000000..317b33c --- /dev/null +++ b/src/x11/xcb_connection/get_property.rs @@ -0,0 +1,188 @@ +/* +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/util/window_property.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 std::error::Error; +use std::ffi::c_int; +use std::fmt; +use std::mem; +use std::sync::Arc; + +use bytemuck::Pod; + +use x11rb::errors::ReplyError; +use x11rb::protocol::xproto::{self, ConnectionExt}; +use x11rb::xcb_ffi::XCBConnection; + +#[derive(Debug, Clone)] +pub enum GetPropertyError { + X11rbError(Arc), + TypeMismatch(xproto::Atom), + FormatMismatch(c_int), +} + +impl> From for GetPropertyError { + fn from(e: T) -> Self { + Self::X11rbError(Arc::new(e.into())) + } +} + +impl fmt::Display for GetPropertyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GetPropertyError::X11rbError(err) => err.fmt(f), + GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"), + GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"), + } + } +} + +impl Error for GetPropertyError {} + +// Number of 32-bit chunks to retrieve per iteration of get_property's inner loop. +// To test if `get_property` works correctly, set this to 1. +const PROPERTY_BUFFER_SIZE: u32 = 1024; // 4k of RAM ought to be enough for anyone! + +pub(super) fn get_property( + window: xproto::Window, property: xproto::Atom, property_type: xproto::Atom, + conn: &XCBConnection, +) -> Result, GetPropertyError> { + let mut iter = PropIterator::new(conn, window, property, property_type); + let mut data = vec![]; + + loop { + if !iter.next_window(&mut data)? { + break; + } + } + + Ok(data) +} + +/// An iterator over the "windows" of the property that we are fetching. +struct PropIterator<'a, T> { + /// Handle to the connection. + conn: &'a XCBConnection, + + /// The window that we're fetching the property from. + window: xproto::Window, + + /// The property that we're fetching. + property: xproto::Atom, + + /// The type of the property that we're fetching. + property_type: xproto::Atom, + + /// The offset of the next window, in 32-bit chunks. + offset: u32, + + /// The format of the type. + format: u8, + + /// Keep a reference to `T`. + _phantom: std::marker::PhantomData, +} + +impl<'a, T: Pod> PropIterator<'a, T> { + /// Create a new property iterator. + fn new( + conn: &'a XCBConnection, window: xproto::Window, property: xproto::Atom, + property_type: xproto::Atom, + ) -> Self { + let format = match mem::size_of::() { + 1 => 8, + 2 => 16, + 4 => 32, + _ => unreachable!(), + }; + + Self { + conn, + window, + property, + property_type, + offset: 0, + format, + _phantom: Default::default(), + } + } + + /// Get the next window and append it to `data`. + /// + /// Returns whether there are more windows to fetch. + fn next_window(&mut self, data: &mut Vec) -> Result { + // Send the request and wait for the reply. + let reply = self + .conn + .get_property( + false, + self.window, + self.property, + self.property_type, + self.offset, + PROPERTY_BUFFER_SIZE, + )? + .reply()?; + + // Make sure that the reply is of the correct type. + if reply.type_ != self.property_type { + return Err(GetPropertyError::TypeMismatch(reply.type_)); + } + + // Make sure that the reply is of the correct format. + if reply.format != self.format { + return Err(GetPropertyError::FormatMismatch(reply.format.into())); + } + + // Append the data to the output. + if mem::size_of::() == 1 && mem::align_of::() == 1 { + // We can just do a bytewise append. + data.extend_from_slice(bytemuck::cast_slice(&reply.value)); + } else { + let old_len = data.len(); + let added_len = reply.value.len() / mem::size_of::(); + + data.resize(old_len + added_len, T::zeroed()); + bytemuck::cast_slice_mut::(&mut data[old_len..]).copy_from_slice(&reply.value); + } + + // Check `bytes_after` to see if there are more windows to fetch. + self.offset += PROPERTY_BUFFER_SIZE; + Ok(reply.bytes_after != 0) + } +}