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 22, 2024
1 parent 4f59796 commit 6818b56
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 80 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
2 changes: 1 addition & 1 deletion examples/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl Application {
#[allow(unused_mut)]
let mut window_attributes = Window::default_attributes()
.with_title("Winit window")
.with_transparent(true)
//.with_transparent(true)
.with_window_icon(Some(self.icon.clone()));

#[cfg(any(x11_platform, wayland_platform))]
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::*;
200 changes: 126 additions & 74 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,119 @@ 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)
};
/// Create a cursor from an image.
fn create_cursor_from_image(
&self,
width: u16,
height: u16,
depth: u8,
hotspot_x: u16,
hotspot_y: u16,
image: &[u8],
) -> Result<xproto::Cursor, X11Error> {
// Create a pixap for the default root window.
let root = self.default_root().root;
let pixmap = self.xcb_connection().generate_id()?;
self.xcb_connection().create_pixmap(depth, pixmap, root, width, height)?.ignore_error();
let pixmap_guard = CallOnDrop(|| {
self.xcb_connection().free_pixmap(pixmap).map(|r| r.ignore_error()).ok();
});

// Create a GC to draw with.
let gc = self.xcb_connection().generate_id()?;
self.xcb_connection().create_gc(gc, pixmap, &Default::default())?.ignore_error();
let gc_guard = CallOnDrop(|| {
self.xcb_connection().free_gc(gc).map(|r| r.ignore_error()).ok();
});

// Draw the data into it.
let format =
if depth == 1 { xproto::ImageFormat::XY_PIXMAP } else { xproto::ImageFormat::Z_PIXMAP };
self.xcb_connection()
.put_image(format, pixmap, gc, width, height, 0, 0, 0, depth, image)?
.ignore_error();
drop(gc_guard);

if pixmap == 0 {
panic!("failed to allocate pixmap for cursor");
}
// Create the XRender picture.
let picture = self.xcb_connection().generate_id()?;
self.xcb_connection()
.render_create_picture(picture, pixmap, self.find_argb32_format(), &Default::default())?
.check()?;
let _picture_guard = CallOnDrop(|| {
self.xcb_connection().render_free_picture(picture).map(|r| r.ignore_error()).ok();
});
drop(pixmap_guard);

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

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(),
0,
0,
);
(self.xlib.XFreePixmap)(self.display, pixmap);

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 create_empty_cursor(&self) -> Result<xproto::Cursor, X11Error> {
self.create_cursor_from_image(1, 1, 1, 0, 0, &[0, 0, 0, 0])
}

fn get_cursor(&self, cursor: Option<CursorIcon>) -> ffi::Cursor {
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 +177,38 @@ 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");
}
(*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 }) }
}
// Reverse RGBA order to BGRA.
cursor.0.rgba.chunks_mut(4).for_each(|chunk| {
chunk[0..3].reverse();
});

let cursor = event_loop
.xconn
.create_cursor_from_image(
cursor.0.width,
cursor.0.height,
32,
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 All @@ -172,3 +217,10 @@ impl Default for SelectedCursor {
SelectedCursor::Named(Default::default())
}
}

struct CallOnDrop<F: FnMut()>(F);
impl<F: FnMut()> Drop for CallOnDrop<F> {
fn drop(&mut self) {
(self.0)();
}
}
24 changes: 20 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,12 @@ 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
}
}

impl fmt::Debug for XConnection {
Expand Down

0 comments on commit 6818b56

Please sign in to comment.