Skip to content

Commit

Permalink
m: Replace libxcursor with custom cursor code
Browse files Browse the repository at this point in the history
Another one bites the dust.

This replaces the code dependent on libxcursor with equivalent code
written using x11rb, featuring its special "cursor" module.

cc #3198

Signed-off-by: John Nunley <[email protected]>
  • Loading branch information
notgull committed Jun 24, 2024
1 parent c0c14aa commit 17c60e7
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 77 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ wayland-protocols-plasma = { version = "0.2.0", features = [
x11-dl = { version = "2.19.1", optional = true }
x11rb = { version = "0.13.0", default-features = false, features = [
"allow-unsafe-code",
"cursor",
"dl-libxcb",
"randr",
"resource_manager",
Expand Down
1 change: 1 addition & 0 deletions src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ changelog entry.

- On Web, let events wake up event loop immediately when using `ControlFlow::Poll`.
- Bump MSRV from `1.70` to `1.73`.
- On X11, remove our dependency on libXcursor. (#3749)

### Removed

Expand Down
1 change: 0 additions & 1 deletion src/platform_impl/linux/x11/ffi.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub use x11_dl::error::OpenError;
pub use x11_dl::xcursor::*;
pub use x11_dl::xinput2::*;
pub use x11_dl::xlib::*;
pub use x11_dl::xlib_xcb::*;
199 changes: 127 additions & 72 deletions src/platform_impl/linux/x11/util/cursor.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::ffi::CString;
use std::hash::{Hash, Hasher};
use std::iter;
use std::sync::Arc;
use std::{iter, slice};

use x11rb::connection::Connection;
use x11rb::protocol::render::{self, ConnectionExt as _};
use x11rb::protocol::xproto;

use crate::platform_impl::PlatformCustomCursorSource;
use crate::window::CursorIcon;
Expand All @@ -18,7 +19,7 @@ impl XConnection {
.lock()
.unwrap()
.entry(cursor)
.or_insert_with(|| self.get_cursor(cursor));
.or_insert_with(|| self.get_cursor(cursor).expect("failed to create cursor"));

self.update_cursor(window, cursor).expect("Failed to set cursor");
}
Expand All @@ -27,66 +28,123 @@ impl XConnection {
self.update_cursor(window, cursor.inner.cursor).expect("Failed to set cursor");
}

fn create_empty_cursor(&self) -> ffi::Cursor {
let data = 0;
let pixmap = unsafe {
let screen = (self.xlib.XDefaultScreen)(self.display);
let window = (self.xlib.XRootWindow)(self.display, screen);
(self.xlib.XCreateBitmapFromData)(self.display, window, &data, 1, 1)
};

if pixmap == 0 {
panic!("failed to allocate pixmap for cursor");
}

unsafe {
// We don't care about this color, since it only fills bytes
// in the pixmap which are not 0 in the mask.
let mut dummy_color = MaybeUninit::uninit();
let cursor = (self.xlib.XCreatePixmapCursor)(
self.display,
pixmap,
pixmap,
dummy_color.as_mut_ptr(),
dummy_color.as_mut_ptr(),
/// Create a cursor from an image.
fn create_cursor_from_image(
&self,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
image: &[u8],
) -> Result<xproto::Cursor, X11Error> {
// Create a pixmap for the default root window.
let root = self.default_root().root;
let pixmap =
xproto::PixmapWrapper::create_pixmap(self.xcb_connection(), 32, root, width, height)?;

// Create a GC to draw with.
let gc = xproto::GcontextWrapper::create_gc(
self.xcb_connection(),
pixmap.pixmap(),
&Default::default(),
)?;

// Draw the data into it.
self.xcb_connection()
.put_image(
xproto::ImageFormat::Z_PIXMAP,
pixmap.pixmap(),
gc.gcontext(),
width,
height,
0,
0,
);
(self.xlib.XFreePixmap)(self.display, pixmap);
0,
32,
image,
)?
.ignore_error();
drop(gc);

// Create the XRender picture.
let picture = render::PictureWrapper::create_picture(
self.xcb_connection(),
pixmap.pixmap(),
self.find_argb32_format(),
&Default::default(),
)?;
drop(pixmap);

// Create the cursor.
let cursor = self.xcb_connection().generate_id()?;
self.xcb_connection()
.render_create_cursor(cursor, picture.picture(), hotspot_x, hotspot_y)?
.check()?;

cursor
Ok(cursor)
}

/// Find the render format that corresponds to ARGB32.
fn find_argb32_format(&self) -> render::Pictformat {
macro_rules! direct {
($format:expr, $shift_name:ident, $mask_name:ident, $shift:expr) => {{
($format).direct.$shift_name == $shift && ($format).direct.$mask_name == 0xff
}};
}

self.render_formats()
.formats
.iter()
.find(|format| {
format.type_ == render::PictType::DIRECT
&& format.depth == 32
&& direct!(format, red_shift, red_mask, 16)
&& direct!(format, green_shift, green_mask, 8)
&& direct!(format, blue_shift, blue_mask, 0)
&& direct!(format, alpha_shift, alpha_mask, 24)
})
.expect("unable to find ARGB32 xrender format")
.id
}

fn get_cursor(&self, cursor: Option<CursorIcon>) -> ffi::Cursor {
fn create_empty_cursor(&self) -> Result<xproto::Cursor, X11Error> {
self.create_cursor_from_image(1, 1, 0, 0, &[0, 0, 0, 0])
}

fn get_cursor(&self, cursor: Option<CursorIcon>) -> Result<xproto::Cursor, X11Error> {
let cursor = match cursor {
Some(cursor) => cursor,
None => return self.create_empty_cursor(),
};

let mut xcursor = 0;
let database = self.database();
let handle = x11rb::cursor::Handle::new(
self.xcb_connection(),
self.default_screen_index(),
&database,
)?
.reply()?;

let mut last_error = None;
for &name in iter::once(&cursor.name()).chain(cursor.alt_names().iter()) {
let name = CString::new(name).unwrap();
xcursor = unsafe {
(self.xcursor.XcursorLibraryLoadCursor)(
self.display,
name.as_ptr() as *const c_char,
)
};

if xcursor != 0 {
break;
match handle.load_cursor(self.xcb_connection(), name) {
Ok(cursor) => return Ok(cursor),
Err(err) => last_error = Some(err.into()),
}
}

xcursor
Err(last_error.unwrap())
}

fn update_cursor(&self, window: xproto::Window, cursor: ffi::Cursor) -> Result<(), X11Error> {
fn update_cursor(
&self,
window: xproto::Window,
cursor: xproto::Cursor,
) -> Result<(), X11Error> {
self.xcb_connection()
.change_window_attributes(
window,
&xproto::ChangeWindowAttributesAux::new().cursor(cursor as xproto::Cursor),
&xproto::ChangeWindowAttributesAux::new().cursor(cursor),
)?
.ignore_error();

Expand Down Expand Up @@ -123,47 +181,44 @@ impl Eq for CustomCursor {}
impl CustomCursor {
pub(crate) fn new(
event_loop: &ActiveEventLoop,
cursor: PlatformCustomCursorSource,
mut cursor: PlatformCustomCursorSource,
) -> CustomCursor {
unsafe {
let ximage = (event_loop.xconn.xcursor.XcursorImageCreate)(
cursor.0.width as i32,
cursor.0.height as i32,
);
if ximage.is_null() {
panic!("failed to allocate cursor image");
// Reverse RGBA order to BGRA.
cursor.0.rgba.chunks_mut(4).for_each(|chunk| {
let chunk: &mut [u8; 4] = chunk.try_into().unwrap();
chunk[0..3].reverse();

// Byteswap if we need to.
if event_loop.xconn.needs_endian_swap() {
let value = u32::from_ne_bytes(*chunk).swap_bytes();
*chunk = value.to_ne_bytes();
}
(*ximage).xhot = cursor.0.hotspot_x as u32;
(*ximage).yhot = cursor.0.hotspot_y as u32;
(*ximage).delay = 0;

let dst = slice::from_raw_parts_mut((*ximage).pixels, cursor.0.rgba.len() / 4);
for (dst, chunk) in dst.iter_mut().zip(cursor.0.rgba.chunks_exact(4)) {
*dst = (chunk[0] as u32) << 16
| (chunk[1] as u32) << 8
| (chunk[2] as u32)
| (chunk[3] as u32) << 24;
}

let cursor =
(event_loop.xconn.xcursor.XcursorImageLoadCursor)(event_loop.xconn.display, ximage);
(event_loop.xconn.xcursor.XcursorImageDestroy)(ximage);
Self { inner: Arc::new(CustomCursorInner { xconn: event_loop.xconn.clone(), cursor }) }
}
});

let cursor = event_loop
.xconn
.create_cursor_from_image(
cursor.0.width,
cursor.0.height,
cursor.0.hotspot_x,
cursor.0.hotspot_y,
&cursor.0.rgba,
)
.expect("failed to create a custom cursor");

Self { inner: Arc::new(CustomCursorInner { xconn: event_loop.xconn.clone(), cursor }) }
}
}

#[derive(Debug)]
struct CustomCursorInner {
xconn: Arc<XConnection>,
cursor: ffi::Cursor,
cursor: xproto::Cursor,
}

impl Drop for CustomCursorInner {
fn drop(&mut self) {
unsafe {
(self.xconn.xlib.XFreeCursor)(self.xconn.display, self.cursor);
}
self.xconn.xcb_connection().free_cursor(self.cursor).map(|r| r.ignore_error()).ok();
}
}

Expand Down
35 changes: 31 additions & 4 deletions src/platform_impl/linux/x11/xdisplay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ use super::ffi;
use super::monitor::MonitorHandle;
use x11rb::connection::Connection;
use x11rb::protocol::randr::ConnectionExt as _;
use x11rb::protocol::render;
use x11rb::protocol::xproto::{self, ConnectionExt};
use x11rb::resource_manager;
use x11rb::xcb_ffi::XCBConnection;

/// A connection to an X server.
pub struct XConnection {
pub xlib: ffi::Xlib,
pub xcursor: ffi::Xcursor,

// TODO(notgull): I'd like to remove this, but apparently Xlib and Xinput2 are tied together
// for some reason.
Expand Down Expand Up @@ -55,8 +55,11 @@ pub struct XConnection {
/// Atom for the XSettings screen.
xsettings_screen: Option<xproto::Atom>,

/// XRender format information.
render_formats: render::QueryPictFormatsReply,

pub latest_error: Mutex<Option<XError>>,
pub cursor_cache: Mutex<HashMap<Option<CursorIcon>, ffi::Cursor>>,
pub cursor_cache: Mutex<HashMap<Option<CursorIcon>, xproto::Cursor>>,
}

unsafe impl Send for XConnection {}
Expand All @@ -69,7 +72,6 @@ impl XConnection {
pub fn new(error_handler: XErrorHandler) -> Result<XConnection, XNotSupported> {
// opening the libraries
let xlib = ffi::Xlib::open()?;
let xcursor = ffi::Xcursor::open()?;
let xlib_xcb = ffi::Xlib_xcb::open()?;
let xinput2 = ffi::XInput2::open()?;

Expand Down Expand Up @@ -118,15 +120,22 @@ impl XConnection {
tracing::warn!("error setting XSETTINGS; Xft options won't reload automatically")
}

// Start getting the XRender formats.
let formats_cookie = render::query_pict_formats(&xcb)
.map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?;

// Fetch atoms.
let atoms = Atoms::new(&xcb)
.map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?
.reply()
.map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?;

// Finish getting everything else.
let formats =
formats_cookie.reply().map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?;

Ok(XConnection {
xlib,
xcursor,
xinput2,
display,
xcb: Some(xcb),
Expand All @@ -138,6 +147,7 @@ impl XConnection {
database: RwLock::new(database),
cursor_cache: Default::default(),
randr_version: (randr_version.major_version, randr_version.minor_version),
render_formats: formats,
xsettings_screen,
})
}
Expand Down Expand Up @@ -257,6 +267,23 @@ impl XConnection {
pub fn xsettings_screen(&self) -> Option<xproto::Atom> {
self.xsettings_screen
}

/// Get the data containing our rendering formats.
#[inline]
pub fn render_formats(&self) -> &render::QueryPictFormatsReply {
&self.render_formats
}

/// Do we need to do an endian swap?
#[inline]
pub fn needs_endian_swap(&self) -> bool {
#[cfg(target_endian = "big")]
let endian = xproto::ImageOrder::MSB_FIRST;
#[cfg(not(target_endian = "big"))]
let endian = xproto::ImageOrder::LSB_FIRST;

self.xcb_connection().setup().image_byte_order != endian
}
}

impl fmt::Debug for XConnection {
Expand Down

0 comments on commit 17c60e7

Please sign in to comment.