use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TryRecvError, TrySendError};
use std::sync::OnceLock;
use dioxus::prelude::*;
use dioxus_desktop::tao::platform::unix::EventLoopWindowTargetExtUnix;
use dioxus_desktop::use_wry_event_handler;
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{ConnectionExt, KeyButMask, Window};
use x11rb::rust_connection::RustConnection;
use crate::core::{DndContext, JoinedWindow};
use super::super::bridge::{subscribed_generation, BridgeGeneration};
use super::{fallback, GlobalCapability};
#[derive(Clone, Copy, Debug, PartialEq)]
struct X11PointerSample {
global: crate::core::Point,
primary_pressed: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum X11ReleaseAction {
Wait,
ObservePressed,
Release,
}
fn x11_release_action(
pressed_generation: Option<BridgeGeneration>,
current_generation: BridgeGeneration,
primary_pressed: bool,
) -> X11ReleaseAction {
if primary_pressed {
X11ReleaseAction::ObservePressed
} else if pressed_generation == Some(current_generation) {
X11ReleaseAction::Release
} else {
X11ReleaseAction::Wait
}
}
struct X11Pointer {
connection: RustConnection,
root: Window,
}
fn query_x11_pointer(pointer: &X11Pointer) -> Option<X11PointerSample> {
let reply = pointer
.connection
.query_pointer(pointer.root)
.ok()?
.reply()
.ok()?;
Some(X11PointerSample {
global: crate::core::Point::new(f64::from(reply.root_x), f64::from(reply.root_y)),
primary_pressed: u16::from(reply.mask) & u16::from(KeyButMask::BUTTON1) != 0,
})
}
type X11SampleReply = SyncSender<Option<X11PointerSample>>;
struct X11PointerSampler {
requests: SyncSender<X11SampleReply>,
}
impl X11PointerSampler {
fn new() -> Self {
let (requests, receiver) = sync_channel::<X11SampleReply>(1);
if let Err(error) = std::thread::Builder::new()
.name("dioxus-dnd-x11-pointer".to_string())
.spawn(move || run_x11_sampler(receiver))
{
tracing::warn!(%error, "failed to start X11 pointer sampler");
}
Self { requests }
}
async fn sample(&self) -> Option<X11PointerSample> {
let (reply, response) = sync_channel(1);
match self.requests.try_send(reply) {
Ok(()) => {}
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => return None,
}
loop {
match response.try_recv() {
Ok(sample) => return sample,
Err(TryRecvError::Disconnected) => return None,
Err(TryRecvError::Empty) => {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
}
}
}
}
fn run_x11_sampler(receiver: Receiver<X11SampleReply>) {
let Some(pointer) = x11rb::connect(None).ok().and_then(|(connection, screen)| {
let root = connection.setup().roots.get(screen)?.root;
Some(X11Pointer { connection, root })
}) else {
return;
};
while let Ok(reply) = receiver.recv() {
let _ = reply.send(query_x11_pointer(&pointer));
}
}
fn x11_pointer() -> &'static X11PointerSampler {
static POINTER: OnceLock<X11PointerSampler> = OnceLock::new();
POINTER.get_or_init(X11PointerSampler::new)
}
fn global_capability_for_backend(is_wayland: bool) -> GlobalCapability {
if is_wayland {
GlobalCapability::Unavailable
} else {
GlobalCapability::Available
}
}
pub(super) fn use_global_capability() -> Signal<GlobalCapability> {
let mut capability = use_signal(GlobalCapability::default);
use_wry_event_handler(move |_, target| {
if *capability.peek() == GlobalCapability::Unknown {
capability.set(global_capability_for_backend(target.is_wayland()));
}
});
capability
}
pub(super) fn use_portable_legs<T: Clone + PartialEq + 'static>(
joined: JoinedWindow<T>,
ctx: DndContext<T>,
capability: Signal<GlobalCapability>,
) {
fallback::use_cursor_poller_leg(joined, ctx, capability);
fallback::use_release_leg(joined, ctx, capability);
use_x11_dead_space_release(joined, ctx, capability);
}
fn use_x11_dead_space_release<T: Clone + PartialEq + 'static>(
joined: JoinedWindow<T>,
ctx: DndContext<T>,
capability: Signal<GlobalCapability>,
) {
let mut x11_confirmed = use_signal(|| false);
let mut pressed_generation = use_signal(|| None::<BridgeGeneration>);
use_wry_event_handler(move |_, target| {
if !*x11_confirmed.peek() && !target.is_wayland() {
x11_confirmed.set(true);
}
});
let _release_observer = use_resource(move || {
let sampler = x11_confirmed().then(x11_pointer);
let generation = subscribed_generation(joined);
let should_watch = generation.is_some_and(|generation| {
fallback::poller_owns_generation(joined, &ctx, capability, generation)
});
async move {
let Some((sampler, generation)) = sampler.zip(generation.filter(|_| should_watch))
else {
return;
};
let mut first_sample = true;
loop {
if !fallback::poller_owns_generation(joined, &ctx, capability, generation) {
break;
}
if first_sample {
first_sample = false;
} else {
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
}
if !fallback::poller_owns_generation(joined, &ctx, capability, generation) {
break;
}
let Some(sample) = sampler.sample().await else {
continue;
};
let pressed_generation_now = *pressed_generation.peek();
match x11_release_action(pressed_generation_now, generation, sample.primary_pressed)
{
X11ReleaseAction::Wait => {}
X11ReleaseAction::ObservePressed => {
if pressed_generation_now != Some(generation) {
pressed_generation.set(Some(generation));
tracing::debug!(
leg = "x11-deadspace",
event = "press-observed",
?generation,
"bridge leg engaged"
);
}
}
X11ReleaseAction::Release => {
tracing::debug!(
leg = "x11-deadspace",
event = "release",
?generation,
"bridge leg engaged"
);
pressed_generation.set(None);
if fallback::poller_owns_generation(joined, &ctx, capability, generation) {
joined.world.drop_at_global(sample.global);
}
break;
}
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_capability_selection_is_explicit() {
assert_eq!(
global_capability_for_backend(true),
GlobalCapability::Unavailable
);
assert_eq!(
global_capability_for_backend(false),
GlobalCapability::Available
);
}
#[test]
fn x11_release_requires_a_press_owned_by_the_same_run() {
let drag_n = BridgeGeneration {
world: 9,
session: None,
};
let drag_n_plus_one = BridgeGeneration {
world: 10,
session: None,
};
assert_eq!(
x11_release_action(None, drag_n, false),
X11ReleaseAction::Wait
);
assert_eq!(
x11_release_action(None, drag_n, true),
X11ReleaseAction::ObservePressed
);
assert_eq!(
x11_release_action(Some(drag_n), drag_n, true),
X11ReleaseAction::ObservePressed
);
assert_eq!(
x11_release_action(Some(drag_n), drag_n, false),
X11ReleaseAction::Release
);
assert_eq!(
x11_release_action(Some(drag_n), drag_n_plus_one, false),
X11ReleaseAction::Wait
);
}
}