Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x11: Add support for custom cursors #1801

Merged
merged 8 commits into from
Jun 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ You can find its changes [documented below](#070---2021-01-01).
- `chrono` feature with `Data` support for [chrono](https://docs.rs/chrono/) types ([#1743] by [@r-ml])
- Text input handles Delete key ([#1746] by [@bjorn])
- `lens` macro can access nested fields ([#1764] by [@Maan2003])
- X11 backend now supports custom cursors ([#1801] by [@psychon])
- X11: Add support for transparent windows ([#1803] by [@psychon])
- `has_focus` method on `WidgetPod` ([#1825] by [@ForLoveOfCats])

Expand Down Expand Up @@ -728,6 +729,7 @@ Last release without a changelog :(
[#1764]: https://github.com/linebender/druid/pull/1764
[#1772]: https://github.com/linebender/druid/pull/1772
[#1787]: https://github.com/linebender/druid/pull/1787
[#1801]: https://github.com/linebender/druid/pull/1800
[#1802]: https://github.com/linebender/druid/pull/1802
[#1803]: https://github.com/linebender/druid/pull/1803
[#1820]: https://github.com/linebender/druid/pull/1820
Expand Down
2 changes: 1 addition & 1 deletion druid-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ glib = { version = "0.10.1", optional = true }
glib-sys = { version = "0.10.0", optional = true }
gtk-sys = { version = "0.10.0", optional = true }
nix = { version = "0.18.0", optional = true }
x11rb = { version = "0.8.0", features = ["allow-unsafe-code", "present", "randr", "xfixes", "resource_manager", "cursor"], optional = true }
x11rb = { version = "0.8.0", features = ["allow-unsafe-code", "present", "render", "randr", "xfixes", "resource_manager", "cursor"], optional = true }

[target.'cfg(target_arch="wasm32")'.dependencies]
wasm-bindgen = "0.2.67"
Expand Down
42 changes: 41 additions & 1 deletion druid-shell/src/platform/x11/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ use std::rc::Rc;
use std::time::{Duration, Instant};

use anyhow::{anyhow, Context, Error};
use x11rb::connection::Connection;
use x11rb::connection::{Connection, RequestConnection};
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,
Expand Down Expand Up @@ -90,6 +91,8 @@ pub(crate) struct Application {
idle_write: RawFd,
/// The major opcode of the Present extension, if it is supported.
present_opcode: Option<u8>,
/// Support for the render extension in at least version 0.5?
render_argb32_pictformat_cursor: Option<Pictformat>,
}

/// The mutable `Application` state.
Expand Down Expand Up @@ -145,6 +148,36 @@ impl Application {
}
};

let pictformats = connection.render_query_pict_formats()?;
let render_create_cursor_supported = matches!(connection
.extension_information(render::X11_EXTENSION_NAME)?
.and_then(|_| connection.render_query_version(0, 5).ok())
.map(|cookie| cookie.reply())
.transpose()?,
Some(version) if version.major_version >= 1 || version.minor_version >= 5);
let render_argb32_pictformat_cursor = if render_create_cursor_supported {
pictformats
.reply()?
.formats
.iter()
.find(|format| {
format.type_ == render::PictType::DIRECT
&& format.depth == 32
&& format.direct.red_shift == 16
&& format.direct.red_mask == 0xff
&& format.direct.green_shift == 8
&& format.direct.green_mask == 0xff
&& format.direct.blue_shift == 0
&& format.direct.blue_mask == 0xff
&& format.direct.alpha_shift == 24
&& format.direct.alpha_mask == 0xff
})
.map(|format| format.id)
} else {
drop(pictformats);
None
};

let handle = x11rb::cursor::Handle::new(connection.as_ref(), screen_num, &rdb)?.reply()?;
let load_cursor = |cursor| {
handle
Expand Down Expand Up @@ -185,6 +218,7 @@ impl Application {
root_visual_type,
argb_visual_type,
marker: std::marker::PhantomData,
render_argb32_pictformat_cursor,
})
}

Expand Down Expand Up @@ -235,6 +269,12 @@ impl Application {
self.present_opcode
}

/// Return the ARGB32 pictformat of the server, but only if RENDER's CreateCursor is supported
#[inline]
pub(crate) fn render_argb32_pictformat_cursor(&self) -> Option<Pictformat> {
self.render_argb32_pictformat_cursor
}

fn create_event_window(conn: &Rc<XCBConnection>, screen_num: i32) -> Result<u32, Error> {
let id = conn.generate_id()?;
let setup = conn.setup();
Expand Down
115 changes: 107 additions & 8 deletions druid-shell/src/platform/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

use std::cell::{Cell, RefCell};
use std::collections::BinaryHeap;
use std::convert::TryFrom;
use std::convert::{TryFrom, TryInto};
use std::os::unix::io::RawFd;
use std::panic::Location;
use std::rc::{Rc, Weak};
Expand All @@ -29,13 +29,15 @@ use cairo::{XCBConnection as CairoXCBConnection, XCBDrawable, XCBSurface, XCBVis
use tracing::{error, info, warn};
use x11rb::atom_manager;
use x11rb::connection::Connection;
use x11rb::errors::ReplyOrIdError;
use x11rb::properties::{WmHints, WmHintsState, WmSizeHints};
use x11rb::protocol::present::{CompleteNotifyEvent, ConnectionExt as _, IdleNotifyEvent};
use x11rb::protocol::render::{ConnectionExt as _, Pictformat};
use x11rb::protocol::xfixes::{ConnectionExt as _, Region as XRegion};
use x11rb::protocol::xproto::{
self, AtomEnum, ChangeWindowAttributesAux, ColormapAlloc, ConfigureNotifyEvent,
ConfigureWindowAux, ConnectionExt, CreateGCAux, EventMask, Gcontext, Pixmap, PropMode,
Rectangle, Visualtype, WindowClass,
ConfigureWindowAux, ConnectionExt, CreateGCAux, EventMask, Gcontext, ImageFormat,
ImageOrder as X11ImageOrder, Pixmap, PropMode, Rectangle, Visualtype, WindowClass,
};
use x11rb::wrapper::ConnectionExt as _;
use x11rb::xcb_ffi::XCBConnection;
Expand Down Expand Up @@ -1027,8 +1029,7 @@ impl Window {
Cursor::NotAllowed => cursors.not_allowed,
Cursor::ResizeLeftRight => cursors.col_resize,
Cursor::ResizeUpDown => cursors.row_resize,
// TODO: (x11/custom cursor)
Cursor::Custom(_) => None,
Cursor::Custom(custom) => Some(custom.0),
};
if cursor.is_none() {
warn!("Unable to load cursor {:?}", cursor);
Expand Down Expand Up @@ -1771,9 +1772,30 @@ impl WindowHandle {
}
}

pub fn make_cursor(&self, _cursor_desc: &CursorDesc) -> Option<Cursor> {
warn!("Custom cursors are not yet supported in the X11 backend");
None
pub fn make_cursor(&self, desc: &CursorDesc) -> Option<Cursor> {
if let Some(w) = self.window.upgrade() {
match w.app.render_argb32_pictformat_cursor() {
None => {
warn!("Custom cursors are not supported by the X11 server");
None
}
Some(format) => {
let conn = w.app.connection();
let setup = &conn.setup();
let screen = &setup.roots[w.app.screen_num() as usize];
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),
Err(err) => {
error!("Failed to create custom cursor: {:?}", err);
None
}
}
}
}
} else {
None
}
}

pub fn open_file(&mut self, _options: FileDialogOptions) -> Option<FileDialogToken> {
Expand Down Expand Up @@ -1829,3 +1851,80 @@ unsafe impl HasRawWindowHandle for WindowHandle {
RawWindowHandle::Xcb(handle)
}
}

fn make_cursor(
conn: &XCBConnection,
byte_order: X11ImageOrder,
root_window: u32,
argb32_format: Pictformat,
desc: &CursorDesc,
) -> Result<Cursor, ReplyOrIdError> {
// BEGIN: Lots of code just to get the image into a RENDER Picture

fn multiply_alpha(color: u8, alpha: u8) -> u8 {
let (color, alpha) = (u16::from(color), u16::from(alpha));
let temp = color * alpha + 0x80u16;
((temp + (temp >> 8)) >> 8) as u8
}

// No idea how to sanely get the pixel values, so I'll go with 'insane':
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImageBuf::raw_pixels should work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but then I need to handle all the different possible ImageFormat values. The GTK backend does not do that (I guess it only handles two of the four possible values properly), so this seems complicated/untested.

And with raw_pixels, RgbaSeparate still needs to be converted to premultiplied "by hand". The simplest way for that could be the already existing code, I guess.

// Iterate over all pixels and build an array
let pixels = desc
.image
.pixel_colors()
.flat_map(|row| {
row.flat_map(|color| {
let (r, g, b, a) = color.as_rgba8();
// RENDER wants premultiplied alpha
let (r, g, b) = (
multiply_alpha(r, a),
multiply_alpha(g, a),
multiply_alpha(b, a),
);
// piet gives us rgba in this order, the server expects an u32 with argb.
let (b0, b1, b2, b3) = match byte_order {
X11ImageOrder::LSB_FIRST => (b, g, r, a),
_ => (a, r, g, b),
};
// TODO Ownership and flat_map don't go well together :-(
vec![b0, b1, b2, b3]
})
})
.collect::<Vec<u8>>();
let width = desc.image.width().try_into().expect("Invalid cursor width");
let height = desc
.image
.height()
.try_into()
.expect("Invalid cursor height");

let pixmap = conn.generate_id()?;
let gc = conn.generate_id()?;
let picture = conn.generate_id()?;
conn.create_pixmap(32, pixmap, root_window, width, height)?;
conn.create_gc(gc, pixmap, &Default::default())?;

conn.put_image(
ImageFormat::Z_PIXMAP,
pixmap,
gc,
width,
height,
0,
0,
0,
32,
&pixels,
)?;
conn.render_create_picture(picture, pixmap, argb32_format, &Default::default())?;

conn.free_gc(gc)?;
conn.free_pixmap(pixmap)?;
// End: Lots of code just to get the image into a RENDER Picture

let cursor = conn.generate_id()?;
conn.render_create_cursor(cursor, picture, desc.hot.x as u16, desc.hot.y as u16)?;
conn.render_free_picture(picture)?;

Ok(Cursor::Custom(CustomCursor(cursor)))
}