diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5f47a533..0f9083c3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,9 @@ You can find its changes [documented below](#070---2021-01-01). - X11 backend now supports custom cursors ([#1801] by [@psychon]) - X11: Add support for transparent windows ([#1803] by [@psychon]) - X11: Added support for `get_monitors` ([#1804] by [@psychon]) +- x11: Remove some unnecessary casts ([#1851] by [@psychon]) - `has_focus` method on `WidgetPod` ([#1825] by [@ForLoveOfCats]) +- x11: Add support for getting clipboard contents ([#1805] by [@psychon]) ### Changed @@ -734,8 +736,10 @@ Last release without a changelog :( [#1802]: https://github.com/linebender/druid/pull/1802 [#1803]: https://github.com/linebender/druid/pull/1803 [#1804]: https://github.com/linebender/druid/pull/1804 +[#1805]: https://github.com/linebender/druid/pull/1805 [#1820]: https://github.com/linebender/druid/pull/1820 [#1825]: https://github.com/linebender/druid/pull/1825 +[#1851]: https://github.com/linebender/druid/pull/1851 [Unreleased]: https://github.com/linebender/druid/compare/v0.7.0...master [0.7.0]: https://github.com/linebender/druid/compare/v0.6.0...v0.7.0 diff --git a/druid-shell/src/platform/mac/keyboard.rs b/druid-shell/src/platform/mac/keyboard.rs index dc8125e6c7..bbd36658c9 100644 --- a/druid-shell/src/platform/mac/keyboard.rs +++ b/druid-shell/src/platform/mac/keyboard.rs @@ -39,7 +39,7 @@ pub(crate) struct KeyboardState { /// Convert a macOS platform key code (keyCode field of NSEvent). /// /// The primary source for this mapping is: -/// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values +/// /// /// It should also match up with CODE_MAP_MAC bindings in /// NativeKeyToDOMCodeName.h. diff --git a/druid-shell/src/platform/mac/text_input.rs b/druid-shell/src/platform/mac/text_input.rs index 28ce903a28..766914e662 100644 --- a/druid-shell/src/platform/mac/text_input.rs +++ b/druid-shell/src/platform/mac/text_input.rs @@ -65,6 +65,8 @@ unsafe impl objc::Encode for NSRange { } } +// BOOL is i8 on x86, but bool on aarch64 +#[cfg_attr(target_arch = "aarch64", allow(clippy::useless_conversion))] pub extern "C" fn has_marked_text(this: &mut Object, _: Sel) -> BOOL { with_edit_lock_from_window(this, false, |edit_lock| { edit_lock.composition_range().is_some() diff --git a/druid-shell/src/platform/windows/keyboard.rs b/druid-shell/src/platform/windows/keyboard.rs index 3957459829..81bd1209a7 100644 --- a/druid-shell/src/platform/windows/keyboard.rs +++ b/druid-shell/src/platform/windows/keyboard.rs @@ -348,8 +348,9 @@ fn vk_to_key(vk: VkCode) -> Option { /// map. /// /// The virtual key code can have modifiers in the higher order byte when the -/// argument is a `Character` variant. See: -/// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw +/// argument is a `Character` variant. See [VkKeyScanW][]. +/// +/// [VkKeyScanW]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw pub(crate) fn key_to_vk(key: &KbKey) -> Option { Some(match key { KbKey::Character(s) => { diff --git a/druid-shell/src/platform/x11/application.rs b/druid-shell/src/platform/x11/application.rs index c3e8ecfd50..c9578c7ef5 100644 --- a/druid-shell/src/platform/x11/application.rs +++ b/druid-shell/src/platform/x11/application.rs @@ -14,8 +14,8 @@ //! X11 implementation of features at the application scope. -use std::cell::RefCell; -use std::collections::HashMap; +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; use std::convert::{TryFrom, TryInto}; use std::os::unix::io::RawFd; use std::rc::Rc; @@ -27,7 +27,7 @@ use x11rb::protocol::present::ConnectionExt as _; use x11rb::protocol::render::{self, ConnectionExt as _, Pictformat}; use x11rb::protocol::xfixes::ConnectionExt as _; use x11rb::protocol::xproto::{ - self, ConnectionExt, CreateWindowAux, EventMask, Visualtype, WindowClass, + self, ConnectionExt, CreateWindowAux, EventMask, Timestamp, Visualtype, WindowClass, }; use x11rb::protocol::Event; use x11rb::resource_manager::Database as ResourceDb; @@ -49,15 +49,19 @@ pub(crate) struct Application { /// A display is a collection of screens. connection: Rc, /// An `XCBConnection` is *technically* safe to use from other threads, but there are - /// subtleties; see https://github.com/psychon/x11rb/blob/41ab6610f44f5041e112569684fc58cd6d690e57/src/event_loop_integration.rs. + /// subtleties; see [x11rb event loop integration notes][1] for more details. /// Let's just avoid the issue altogether. As far as public API is concerned, this causes /// `druid_shell::WindowHandle` to be `!Send` and `!Sync`. + /// + /// [1]: https://github.com/psychon/x11rb/blob/41ab6610f44f5041e112569684fc58cd6d690e57/src/event_loop_integration.rs. marker: std::marker::PhantomData<*mut XCBConnection>, /// The type of visual used by the root window root_visual_type: Visualtype, /// The visual for windows with transparent backgrounds, if supported argb_visual_type: Option, + /// Pending events that need to be handled later + pending_events: Rc>>, /// The X11 resource database used to query dpi. pub(crate) rdb: Rc, @@ -72,7 +76,7 @@ pub(crate) struct Application { /// In practice multiple physical monitor drawing areas are present on a single screen. /// This is achieved via various X server extensions (XRandR/Xinerama/TwinView), /// with XRandR seeming like the best choice. - screen_num: i32, // Needs a container when no longer const + screen_num: usize, // Needs a container when no longer const /// The X11 window id of this `Application`. /// /// This is an input-only non-visual X11 window that is created first during initialization, @@ -93,6 +97,8 @@ pub(crate) struct Application { present_opcode: Option, /// Support for the render extension in at least version 0.5? render_argb32_pictformat_cursor: Option, + /// Newest timestamp that we received + timestamp: Rc>, xkb_context: xkb::Context, } @@ -131,7 +137,9 @@ impl Application { let xkb_context = xkb::Context::new(); xkb_context.set_log_level(tracing::Level::DEBUG); use x11rb::protocol::xkb::ConnectionExt; - conn.xkb_use_extension(1, 0)?.reply().context("init xkb extension")?; + conn.xkb_use_extension(1, 0)? + .reply() + .context("init xkb extension")?; let device_id = xkb_context .core_keyboard_device_id(&conn) .context("get core keyboard device id")?; @@ -142,8 +150,7 @@ impl Application { let xkb_state = keymap.state(); let connection = Rc::new(conn); - let window_id = Application::create_event_window(&connection, screen_num as i32)?; - + let window_id = Application::create_event_window(&connection, screen_num)?; let state = Rc::new(RefCell::new(State { quitting: false, windows: HashMap::new(), @@ -216,7 +223,7 @@ impl Application { let screen = connection .setup() .roots - .get(screen_num as usize) + .get(screen_num) .ok_or_else(|| anyhow!("Invalid screen num: {}", screen_num))?; let root_visual_type = util::get_visual_from_screen(&screen) .ok_or_else(|| anyhow!("Couldn't get visual from screen"))?; @@ -225,7 +232,7 @@ impl Application { Ok(Application { connection, rdb, - screen_num: screen_num as i32, + screen_num, window_id, state, idle_read, @@ -234,8 +241,10 @@ impl Application { present_opcode, root_visual_type, argb_visual_type, + pending_events: Default::default(), marker: std::marker::PhantomData, render_argb32_pictformat_cursor, + timestamp: Rc::new(Cell::new(x11rb::CURRENT_TIME)), xkb_context, }) } @@ -293,12 +302,12 @@ impl Application { self.render_argb32_pictformat_cursor } - fn create_event_window(conn: &Rc, screen_num: i32) -> Result { + fn create_event_window(conn: &Rc, screen_num: usize) -> Result { let id = conn.generate_id()?; let setup = conn.setup(); let screen = setup .roots - .get(screen_num as usize) + .get(screen_num) .ok_or_else(|| anyhow!("invalid screen num: {}", screen_num))?; // Create the actual window @@ -358,7 +367,7 @@ impl Application { } #[inline] - pub(crate) fn screen_num(&self) -> i32 { + pub(crate) fn screen_num(&self) -> usize { self.screen_num } @@ -391,6 +400,21 @@ impl Application { /// Returns `Ok(true)` if we want to exit the main loop. fn handle_event(&self, ev: &Event) -> Result { + if ev.server_generated() { + // Update our latest timestamp + let timestamp = match ev { + Event::KeyPress(ev) => ev.time, + Event::KeyRelease(ev) => ev.time, + Event::ButtonPress(ev) => ev.time, + Event::ButtonRelease(ev) => ev.time, + Event::MotionNotify(ev) => ev.time, + Event::EnterNotify(ev) => ev.time, + Event::LeaveNotify(ev) => ev.time, + Event::PropertyNotify(ev) => ev.time, + _ => self.timestamp.get(), + }; + self.timestamp.set(timestamp); + } match ev { // NOTE: When adding handling for any of the following events, // there must be a check against self.window_id @@ -548,10 +572,15 @@ impl Application { self.connection.flush()?; + // Deal with pending events + let mut event = self.pending_events.borrow_mut().pop_front(); + // Before we poll on the connection's file descriptor, check whether there are any // events ready. It could be that XCB has some events in its internal buffers because // of something that happened during the idle loop. - let mut event = self.connection.poll_for_event()?; + if event.is_none() { + event = self.connection.poll_for_event()?; + } if event.is_none() { poll_with_timeout( @@ -641,9 +670,12 @@ impl Application { } pub fn clipboard(&self) -> Clipboard { - // TODO(x11/clipboard): implement Application::clipboard - tracing::warn!("Application::clipboard is currently unimplemented for X11 platforms."); - Clipboard {} + Clipboard::new( + Rc::clone(&self.connection), + self.screen_num, + Rc::clone(&self.pending_events), + Rc::clone(&self.timestamp), + ) } pub fn get_locale() -> String { diff --git a/druid-shell/src/platform/x11/clipboard.rs b/druid-shell/src/platform/x11/clipboard.rs index 88fcc4e9bf..eae5c897ae 100644 --- a/druid-shell/src/platform/x11/clipboard.rs +++ b/druid-shell/src/platform/x11/clipboard.rs @@ -14,13 +14,56 @@ //! Interactions with the system pasteboard on X11. +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; + +use x11rb::connection::Connection; +use x11rb::errors::ReplyOrIdError; +use x11rb::protocol::xproto::{ + AtomEnum, ChangeWindowAttributesAux, ConnectionExt, EventMask, GetPropertyReply, + GetPropertyType, Property, Timestamp, WindowClass, +}; +use x11rb::protocol::Event; +use x11rb::xcb_ffi::XCBConnection; + use crate::clipboard::{ClipboardFormat, FormatId}; -use tracing::warn; +use tracing::{debug, warn}; -#[derive(Debug, Clone, Default)] -pub struct Clipboard; +// We can pick an arbitrary atom that is used for the transfer. This is our pick. +const TRANSFER_ATOM: AtomEnum = AtomEnum::CUT_BUFFE_R4; + +const STRING_TARGETS: [&str; 5] = [ + "UTF8_STRING", + "TEXT", + "STRING", + "text/plain;charset=utf-8", + "text/plain", +]; + +#[derive(Debug, Clone)] +pub struct Clipboard { + connection: Rc, + screen_num: usize, + event_queue: Rc>>, + timestamp: Rc>, +} impl Clipboard { + pub(crate) fn new( + connection: Rc, + screen_num: usize, + event_queue: Rc>>, + timestamp: Rc>, + ) -> Self { + Clipboard { + connection, + screen_num, + event_queue, + timestamp, + } + } + pub fn put_string(&mut self, _s: impl AsRef) { // TODO(x11/clipboard): implement Clipboard::put_string warn!("Clipboard::put_string is currently unimplemented for X11 platforms."); @@ -32,26 +75,191 @@ impl Clipboard { } pub fn get_string(&self) -> Option { - // TODO(x11/clipboard): implement Clipboard::get_string - warn!("Clipboard::set_string is currently unimplemented for X11 platforms."); - None + STRING_TARGETS.iter().find_map(|target| { + self.get_format(target) + .and_then(|data| String::from_utf8(data).ok()) + }) } - pub fn preferred_format(&self, _formats: &[FormatId]) -> Option { - // TODO(x11/clipboard): implement Clipboard::preferred_format - warn!("Clipboard::preferred_format is currently unimplemented for X11 platforms."); - None + pub fn preferred_format(&self, formats: &[FormatId]) -> Option { + let available = self.available_type_names(); + formats + .iter() + .find(|f1| available.iter().any(|f2| *f1 == f2)) + .copied() } - pub fn get_format(&self, _format: FormatId) -> Option> { - // TODO(x11/clipboard): implement Clipboard::get_format - warn!("Clipboard::get_format is currently unimplemented for X11 platforms."); - None + pub fn get_format(&self, format: FormatId) -> Option> { + self.do_transfer(format, |prop| prop.value) } + #[allow(clippy::needless_collect)] pub fn available_type_names(&self) -> Vec { - // TODO(x11/clipboard): implement Clipboard::available_type_names - warn!("Clipboard::available_type_names is currently unimplemented for X11 platforms."); - vec![] + let requests = self + .do_transfer("TARGETS", |prop| { + prop.value32() + .map(|iter| iter.collect()) + .unwrap_or_default() + }) + .unwrap_or_default() + .into_iter() + .filter_map(|atom| self.connection.get_atom_name(atom).ok()) + .collect::>(); + // We first send all requests above and then fetch the replies with only one round-trip to + // the X11 server. Hence, the collect() above is not unnecessary! + requests + .into_iter() + .filter_map(|req| req.reply().ok()) + .filter_map(|reply| String::from_utf8(reply.name).ok()) + .collect() + } + + fn do_transfer(&self, format: FormatId, converter: F) -> Option> + where + R: Clone, + F: FnMut(GetPropertyReply) -> Vec, + { + match self.do_transfer_impl(format, converter) { + Ok(result) => result, + Err(error) => { + warn!("Error in Clipboard::do_transfer: {:?}", error); + None + } + } + } + + fn do_transfer_impl( + &self, + format: FormatId, + mut converter: F, + ) -> Result>, ReplyOrIdError> + where + R: Clone, + F: FnMut(GetPropertyReply) -> Vec, + { + debug!("Getting clipboard contents in format {}", format); + + let conn = &*self.connection; + let (format_atom, clipboard_atom, incr_atom) = { + let format = conn.intern_atom(false, format.as_bytes())?; + let clipboard = conn.intern_atom(false, b"CLIPBOARD")?; + let incr = conn.intern_atom(false, b"INCR")?; + ( + format.reply()?.atom, + clipboard.reply()?.atom, + incr.reply()?.atom, + ) + }; + + // Create a window for the transfer + let window = WindowContainer::new(conn, self.screen_num)?; + + conn.convert_selection( + window.window, + clipboard_atom, + format_atom, + TRANSFER_ATOM, + self.timestamp.get(), + )?; + + // Now wait for the selection notify event + conn.flush()?; + let notify = loop { + match conn.wait_for_event()? { + Event::SelectionNotify(notify) if notify.requestor == window.window => { + break notify + } + event => self.event_queue.borrow_mut().push_back(event), + } + }; + + if notify.property == x11rb::NONE { + // Selection is empty + debug!("Selection transfer was rejected"); + return Ok(None); + } + + conn.change_window_attributes( + window.window, + &ChangeWindowAttributesAux::default().event_mask(EventMask::PROPERTY_CHANGE), + )?; + + let property = conn + .get_property( + true, + window.window, + TRANSFER_ATOM, + GetPropertyType::ANY, + 0, + u32::MAX, + )? + .reply()?; + + if property.type_ != incr_atom { + debug!("Got selection contents directly"); + return Ok(Some(converter(property))); + } + + // The above GetProperty with delete=true indicated that the INCR transfer starts + // now, wait for the property notifies + debug!("Doing an INCR transfer for the selection"); + conn.flush()?; + let mut value = Vec::new(); + loop { + match conn.wait_for_event()? { + Event::PropertyNotify(notify) + if (notify.window, notify.state) == (window.window, Property::NEW_VALUE) => + { + let property = conn + .get_property( + true, + window.window, + TRANSFER_ATOM, + GetPropertyType::ANY, + 0, + u32::MAX, + )? + .reply()?; + if property.value.is_empty() { + debug!("INCR transfer finished"); + return Ok(Some(value)); + } else { + value.extend_from_slice(&converter(property)); + } + } + event => self.event_queue.borrow_mut().push_back(event), + } + } + } +} + +struct WindowContainer<'a> { + window: u32, + conn: &'a XCBConnection, +} + +impl<'a> WindowContainer<'a> { + fn new(conn: &'a XCBConnection, screen_num: usize) -> Result { + let window = conn.generate_id()?; + conn.create_window( + x11rb::COPY_DEPTH_FROM_PARENT, + window, + conn.setup().roots[screen_num].root, + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_OUTPUT, + x11rb::COPY_FROM_PARENT, + &Default::default(), + )?; + Ok(WindowContainer { window, conn }) + } +} + +impl Drop for WindowContainer<'_> { + fn drop(&mut self) { + let _ = self.conn.destroy_window(self.window); } } diff --git a/druid-shell/src/platform/x11/screen.rs b/druid-shell/src/platform/x11/screen.rs index c08ee12fac..caab785ae2 100644 --- a/druid-shell/src/platform/x11/screen.rs +++ b/druid-shell/src/platform/x11/screen.rs @@ -37,7 +37,7 @@ where pub(crate) fn get_monitors() -> Vec { let result = if let Some(app) = crate::Application::try_global() { let app = app.platform_app; - get_monitors_impl(app.connection().as_ref(), app.screen_num() as usize) + get_monitors_impl(app.connection().as_ref(), app.screen_num()) } else { let (conn, screen_num) = match x11rb::connect(None) { Ok(res) => res, diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 24056f5f60..1aefbfe784 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -270,7 +270,7 @@ impl WindowBuilder { let size_px = self.size.to_px(scale); let screen = setup .roots - .get(screen_num as usize) + .get(screen_num) .ok_or_else(|| anyhow!("Invalid screen num: {}", screen_num))?; let visual_type = if self.transparent { self.app.argb_visual_type() @@ -1767,7 +1767,7 @@ impl WindowHandle { Some(format) => { let conn = w.app.connection(); let setup = &conn.setup(); - let screen = &setup.roots[w.app.screen_num() as usize]; + let screen = &setup.roots[w.app.screen_num()]; match make_cursor(&**conn, setup.image_byte_order, screen.root, format, desc) { // TODO: We 'leak' the cursor - nothing ever calls render_free_cursor Ok(cursor) => Some(cursor), diff --git a/druid/examples/value_formatting/src/formatters.rs b/druid/examples/value_formatting/src/formatters.rs index 53d13e56e8..76a5c0fc81 100644 --- a/druid/examples/value_formatting/src/formatters.rs +++ b/druid/examples/value_formatting/src/formatters.rs @@ -165,6 +165,7 @@ impl Formatter for NaiveCurrencyFormatter { Validation::failure(CurrencyValidationError::TooManyCharsAfterDecimal) } (Some(c), None, _) if c.is_ascii_digit() => Validation::success(), + (Some(c), None, _) => Validation::failure(CurrencyValidationError::InvalidChar(c)), (None, None, _) => Validation::success(), (Some(c1), Some(c2), _) if c1.is_ascii_digit() && c2.is_ascii_digit() => { Validation::success() @@ -173,7 +174,7 @@ impl Formatter for NaiveCurrencyFormatter { let bad_char = if c1.is_ascii_digit() { other } else { c1 }; Validation::failure(CurrencyValidationError::InvalidChar(bad_char)) } - _ => unreachable!(), + other => panic!("unexpected: {:?}", other), } } } diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index bb716a8bb6..da8933c3b9 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -606,12 +606,16 @@ impl Widget for TextBox { let padding_offset = Vec2::new(textbox_insets.x0, textbox_insets.y0); - let cursor = if data.is_empty() { + let mut cursor = if data.is_empty() { cursor_line + padding_offset } else { cursor_line + padding_offset - self.inner.offset() }; + // Snap the cursor to the pixel grid so it stays sharp. + cursor.p0.x = cursor.p0.x.trunc() + 0.5; + cursor.p1.x = cursor.p0.x; + ctx.with_save(|ctx| { ctx.clip(clip_rect); ctx.stroke(cursor, &cursor_color, 1.);