feat: render named cursor images
This commit is contained in:
1 parent
37eb7cb9e9
commit
df884cb623
8 files changed
+239
-31
No files matched your search
Generated
+1
@@ -423,6 +423,7 @@ dependencies = [
|
|||||||
"smithay",
|
"smithay",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"xcursor",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ smithay = { version = "0.7.0", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||||
|
xcursor = "0.3.11"
|
||||||
@@ -13,3 +13,4 @@ libc.workspace = true
|
|||||||
smithay.workspace = true
|
smithay.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
xcursor.workspace = true
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
use std::{fs, sync::OnceLock};
|
||||||
|
|
||||||
|
use smithay::{
|
||||||
|
backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer},
|
||||||
|
input::pointer::CursorIcon,
|
||||||
|
utils::{Buffer, Point, Size, Transform},
|
||||||
|
};
|
||||||
|
use xcursor::{CursorTheme, parser::Image};
|
||||||
|
|
||||||
|
const DEFAULT_CURSOR_SIZE: u32 = 24;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct NamedCursor {
|
||||||
|
pub buffer: MemoryRenderBuffer,
|
||||||
|
pub hotspot: Point<i32, Buffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cursor_theme() -> CursorTheme {
|
||||||
|
let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into());
|
||||||
|
|
||||||
|
CursorTheme::load(&name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_named_cursor(theme: &CursorTheme, icon: CursorIcon) -> NamedCursor {
|
||||||
|
let requested_size = cursor_size();
|
||||||
|
let image = cursor_names(icon)
|
||||||
|
.iter()
|
||||||
|
.find_map(|name| load_image(theme, name, requested_size));
|
||||||
|
|
||||||
|
match image {
|
||||||
|
Some(image) => cursor_from_image(image),
|
||||||
|
None => fallback_cursor(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_size() -> u32 {
|
||||||
|
static SIZE: OnceLock<u32> = OnceLock::new();
|
||||||
|
|
||||||
|
*SIZE.get_or_init(|| {
|
||||||
|
std::env::var("XCURSOR_SIZE")
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.filter(|size| *size > 0)
|
||||||
|
.unwrap_or(DEFAULT_CURSOR_SIZE)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_names(icon: CursorIcon) -> [&'static str; 3] {
|
||||||
|
match icon {
|
||||||
|
CursorIcon::Default => ["default", "left_ptr", "arrow"],
|
||||||
|
CursorIcon::Pointer => ["pointer", "hand2", "left_ptr"],
|
||||||
|
CursorIcon::Text => ["text", "xterm", "left_ptr"],
|
||||||
|
_ => [icon.name(), "default", "left_ptr"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_image(theme: &CursorTheme, name: &str, requested_size: u32) -> Option<Image> {
|
||||||
|
let path = theme.load_icon(name)?;
|
||||||
|
let bytes = fs::read(path).ok()?;
|
||||||
|
|
||||||
|
xcursor::parser::parse_xcursor(&bytes)?
|
||||||
|
.into_iter()
|
||||||
|
.min_by_key(|image| {
|
||||||
|
(
|
||||||
|
image.size.abs_diff(requested_size),
|
||||||
|
image.width.abs_diff(requested_size),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_from_image(image: Image) -> NamedCursor {
|
||||||
|
let size = Size::<i32, Buffer>::from((image.width as i32, image.height as i32));
|
||||||
|
let buffer = MemoryRenderBuffer::from_slice(
|
||||||
|
&image.pixels_rgba,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
size,
|
||||||
|
1,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
NamedCursor {
|
||||||
|
buffer,
|
||||||
|
hotspot: Point::from((image.xhot as i32, image.yhot as i32)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_cursor() -> NamedCursor {
|
||||||
|
const WIDTH: usize = 12;
|
||||||
|
const HEIGHT: usize = 18;
|
||||||
|
let mut pixels = vec![0_u8; WIDTH * HEIGHT * 4];
|
||||||
|
|
||||||
|
for y in 0..HEIGHT {
|
||||||
|
let body_width = (y / 2 + 1).min(8);
|
||||||
|
|
||||||
|
for x in 0..body_width {
|
||||||
|
let edge = x == 0 || x + 1 == body_width || y == 0;
|
||||||
|
let offset = (y * WIDTH + x) * 4;
|
||||||
|
let color = if edge { 0 } else { 255 };
|
||||||
|
pixels[offset..offset + 4].copy_from_slice(&[color, color, color, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NamedCursor {
|
||||||
|
buffer: MemoryRenderBuffer::from_slice(
|
||||||
|
&pixels,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
Size::<i32, Buffer>::from((WIDTH as i32, HEIGHT as i32)),
|
||||||
|
1,
|
||||||
|
Transform::Normal,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
hotspot: Point::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use smithay::input::pointer::CursorIcon;
|
||||||
|
|
||||||
|
use super::cursor_names;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn supplies_legacy_fallback_names_for_common_cursors() {
|
||||||
|
assert_eq!(
|
||||||
|
cursor_names(CursorIcon::Default),
|
||||||
|
["default", "left_ptr", "arrow"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cursor_names(CursorIcon::Pointer),
|
||||||
|
["pointer", "hand2", "left_ptr"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cursor_names(CursorIcon::Text),
|
||||||
|
["text", "xterm", "left_ptr"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_default_for_other_named_cursors() {
|
||||||
|
assert_eq!(
|
||||||
|
cursor_names(CursorIcon::Crosshair),
|
||||||
|
["crosshair", "default", "left_ptr"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,11 @@ impl SeatHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
|
fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
|
||||||
|
if let CursorImageStatus::Named(icon) = &image {
|
||||||
|
self.named_cursors
|
||||||
|
.entry(*icon)
|
||||||
|
.or_insert_with(|| crate::cursor::load_named_cursor(&self.cursor_theme, *icon));
|
||||||
|
}
|
||||||
self.cursor_status = image;
|
self.cursor_status = image;
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![allow(irrefutable_let_patterns)]
|
#![allow(irrefutable_let_patterns)]
|
||||||
|
|
||||||
mod backends;
|
mod backends;
|
||||||
|
mod cursor;
|
||||||
mod grabs;
|
mod grabs;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod input;
|
mod input;
|
||||||
|
|||||||
+11
-1
@@ -16,7 +16,10 @@ use smithay::{
|
|||||||
LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output,
|
LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output,
|
||||||
utils::send_frames_surface_tree,
|
utils::send_frames_surface_tree,
|
||||||
},
|
},
|
||||||
input::{Seat, SeatState, pointer::CursorImageStatus},
|
input::{
|
||||||
|
Seat, SeatState,
|
||||||
|
pointer::{CursorIcon, CursorImageStatus},
|
||||||
|
},
|
||||||
output::Output,
|
output::Output,
|
||||||
reexports::{
|
reexports::{
|
||||||
calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic},
|
calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic},
|
||||||
@@ -62,6 +65,8 @@ pub struct Ferese {
|
|||||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
||||||
pub focused_window: Option<WindowId>,
|
pub focused_window: Option<WindowId>,
|
||||||
pub cursor_status: CursorImageStatus,
|
pub cursor_status: CursorImageStatus,
|
||||||
|
pub(crate) cursor_theme: xcursor::CursorTheme,
|
||||||
|
pub(crate) named_cursors: HashMap<CursorIcon, crate::cursor::NamedCursor>,
|
||||||
pub intercepted_keys: HashSet<smithay::input::keyboard::Keycode>,
|
pub intercepted_keys: HashSet<smithay::input::keyboard::Keycode>,
|
||||||
pub idle_inhibitors: HashMap<WlSurface, usize>,
|
pub idle_inhibitors: HashMap<WlSurface, usize>,
|
||||||
pub direct_backend: Option<crate::backends::direct::DirectBackendState>,
|
pub direct_backend: Option<crate::backends::direct::DirectBackendState>,
|
||||||
@@ -122,6 +127,9 @@ impl Ferese {
|
|||||||
let socket_name = Self::init_wayland_listener(display, event_loop)?;
|
let socket_name = Self::init_wayland_listener(display, event_loop)?;
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
let cursor_theme = crate::cursor::cursor_theme();
|
||||||
|
let default_cursor = crate::cursor::load_named_cursor(&cursor_theme, CursorIcon::Default);
|
||||||
|
let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
start_time,
|
start_time,
|
||||||
@@ -134,6 +142,8 @@ impl Ferese {
|
|||||||
window_geometry: HashMap::new(),
|
window_geometry: HashMap::new(),
|
||||||
focused_window: None,
|
focused_window: None,
|
||||||
cursor_status: CursorImageStatus::default_named(),
|
cursor_status: CursorImageStatus::default_named(),
|
||||||
|
cursor_theme,
|
||||||
|
named_cursors,
|
||||||
intercepted_keys: HashSet::new(),
|
intercepted_keys: HashSet::new(),
|
||||||
idle_inhibitors: HashMap::new(),
|
idle_inhibitors: HashMap::new(),
|
||||||
direct_backend: None,
|
direct_backend: None,
|
||||||
|
|||||||
+73
-30
@@ -9,6 +9,8 @@ use smithay::{
|
|||||||
damage::OutputDamageTracker,
|
damage::OutputDamageTracker,
|
||||||
element::{
|
element::{
|
||||||
AsRenderElements, Kind as RenderElementKind,
|
AsRenderElements, Kind as RenderElementKind,
|
||||||
|
memory::MemoryRenderBufferRenderElement,
|
||||||
|
render_elements,
|
||||||
surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree},
|
surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree},
|
||||||
utils::{
|
utils::{
|
||||||
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate,
|
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate,
|
||||||
@@ -35,10 +37,20 @@ use smithay::{
|
|||||||
|
|
||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
|
|
||||||
pub(crate) type AnimatedWindowRenderElement = CropRenderElement<
|
type SurfaceRenderElement = CropRenderElement<
|
||||||
RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>,
|
RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type MemoryRenderElement = CropRenderElement<
|
||||||
|
RelocateRenderElement<RescaleRenderElement<MemoryRenderBufferRenderElement<GlesRenderer>>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
render_elements! {
|
||||||
|
pub(crate) AnimatedWindowRenderElement<=GlesRenderer>;
|
||||||
|
Surface=SurfaceRenderElement,
|
||||||
|
Memory=MemoryRenderElement,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
|
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
|
||||||
let (mut backend, event_source) = winit::init()?;
|
let (mut backend, event_source) = winit::init()?;
|
||||||
let initial_scale = normalized_scale(backend.scale_factor());
|
let initial_scale = normalized_scale(backend.scale_factor());
|
||||||
@@ -281,7 +293,7 @@ fn layer_elements(
|
|||||||
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
||||||
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
||||||
|
|
||||||
CropRenderElement::from_element(element, scale, output_crop)
|
CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -292,42 +304,73 @@ fn cursor_elements(
|
|||||||
output_geometry: Rectangle<i32, Logical>,
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
let CursorImageStatus::Surface(surface) = &state.cursor_status else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let Some(pointer) = state.seat.get_pointer() else {
|
let Some(pointer) = state.seat.get_pointer() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let hotspot = with_states(surface, |states| {
|
|
||||||
states
|
|
||||||
.data_map
|
|
||||||
.get::<CursorImageSurfaceData>()
|
|
||||||
.map(|attributes| attributes.lock().unwrap().hotspot)
|
|
||||||
.unwrap_or_default()
|
|
||||||
});
|
|
||||||
let logical_location =
|
|
||||||
pointer.current_location() - output_geometry.loc.to_f64() - hotspot.to_f64();
|
|
||||||
let physical_location = logical_location.to_physical_precise_round(scale);
|
|
||||||
let output_crop =
|
let output_crop =
|
||||||
Rectangle::<i32, Logical>::from_size(output_geometry.size).to_physical_precise_round(scale);
|
Rectangle::<i32, Logical>::from_size(output_geometry.size).to_physical_precise_round(scale);
|
||||||
let origin = Point::<i32, Physical>::default();
|
let origin = Point::<i32, Physical>::default();
|
||||||
|
let pointer_location = pointer.current_location() - output_geometry.loc.to_f64();
|
||||||
|
match &state.cursor_status {
|
||||||
|
CursorImageStatus::Surface(surface) => {
|
||||||
|
let hotspot = with_states(surface, |states| {
|
||||||
|
states
|
||||||
|
.data_map
|
||||||
|
.get::<CursorImageSurfaceData>()
|
||||||
|
.map(|attributes| attributes.lock().unwrap().hotspot)
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let physical_location =
|
||||||
|
(pointer_location - hotspot.to_f64()).to_physical_precise_round(scale);
|
||||||
|
|
||||||
render_elements_from_surface_tree::<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>(
|
render_elements_from_surface_tree::<
|
||||||
renderer,
|
GlesRenderer,
|
||||||
surface,
|
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||||
physical_location,
|
>(
|
||||||
scale,
|
renderer,
|
||||||
1.0,
|
surface,
|
||||||
RenderElementKind::Cursor,
|
physical_location,
|
||||||
)
|
scale,
|
||||||
.into_iter()
|
1.0,
|
||||||
.filter_map(|element| {
|
RenderElementKind::Cursor,
|
||||||
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
)
|
||||||
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
.into_iter()
|
||||||
|
.filter_map(|element| {
|
||||||
|
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
||||||
|
let element =
|
||||||
|
RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
||||||
|
|
||||||
CropRenderElement::from_element(element, scale, output_crop)
|
CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
}
|
||||||
|
CursorImageStatus::Named(icon) => {
|
||||||
|
let Some(cursor) = state.named_cursors.get(icon) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let hotspot = Point::<i32, Physical>::from((cursor.hotspot.x, cursor.hotspot.y));
|
||||||
|
let physical_location = pointer_location.to_physical_precise_round(scale) - hotspot;
|
||||||
|
let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
|
||||||
|
renderer,
|
||||||
|
physical_location.to_f64(),
|
||||||
|
&cursor.buffer,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
RenderElementKind::Cursor,
|
||||||
|
) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let element = RescaleRenderElement::from_element(element, origin, 1.0);
|
||||||
|
let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative);
|
||||||
|
|
||||||
|
CropRenderElement::from_element(element, scale, output_crop)
|
||||||
|
.map(|element| vec![element.into()])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
CursorImageStatus::Hidden => Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalized_scale(scale: f64) -> f64 {
|
fn normalized_scale(scale: f64) -> f64 {
|
||||||
|
|||||||
Reference in new issue
Block a user