dioxus-dnd 3.1.0

Modular, accessible drag-and-drop for Dioxus: sortable lists, kanban boards, trees, grids, file drops, multi-select, touch support and more
Documentation
//! Linux backend policy. Portable Tao cursor polling and window-event release
//! handling remain in `fallback`; this module owns the genuinely Linux/X11
//! pieces: selecting the live backend and querying X11's primary-button mask
//! so a release over dead space cannot strand a drag. Tao's live event-loop
//! target is authoritative: X11 exposes global window geometry and a global
//! cursor, while Wayland deliberately exposes neither. WSLg follows whichever
//! backend Tao actually selected and has no special runtime branch.
//!
//! The pointer query rides a first-party x11rb connection rather than tao's
//! re-exported xlib FFI: that re-export is not part of tao's semver contract,
//! so a tao minor could strand this leg mid-2.x. A second client connection
//! to the X server is ordinary (xdotool works this way) and observation-only;
//! it never changes server state, so it cannot fight tao's own connection.

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
    }
}

/// The sampling connection plus the root window it queries. It lives only on
/// the dedicated sampler thread, so an X server roundtrip never blocks the
/// Dioxus UI executor.
struct X11Pointer {
    connection: RustConnection,
    root: Window,
}

fn query_x11_pointer(pointer: &X11Pointer) -> Option<X11PointerSample> {
    // A failed request or reply is a transient sample miss, retried by the
    // caller's loop - never a reason to reclassify the backend.
    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>>;

/// One process-wide, bounded X11 worker. At most one query is running and
/// one is queued; extra windows retry on their next 30 ms sampling tick.
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 {
            // The first event-loop target owns the immutable backend decision;
            // API failures later in the session cannot reclassify X11 as Wayland.
            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() {
            // Tao's live event-loop target owns the backend verdict; only a
            // confirmed X11 session may ever open the sampling connection.
            // Under XWayland an X connect would succeed anyway, so gating on
            // the connect instead of on tao would misclassify 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) {
                            // Press evidence lives across task restarts but is
                            // keyed to this composite generation. A modifier
                            // update retains ownership; drag N+1 cannot use it.
                            pressed_generation.set(Some(generation));
                            // Leg diagnostics: after an upstream update, a bug
                            // report's engaged-leg trace tells us which
                            // platform assumption moved.
                            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"
                        );
                        // Retire this run's proof before delivery invokes user
                        // code. A synchronously started N+1 can then install
                        // its own proof without this N callback erasing it.
                        pressed_generation.set(None);
                        // The same captured generation owns both the observed
                        // press and this final mutation; completion or a new
                        // begin invalidates it immediately before the call.
                        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
        );
    }
}