use core::ffi::CStr;
use core::ptr::{self, NonNull};
use std::collections::HashMap;
use super::ffi::{self, Connection, CursorContext, CursorLib, Lib, Screen};
use crate::CursorIcon;
pub(super) const fn theme_names(icon: CursorIcon) -> &'static [&'static CStr] {
match icon {
CursorIcon::Default => &[c"default", c"left_ptr"],
CursorIcon::Pointer => &[c"pointer", c"hand2"],
CursorIcon::Text => &[c"text", c"xterm"],
CursorIcon::Crosshair => &[c"crosshair", c"cross"],
CursorIcon::Wait => &[c"wait", c"watch"],
CursorIcon::Progress => &[c"progress", c"left_ptr_watch", c"watch"],
CursorIcon::NotAllowed => &[c"not-allowed", c"crossed_circle", c"circle"],
CursorIcon::Grab => &[c"grab", c"openhand", c"hand1"],
CursorIcon::Grabbing => &[c"grabbing", c"closedhand", c"fleur"],
CursorIcon::Move => &[c"move", c"fleur"],
CursorIcon::ResizeNorthSouth => &[c"ns-resize", c"sb_v_double_arrow"],
CursorIcon::ResizeEastWest => &[c"ew-resize", c"sb_h_double_arrow"],
CursorIcon::ResizeNorthEastSouthWest => {
&[c"nesw-resize", c"fd_double_arrow", c"bottom_left_corner"]
}
CursorIcon::ResizeNorthWestSouthEast => {
&[c"nwse-resize", c"bd_double_arrow", c"bottom_right_corner"]
}
CursorIcon::ResizeColumn => &[c"col-resize", c"sb_h_double_arrow"],
CursorIcon::ResizeRow => &[c"row-resize", c"sb_v_double_arrow"],
}
}
fn resolve(
cache: &mut HashMap<CursorIcon, u32>,
icon: CursorIcon,
mut load: impl FnMut(&CStr) -> u32,
) -> u32 {
*cache.entry(icon).or_insert_with(|| {
theme_names(icon)
.iter()
.map(|name| load(name))
.find(|&cursor| cursor != ffi::value::NONE)
.unwrap_or(ffi::value::NONE)
})
}
struct Theme {
lib: &'static CursorLib,
context: NonNull<CursorContext>,
}
pub(super) struct Cursors {
theme: Option<Theme>,
loaded: HashMap<CursorIcon, u32>,
}
impl Cursors {
pub(super) fn unthemed() -> Self {
Self {
theme: None,
loaded: HashMap::new(),
}
}
pub(super) unsafe fn open(
lib: Option<&'static CursorLib>,
connection: *mut Connection,
screen: *mut Screen,
) -> Self {
let Some(lib) = lib else {
return Self::unthemed();
};
let mut context = ptr::null_mut();
let status = unsafe { (lib.context_new)(connection, screen, &raw mut context) };
let theme = match NonNull::new(context) {
Some(context) if status >= 0 => Some(Theme { lib, context }),
_ => {
crcbl_core::log::warn!(
"xcb_cursor_context_new failed ({status}): cursor shapes are recorded and \
not applied, so ShellCaps::CURSOR_SHAPE stays clear"
);
None
}
};
Self {
theme,
loaded: HashMap::new(),
}
}
pub(super) const fn applies_shapes(&self) -> bool {
self.theme.is_some()
}
pub(super) fn cursor(&mut self, icon: CursorIcon) -> u32 {
let Some(theme) = &self.theme else {
return ffi::value::NONE;
};
let (load, context) = (theme.lib.load_cursor, theme.context);
resolve(&mut self.loaded, icon, |name| {
unsafe { load(context.as_ptr(), name.as_ptr()) }
})
}
pub(super) unsafe fn release(&mut self, lib: &Lib, connection: *mut Connection) {
for (_, cursor) in self.loaded.drain() {
if cursor != ffi::value::NONE {
unsafe { (lib.free_cursor)(connection, cursor) };
}
}
if let Some(theme) = self.theme.take() {
unsafe { (theme.lib.context_free)(theme.context.as_ptr()) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const CORE_CURSOR_FONT: &[&str] = &[
"X_cursor",
"arrow",
"based_arrow_down",
"based_arrow_up",
"boat",
"bogosity",
"bottom_left_corner",
"bottom_right_corner",
"bottom_side",
"bottom_tee",
"box_spiral",
"center_ptr",
"circle",
"clock",
"coffee_mug",
"cross",
"cross_reverse",
"crosshair",
"diamond_cross",
"dot",
"dotbox",
"double_arrow",
"draft_large",
"draft_small",
"draped_box",
"exchange",
"fleur",
"gobbler",
"gumby",
"hand1",
"hand2",
"heart",
"icon",
"iron_cross",
"left_ptr",
"left_side",
"left_tee",
"leftbutton",
"ll_angle",
"lr_angle",
"man",
"middlebutton",
"mouse",
"pencil",
"pirate",
"plus",
"question_arrow",
"right_ptr",
"right_side",
"right_tee",
"rightbutton",
"rtl_logo",
"sailboat",
"sb_down_arrow",
"sb_h_double_arrow",
"sb_left_arrow",
"sb_right_arrow",
"sb_up_arrow",
"sb_v_double_arrow",
"shuttle",
"sizing",
"spider",
"spraycan",
"star",
"target",
"tcross",
"top_left_arrow",
"top_left_corner",
"top_right_corner",
"top_side",
"top_tee",
"trek",
"ul_angle",
"umbrella",
"ur_angle",
"watch",
"xterm",
];
fn names(icon: CursorIcon) -> Vec<&'static str> {
theme_names(icon)
.iter()
.map(|name| name.to_str().expect("an ASCII name"))
.collect()
}
#[test]
fn every_icon_tries_its_css_keyword_first_and_ends_on_a_core_font_glyph() {
for &icon in CursorIcon::ALL {
let names = names(icon);
assert!(names.len() >= 2, "{icon:?} has a fallback: {names:?}");
assert_eq!(names[0], icon.as_css_name(), "{icon:?} starts at CSS");
let last = names[names.len() - 1];
assert!(
CORE_CURSOR_FONT.contains(&last),
"{icon:?} ends on `{last}`, which the core cursor font lacks"
);
}
}
#[test]
fn the_legacy_names_are_the_ones_older_themes_ship() {
assert_eq!(names(CursorIcon::Default), ["default", "left_ptr"]);
assert_eq!(names(CursorIcon::Text), ["text", "xterm"]);
assert_eq!(names(CursorIcon::Pointer), ["pointer", "hand2"]);
assert_eq!(names(CursorIcon::Wait), ["wait", "watch"]);
assert_eq!(
names(CursorIcon::ResizeEastWest),
["ew-resize", "sb_h_double_arrow"]
);
}
#[test]
fn names_are_tried_in_order_until_one_loads() {
let mut cache = HashMap::new();
let mut tried = Vec::new();
let cursor = resolve(&mut cache, CursorIcon::Text, |name| {
tried.push(name.to_owned());
if name == c"xterm" {
7
} else {
ffi::value::NONE
}
});
assert_eq!(cursor, 7);
assert_eq!(tried, [c"text".to_owned(), c"xterm".to_owned()]);
let mut tried = 0;
let cursor = resolve(&mut cache, CursorIcon::Pointer, |_| {
tried += 1;
9
});
assert_eq!((cursor, tried), (9, 1));
}
#[test]
fn a_shape_is_loaded_once_including_one_that_loaded_nothing() {
let mut cache = HashMap::new();
let mut loads = 0;
for _ in 0..3 {
let cursor = resolve(&mut cache, CursorIcon::Default, |_| {
loads += 1;
5
});
assert_eq!(cursor, 5);
}
assert_eq!(loads, 1, "a repeat hover reuses the loaded cursor");
let mut loads = 0;
for _ in 0..3 {
let cursor = resolve(&mut cache, CursorIcon::Grab, |_| {
loads += 1;
ffi::value::NONE
});
assert_eq!(cursor, ffi::value::NONE);
}
assert_eq!(
loads,
theme_names(CursorIcon::Grab).len(),
"every name is tried once, and the miss is remembered"
);
}
#[test]
fn without_a_theme_no_shape_is_applied() {
let mut cursors = Cursors::unthemed();
assert!(!cursors.applies_shapes(), "CURSOR_SHAPE stays clear");
for &icon in CursorIcon::ALL {
assert_eq!(cursors.cursor(icon), ffi::value::NONE);
}
}
}