use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use teksilo_canvas::Point;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::trace_input;
use teksilo_tokens::PenKind;
use wayland_backend::client::ObjectId;
use wayland_backend::sys::client::Backend;
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum, event_created_child};
use wayland_protocols::wp::tablet::zv2::client::{
zwp_tablet_manager_v2::ZwpTabletManagerV2,
zwp_tablet_pad_group_v2::{self, ZwpTabletPadGroupV2},
zwp_tablet_pad_ring_v2::ZwpTabletPadRingV2,
zwp_tablet_pad_strip_v2::ZwpTabletPadStripV2,
zwp_tablet_pad_v2::{self, ZwpTabletPadV2},
zwp_tablet_seat_v2::{self, ZwpTabletSeatV2},
zwp_tablet_tool_v2::{self, ZwpTabletToolV2},
zwp_tablet_v2::ZwpTabletV2,
};
use super::{PenButtons, PenCaps, PenPacket, PenSource};
use super::PEN_POLL_INTERVAL as POLL_INTERVAL;
const PRESSURE_RANGE: f32 = 65535.0;
const BTN_STYLUS: u32 = 0x14b;
const BTN_STYLUS2: u32 = 0x14c;
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ToolEvent {
Type(u32),
ProximityIn { surface: u32 },
ProximityOut,
Down,
Up,
Motion { x: f64, y: f64 },
Pressure(u32),
Tilt { x: f64, y: f64 },
Rotation(f64),
Button { button: u32, pressed: bool },
Frame { time_ms: u32 },
Removed,
}
pub fn tool_kind(raw: u32) -> Option<PenKind> {
match raw {
0x140 => Some(PenKind::Pen),
0x141 => Some(PenKind::Eraser),
0x142 => Some(PenKind::Brush),
0x143 => Some(PenKind::Pencil),
0x144 => Some(PenKind::Airbrush),
0x147 => Some(PenKind::Lens),
_ => None,
}
}
#[derive(Clone, Debug, Default)]
pub struct ToolState {
tool: Option<PenKind>,
surface: Option<u32>,
in_proximity: bool,
down: bool,
position: Point,
pressure: f32,
tilt: Option<(f32, f32)>,
twist: Option<f32>,
buttons: PenButtons,
reported_proximity: bool,
last_frame_ms: Option<u32>,
dirty: bool,
}
impl ToolState {
pub fn apply(&mut self, event: ToolEvent, our_surface: u32, out: &mut Vec<PenPacket>) {
match event {
ToolEvent::Type(raw) => self.tool = tool_kind(raw),
ToolEvent::ProximityIn { surface } => {
self.surface = Some(surface);
self.in_proximity = true;
self.down = false;
self.pressure = 0.0;
self.buttons = PenButtons::NONE;
self.dirty = true;
}
ToolEvent::ProximityOut => {
self.in_proximity = false;
self.down = false;
self.pressure = 0.0;
self.buttons = PenButtons::NONE;
self.dirty = true;
}
ToolEvent::Removed => {
self.in_proximity = false;
self.down = false;
self.pressure = 0.0;
self.buttons = PenButtons::NONE;
self.dirty = true;
if let Some(packet) = self.commit(our_surface, self.last_frame_ms) {
out.push(packet);
}
}
ToolEvent::Down => {
self.down = true;
self.dirty = true;
}
ToolEvent::Up => {
self.down = false;
self.pressure = 0.0;
self.dirty = true;
}
ToolEvent::Motion { x, y } => {
self.position = Point::new(x as f32, y as f32);
self.dirty = true;
}
ToolEvent::Pressure(raw) => {
self.pressure = (raw as f32 / PRESSURE_RANGE).clamp(0.0, 1.0);
self.dirty = true;
}
ToolEvent::Tilt { x, y } => {
self.tilt = Some(((x as f32).clamp(-90.0, 90.0), (y as f32).clamp(-90.0, 90.0)));
self.dirty = true;
}
ToolEvent::Rotation(degrees) => {
self.twist = Some(degrees.rem_euclid(360.0) as f32);
self.dirty = true;
}
ToolEvent::Button { button, pressed } => {
let which = match button {
BTN_STYLUS => PenButtons::BARREL,
BTN_STYLUS2 => PenButtons::SECONDARY_BARREL,
_ => return,
};
self.buttons = self.buttons.with(which, pressed);
self.dirty = true;
}
ToolEvent::Frame { time_ms } => {
self.last_frame_ms = Some(time_ms);
if let Some(packet) = self.commit(our_surface, Some(time_ms)) {
out.push(packet);
}
}
}
}
fn commit(&mut self, our_surface: u32, time_ms: Option<u32>) -> Option<PenPacket> {
if !self.dirty {
return None;
}
self.dirty = false;
let tool = self.tool?;
if self.surface != Some(our_surface) {
return None;
}
if !self.in_proximity && !self.reported_proximity {
return None;
}
self.reported_proximity = self.in_proximity;
if !self.in_proximity {
self.surface = None;
}
Some(PenPacket {
tool,
position: self.position,
pressure: self.pressure,
tilt: self.tilt,
twist: self.twist,
buttons: self.buttons,
in_proximity: self.in_proximity,
down: self.down,
device_time_ms: time_ms,
})
}
}
#[derive(Debug, Default)]
struct PenQueue {
packets: Mutex<Vec<PenPacket>>,
stop: AtomicBool,
has_tool: AtomicBool,
}
struct TabletState {
our_surface: u32,
queue: Arc<PenQueue>,
tools: HashMap<ObjectId, ToolState>,
_manager: ZwpTabletManagerV2,
_seat: WlSeat,
_tablet_seat: ZwpTabletSeatV2,
}
impl TabletState {
fn sync_tool_presence(&self) {
self.queue
.has_tool
.store(!self.tools.is_empty(), Ordering::Relaxed);
}
fn push(&self, packets: Vec<PenPacket>) {
if packets.is_empty() {
return;
}
if let Ok(mut queued) = self.queue.packets.lock() {
queued.extend(packets);
}
}
}
impl Dispatch<WlRegistry, GlobalListContents> for TabletState {
fn event(
_: &mut Self,
_: &WlRegistry,
_: <WlRegistry as Proxy>::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WlSeat, ()> for TabletState {
fn event(
_: &mut Self,
_: &WlSeat,
_: <WlSeat as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwpTabletManagerV2, ()> for TabletState {
fn event(
_: &mut Self,
_: &ZwpTabletManagerV2,
_: <ZwpTabletManagerV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwpTabletV2, ()> for TabletState {
fn event(
_: &mut Self,
_: &ZwpTabletV2,
_: <ZwpTabletV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwpTabletSeatV2, ()> for TabletState {
fn event(
state: &mut Self,
_: &ZwpTabletSeatV2,
event: <ZwpTabletSeatV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let zwp_tablet_seat_v2::Event::ToolAdded { id } = event {
state.tools.insert(id.id(), ToolState::default());
state.sync_tool_presence();
}
}
event_created_child!(TabletState, ZwpTabletSeatV2, [
zwp_tablet_seat_v2::EVT_TABLET_ADDED_OPCODE => (ZwpTabletV2, ()),
zwp_tablet_seat_v2::EVT_TOOL_ADDED_OPCODE => (ZwpTabletToolV2, ()),
zwp_tablet_seat_v2::EVT_PAD_ADDED_OPCODE => (ZwpTabletPadV2, ()),
]);
}
impl Dispatch<ZwpTabletToolV2, ()> for TabletState {
fn event(
state: &mut Self,
tool: &ZwpTabletToolV2,
event: <ZwpTabletToolV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let Some(translated) = translate_tool_event(&event) else {
return;
};
let our_surface = state.our_surface;
let mut out = Vec::new();
let removed = matches!(translated, ToolEvent::Removed);
let id = tool.id();
if let Some(accumulator) = state.tools.get_mut(&id) {
accumulator.apply(translated, our_surface, &mut out);
}
if removed {
state.tools.remove(&id);
state.sync_tool_presence();
}
state.push(out);
}
}
impl Dispatch<ZwpTabletPadV2, ()> for TabletState {
fn event(
_: &mut Self,
_: &ZwpTabletPadV2,
_: <ZwpTabletPadV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
event_created_child!(TabletState, ZwpTabletPadV2, [
zwp_tablet_pad_v2::EVT_GROUP_OPCODE => (ZwpTabletPadGroupV2, ()),
]);
}
impl Dispatch<ZwpTabletPadGroupV2, ()> for TabletState {
fn event(
_: &mut Self,
_: &ZwpTabletPadGroupV2,
_: <ZwpTabletPadGroupV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
event_created_child!(TabletState, ZwpTabletPadGroupV2, [
zwp_tablet_pad_group_v2::EVT_RING_OPCODE => (ZwpTabletPadRingV2, ()),
zwp_tablet_pad_group_v2::EVT_STRIP_OPCODE => (ZwpTabletPadStripV2, ()),
]);
}
impl Dispatch<ZwpTabletPadRingV2, ()> for TabletState {
fn event(
_: &mut Self,
_: &ZwpTabletPadRingV2,
_: <ZwpTabletPadRingV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwpTabletPadStripV2, ()> for TabletState {
fn event(
_: &mut Self,
_: &ZwpTabletPadStripV2,
_: <ZwpTabletPadStripV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
fn translate_tool_event(event: &zwp_tablet_tool_v2::Event) -> Option<ToolEvent> {
use zwp_tablet_tool_v2::Event as E;
Some(match event {
E::Type { tool_type } => ToolEvent::Type(match tool_type {
WEnum::Value(value) => *value as u32,
WEnum::Unknown(raw) => *raw,
}),
E::ProximityIn { surface, .. } => ToolEvent::ProximityIn {
surface: surface.id().protocol_id(),
},
E::ProximityOut => ToolEvent::ProximityOut,
E::Down { .. } => ToolEvent::Down,
E::Up => ToolEvent::Up,
E::Motion { x, y } => ToolEvent::Motion { x: *x, y: *y },
E::Pressure { pressure } => ToolEvent::Pressure(*pressure),
E::Tilt { tilt_x, tilt_y } => ToolEvent::Tilt {
x: *tilt_x,
y: *tilt_y,
},
E::Rotation { degrees } => ToolEvent::Rotation(*degrees),
E::Button { button, state, .. } => ToolEvent::Button {
button: *button,
pressed: matches!(
state,
WEnum::Value(zwp_tablet_tool_v2::ButtonState::Pressed)
),
},
E::Frame { time } => ToolEvent::Frame { time_ms: *time },
E::Removed => ToolEvent::Removed,
_ => return None,
})
}
#[derive(Debug)]
pub struct WaylandPenSource {
queue: Arc<PenQueue>,
}
const IDLE_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
impl WaylandPenSource {
fn poll_interval(has_tool: bool) -> std::time::Duration {
if has_tool {
POLL_INTERVAL
} else {
IDLE_POLL_INTERVAL
}
}
pub fn attach(parent: &ParentHandle) -> Option<Self> {
let RawDisplayHandle::Wayland(display) = parent.raw_display_handle() else {
return None;
};
let RawWindowHandle::Wayland(window) = parent.raw_window_handle() else {
return None;
};
let backend = unsafe { Backend::from_foreign_display(display.display.as_ptr() as *mut _) };
let conn = Connection::from_backend(backend);
let (globals, mut queue) = registry_queue_init::<TabletState>(&conn).ok()?;
let qh = queue.handle();
let manager = globals
.bind::<ZwpTabletManagerV2, _, _>(&qh, 1..=1, ())
.ok();
let Some(manager) = manager else {
trace_input!(
Samples,
"pen: the compositor advertises no zwp_tablet_manager_v2"
);
return None;
};
let seat = globals.bind::<WlSeat, _, _>(&qh, 1..=5, ()).ok()?;
let tablet_seat = manager.get_tablet_seat(&seat, &qh, ());
let our_surface = unsafe {
ObjectId::from_ptr(WlSurface::interface(), window.surface.as_ptr() as *mut _)
}
.ok()?
.protocol_id();
let shared = Arc::new(PenQueue::default());
let mut state = TabletState {
our_surface,
queue: shared.clone(),
tools: HashMap::new(),
_manager: manager,
_seat: seat,
_tablet_seat: tablet_seat,
};
let thread_queue = shared.clone();
std::thread::Builder::new()
.name(format!("teksilo-wayland-pen-{our_surface}"))
.spawn(move || {
while queue.dispatch_pending(&mut state).is_ok() {
if thread_queue.stop.load(Ordering::Relaxed) {
break;
}
let _ = conn.flush();
std::thread::sleep(Self::poll_interval(
thread_queue.has_tool.load(Ordering::Relaxed),
));
}
})
.ok()?;
Some(Self { queue: shared })
}
}
impl Drop for WaylandPenSource {
fn drop(&mut self) {
self.queue.stop.store(true, Ordering::Relaxed);
}
}
impl PenSource for WaylandPenSource {
fn poll(&mut self, out: &mut Vec<PenPacket>) {
if let Ok(mut packets) = self.queue.packets.lock() {
out.append(&mut packets);
}
}
fn capabilities(&self) -> PenCaps {
PenCaps::FULL_PEN
}
fn polls_off_thread(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests;