use crcbl_core::KeyCode;
use crcbl_core::input::{PointerButton, ScrollDelta};
use crate::linux::keymap;
use crate::linux::xkb::EVDEV_OFFSET;
#[must_use]
pub const fn scancode(keycode: u8) -> Option<u32> {
let keycode = keycode as u32;
if keycode < EVDEV_OFFSET {
return None;
}
Some(keycode - EVDEV_OFFSET)
}
#[must_use]
pub fn key_code(keycode: u8) -> Option<KeyCode> {
keymap::key_code(scancode(keycode)?)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Button {
Press(PointerButton),
Scroll(ScrollDelta),
ScrollRelease,
}
const LINES_PER_NOTCH: f32 = 1.0;
#[must_use]
pub const fn button(detail: u8, pressed: bool) -> Button {
match detail {
1 => Button::Press(PointerButton::Left),
2 => Button::Press(PointerButton::Middle),
3 => Button::Press(PointerButton::Right),
4..=7 if !pressed => Button::ScrollRelease,
4 => Button::Scroll(ScrollDelta::Lines {
x: 0.0,
y: LINES_PER_NOTCH,
}),
5 => Button::Scroll(ScrollDelta::Lines {
x: 0.0,
y: -LINES_PER_NOTCH,
}),
6 => Button::Scroll(ScrollDelta::Lines {
x: -LINES_PER_NOTCH,
y: 0.0,
}),
7 => Button::Scroll(ScrollDelta::Lines {
x: LINES_PER_NOTCH,
y: 0.0,
}),
8 => Button::Press(PointerButton::Back),
9 => Button::Press(PointerButton::Forward),
other => Button::Press(PointerButton::Other(evdev_of(other))),
}
}
const fn evdev_of(x11_button: u8) -> u16 {
const BTN_SIDE: u16 = 0x113;
const FIRST_DERIVED: u16 = 8;
BTN_SIDE + (x11_button as u16 - FIRST_DERIVED)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SizeHints {
pub flags: u32,
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub min_width: i32,
pub min_height: i32,
pub max_width: i32,
pub max_height: i32,
pub width_inc: i32,
pub height_inc: i32,
pub min_aspect_num: i32,
pub min_aspect_den: i32,
pub max_aspect_num: i32,
pub max_aspect_den: i32,
pub base_width: i32,
pub base_height: i32,
pub win_gravity: i32,
}
impl SizeHints {
pub const US_SIZE: u32 = 1 << 1;
pub const P_MIN_SIZE: u32 = 1 << 4;
pub const P_MAX_SIZE: u32 = 1 << 5;
pub const P_ASPECT: u32 = 1 << 7;
#[must_use]
pub const fn to_words(self) -> [i32; 18] {
[
self.flags as i32,
self.x,
self.y,
self.width,
self.height,
self.min_width,
self.min_height,
self.max_width,
self.max_height,
self.width_inc,
self.height_inc,
self.min_aspect_num,
self.min_aspect_den,
self.max_aspect_num,
self.max_aspect_den,
self.base_width,
self.base_height,
self.win_gravity,
]
}
#[must_use]
pub fn from_constraints(
constraints: crate::SizeConstraints,
resizable: bool,
current: crate::PhysicalSize,
scale_factor: f64,
) -> Self {
let mut hints = Self {
flags: Self::US_SIZE,
win_gravity: 1,
..Self::default()
};
if !resizable {
hints.flags |= Self::P_MIN_SIZE | Self::P_MAX_SIZE;
let (width, height) = clamp_extent(current.width, current.height);
hints.min_width = width;
hints.min_height = height;
hints.max_width = width;
hints.max_height = height;
return hints;
}
if let Some(min) = constraints.min {
let min = min.to_physical(scale_factor);
hints.flags |= Self::P_MIN_SIZE;
let (width, height) = clamp_extent(min.width, min.height);
hints.min_width = width;
hints.min_height = height;
}
if let Some(max) = constraints.max {
let max = max.to_physical(scale_factor);
hints.flags |= Self::P_MAX_SIZE;
let (width, height) = clamp_extent(max.width, max.height);
hints.max_width = width;
hints.max_height = height;
}
if let Some(aspect) = constraints.aspect {
hints.flags |= Self::P_ASPECT;
let numerator = i32::try_from(aspect.numerator()).unwrap_or(i32::MAX);
let denominator = i32::try_from(aspect.denominator()).unwrap_or(i32::MAX);
hints.min_aspect_num = numerator;
hints.min_aspect_den = denominator;
hints.max_aspect_num = numerator;
hints.max_aspect_den = denominator;
}
hints
}
}
const fn clamp_extent(width: u32, height: u32) -> (i32, i32) {
const fn clamp(value: u32) -> i32 {
if value > i32::MAX as u32 {
i32::MAX
} else {
value as i32
}
}
(clamp(width), clamp(height))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WmStateAction {
Remove = 0,
Add = 1,
}
#[must_use]
pub const fn net_wm_state_message(action: WmStateAction, first: u32, second: u32) -> [u32; 5] {
[action as u32, first, second, 1, 0]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AspectRatio, LogicalSize, PhysicalSize, SizeConstraints};
#[test]
fn x11_keycodes_are_evdev_plus_eight_and_the_low_ones_name_nothing() {
assert_eq!(scancode(38), Some(30));
assert_eq!(key_code(38), Some(KeyCode::KeyA));
assert_eq!(key_code(25), Some(KeyCode::KeyW), "evdev 17");
assert_eq!(key_code(39), Some(KeyCode::KeyS), "evdev 31");
assert_eq!(key_code(40), Some(KeyCode::KeyD), "evdev 32");
assert_eq!(key_code(65), Some(KeyCode::Space), "evdev 57");
assert_eq!(key_code(9), Some(KeyCode::Escape), "evdev 1");
for reserved in 0..8u8 {
assert_eq!(scancode(reserved), None, "keycode {reserved}");
assert_eq!(key_code(reserved), None, "keycode {reserved}");
}
assert_eq!(scancode(8), Some(0), "the offset itself is evdev 0");
}
#[test]
fn the_wheel_is_four_buttons_and_only_the_press_scrolls() {
assert_eq!(
button(4, true),
Button::Scroll(ScrollDelta::Lines { x: 0.0, y: 1.0 }),
"wheel up"
);
assert_eq!(
button(5, true),
Button::Scroll(ScrollDelta::Lines { x: 0.0, y: -1.0 }),
"wheel down"
);
assert_eq!(
button(6, true),
Button::Scroll(ScrollDelta::Lines { x: -1.0, y: 0.0 }),
"tilt left"
);
assert_eq!(
button(7, true),
Button::Scroll(ScrollDelta::Lines { x: 1.0, y: 0.0 }),
"tilt right"
);
for notch in 4..=7u8 {
assert_eq!(button(notch, false), Button::ScrollRelease, "{notch}");
}
}
#[test]
fn x11_numbers_the_middle_button_two_not_three() {
assert_eq!(button(1, true), Button::Press(PointerButton::Left));
assert_eq!(button(2, true), Button::Press(PointerButton::Middle));
assert_eq!(button(3, true), Button::Press(PointerButton::Right));
assert_eq!(button(8, true), Button::Press(PointerButton::Back));
assert_eq!(button(9, true), Button::Press(PointerButton::Forward));
assert_eq!(
button(10, true),
Button::Press(crate::linux::keymap::pointer_button(0x115))
);
assert_eq!(button(10, true), Button::Press(PointerButton::Other(0x115)));
assert_eq!(
button(10, false),
Button::Press(PointerButton::Other(0x115))
);
assert_eq!(button(11, true), Button::Press(PointerButton::Other(0x116)));
assert_eq!(
button(8, true),
Button::Press(crate::linux::keymap::pointer_button(0x113))
);
assert_eq!(
button(9, true),
Button::Press(crate::linux::keymap::pointer_button(0x114))
);
}
#[test]
fn size_hints_are_eighteen_words_with_the_flags_first() {
let hints = SizeHints::from_constraints(
SizeConstraints::NONE,
true,
PhysicalSize::new(800, 600),
1.0,
);
let words = hints.to_words();
assert_eq!(words.len(), 18);
assert_eq!(words[0], SizeHints::US_SIZE as i32);
assert_eq!(words[17], 1, "NorthWestGravity");
}
#[test]
fn an_aspect_lock_sets_both_bounds_to_the_same_ratio() {
let hints = SizeHints::from_constraints(
SizeConstraints::NONE.with_aspect(AspectRatio::WIDESCREEN),
true,
PhysicalSize::new(800, 600),
1.0,
);
assert!(hints.flags & SizeHints::P_ASPECT != 0);
assert_eq!(hints.min_aspect_num, 16);
assert_eq!(hints.min_aspect_den, 9);
assert_eq!(
(hints.min_aspect_num, hints.min_aspect_den),
(hints.max_aspect_num, hints.max_aspect_den),
"a range would not be a lock"
);
let words = hints.to_words();
assert_eq!(&words[11..15], &[16, 9, 16, 9]);
}
#[test]
fn constraints_are_logical_and_the_hints_are_pixels() {
let constraints = SizeConstraints {
min: Some(LogicalSize::new(320.0, 240.0)),
max: Some(LogicalSize::new(1920.0, 1080.0)),
aspect: None,
};
let one = SizeHints::from_constraints(constraints, true, PhysicalSize::new(800, 600), 1.0);
assert_eq!((one.min_width, one.min_height), (320, 240));
assert_eq!((one.max_width, one.max_height), (1920, 1080));
let two = SizeHints::from_constraints(constraints, true, PhysicalSize::new(800, 600), 2.0);
assert_eq!((two.min_width, two.min_height), (640, 480));
assert_eq!((two.max_width, two.max_height), (3840, 2160));
assert_eq!(
two.flags & (SizeHints::P_MIN_SIZE | SizeHints::P_MAX_SIZE),
SizeHints::P_MIN_SIZE | SizeHints::P_MAX_SIZE
);
}
#[test]
fn a_non_resizable_window_pins_min_and_max_to_its_current_size() {
let hints = SizeHints::from_constraints(
SizeConstraints::min(LogicalSize::new(100.0, 100.0)),
false,
PhysicalSize::new(1280, 720),
1.0,
);
assert_eq!((hints.min_width, hints.min_height), (1280, 720));
assert_eq!((hints.max_width, hints.max_height), (1280, 720));
assert_eq!(hints.flags & SizeHints::P_ASPECT, 0, "no aspect was asked");
}
#[test]
fn the_state_message_names_a_normal_application_as_its_source() {
let add = net_wm_state_message(WmStateAction::Add, 42, 0);
assert_eq!(add, [1, 42, 0, 1, 0]);
let remove = net_wm_state_message(WmStateAction::Remove, 42, 0);
assert_eq!(remove, [0, 42, 0, 1, 0]);
assert_eq!(add[3], 1, "source indication: normal application");
}
}