halley-wl 0.2.0

Wayland backend and rendering implementation for the Halley Wayland compositor.
use std::collections::HashMap;
use std::os::unix::io::AsFd;
use std::rc::Rc;

use smithay::{
    desktop::PopupManager,
    input::{Seat, SeatState, pointer::CursorImageStatus},
    reexports::{
        wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as XdgDecorationMode,
        wayland_server::{
            DisplayHandle, Resource, backend::ObjectId, protocol::wl_surface::WlSurface,
        },
    },
    wayland::{
        compositor::{CompositorState, add_blocker, with_states},
        cursor_shape::CursorShapeManagerState,
        dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufState},
        drm_syncobj::{DrmSyncPoint, DrmSyncobjCachedState, DrmSyncobjState},
        idle_notify::IdleNotifierState,
        output::OutputManagerState,
        pointer_constraints::PointerConstraintsState,
        relative_pointer::RelativePointerManagerState,
        selection::{
            data_device::DataDeviceState, primary_selection::PrimarySelectionState,
            wlr_data_control::DataControlState,
        },
        shell::wlr_layer::WlrLayerShellState,
        shell::xdg::{ToplevelState, XdgShellState, decoration::XdgDecorationState},
        shm::ShmState,
        viewporter::ViewporterState,
        xdg_activation::XdgActivationState,
    },
};

use super::root::Halley;
use crate::backend::interface::DmabufImportBackend;
use crate::protocol::wayland::ClientState;

fn preferred_xdg_decoration_mode_for() -> XdgDecorationMode {
    XdgDecorationMode::ServerSide
}

fn should_apply_toplevel_tiled_hint(fullscreen: bool) -> bool {
    !fullscreen
}

#[allow(dead_code)]
pub(crate) struct PlatformState {
    pub(crate) display_handle: DisplayHandle,
    pub(crate) compositor_state: CompositorState,
    pub(crate) viewporter_state: ViewporterState,
    pub(crate) xdg_shell_state: XdgShellState,
    pub(crate) xdg_activation_state: XdgActivationState,
    pub(crate) xdg_decoration_state: XdgDecorationState,
    pub(crate) cursor_shape_manager_state: CursorShapeManagerState,
    pub(crate) popup_manager: PopupManager,
    pub(crate) wlr_layer_shell_state: WlrLayerShellState,
    pub(crate) pointer_constraints_state: PointerConstraintsState,
    pub(crate) relative_pointer_manager_state: RelativePointerManagerState,
    pub(crate) idle_notifier_state: IdleNotifierState<Halley>,
    pub(crate) drm_syncobj_state: Option<DrmSyncobjState>,
    pub(crate) output_manager_state: OutputManagerState,
    pub(crate) shm_state: ShmState,
    pub(crate) dmabuf_state: DmabufState,
    pub(crate) dmabuf_global: Option<DmabufGlobal>,
    pub(crate) seat_state: SeatState<Halley>,
    pub(crate) data_device_state: DataDeviceState,
    pub(crate) primary_selection_state: PrimarySelectionState,
    pub(crate) data_control_state: DataControlState,
    pub(crate) session_lock: crate::protocol::wayland::session_lock::HalleySessionLockState,
    pub(crate) seat: Seat<Halley>,
    pub(crate) cursor_image_status: CursorImageStatus,
    pub(crate) dmabuf_importer: Option<Rc<dyn DmabufImportBackend>>,
    pub(crate) dmabuf_output_feedbacks: HashMap<String, DmabufFeedback>,
}

pub(crate) fn preferred_xdg_decoration_mode(st: &Halley) -> XdgDecorationMode {
    let _ = st;
    preferred_xdg_decoration_mode_for()
}

pub(crate) fn apply_toplevel_tiled_hint(_st: &Halley, state: &mut ToplevelState) {
    let tiled = should_apply_toplevel_tiled_hint(state.states.contains(
        smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::Fullscreen,
    ));
    for edge in [
        smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::TiledTop,
        smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::TiledBottom,
        smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::TiledLeft,
        smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::TiledRight,
    ] {
        if tiled {
            state.states.set(edge);
        } else {
            state.states.unset(edge);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use smithay::input::pointer::{CursorIcon, CursorImageStatus};

    #[test]
    fn preferred_decoration_mode_is_always_server_side() {
        assert_eq!(
            preferred_xdg_decoration_mode_for(),
            XdgDecorationMode::ServerSide
        );
    }

    #[test]
    fn tiled_hints_apply_to_all_non_fullscreen_toplevels() {
        assert!(should_apply_toplevel_tiled_hint(false));
        assert!(!should_apply_toplevel_tiled_hint(true));
    }

    #[test]
    fn idle_hide_does_not_hide_cursor_without_pointer_focus() {
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut tuning = halley_config::RuntimeTuning::default();
        tuning.cursor.hide_after_ms = 5_000;
        let mut state = Halley::new_for_test(&dh, tuning);
        state.platform.cursor_image_status = CursorImageStatus::default_named();
        state.input.interaction_state.last_cursor_activity_at_ms = 0;

        assert!(matches!(
            effective_cursor_image_status(&state),
            CursorImageStatus::Named(_)
        ));
    }

    #[test]
    fn compositor_override_icon_still_applies_without_pointer_focus() {
        let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
            .expect("display")
            .handle();
        let mut state = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
        state.platform.cursor_image_status = CursorImageStatus::Hidden;
        state.input.interaction_state.cursor_override_icon = Some(CursorIcon::Pointer);

        assert!(matches!(
            effective_cursor_image_status(&state),
            CursorImageStatus::Named(CursorIcon::Pointer)
        ));
    }
}

pub(crate) fn refresh_xdg_decoration_mode(st: &mut Halley) {
    let mode = preferred_xdg_decoration_mode(st);
    for toplevel in st.platform.xdg_shell_state.toplevel_surfaces() {
        toplevel.with_pending_state(|state| {
            state.decoration_mode = Some(mode);
            apply_toplevel_tiled_hint(st, state);
        });
        toplevel.send_configure();
    }
}

pub(crate) fn effective_cursor_image_status(st: &Halley) -> CursorImageStatus {
    let pointer_has_client_focus = st
        .platform
        .seat
        .get_pointer()
        .and_then(|pointer| pointer.current_focus())
        .is_some();

    if st.input.interaction_state.cursor_hidden_by_typing {
        return CursorImageStatus::Hidden;
    }

    let hide_after_ms = st.runtime.tuning.cursor.hide_after_ms;
    if hide_after_ms > 0 && pointer_has_client_focus {
        let now_ms = st.now_ms(std::time::Instant::now());
        if now_ms.saturating_sub(st.input.interaction_state.last_cursor_activity_at_ms)
            >= hide_after_ms
        {
            return CursorImageStatus::Hidden;
        }
    }

    if matches!(st.platform.cursor_image_status, CursorImageStatus::Hidden)
        && pointer_has_client_focus
    {
        return CursorImageStatus::Hidden;
    }

    st.input
        .interaction_state
        .cursor_override_icon
        .map(CursorImageStatus::Named)
        .unwrap_or_else(|| st.platform.cursor_image_status.clone())
}

pub(crate) fn install_drm_syncobj_blocker(st: &mut Halley, surface: &WlSurface) {
    if st.platform.drm_syncobj_state.is_none() {
        return;
    }

    let acquire_point = with_states(surface, |states| {
        let mut cached = states.cached_state.get::<DrmSyncobjCachedState>();
        cached.pending().acquire_point.clone()
    });

    let Some(acquire_point) = acquire_point else {
        return;
    };

    let blocker_state = SyncobjCommitBlockerState::default();
    add_blocker(
        surface,
        SyncobjCommitBlocker {
            state: blocker_state.clone(),
        },
    );
    spawn_drm_syncobj_waiter(st, surface.id(), acquire_point, blocker_state);
}

fn spawn_drm_syncobj_waiter(
    st: &Halley,
    surface_id: ObjectId,
    acquire_point: DrmSyncPoint,
    blocker_state: SyncobjCommitBlockerState,
) {
    let pending_surfaces = st.runtime.pending_drm_syncobj_surfaces.clone();
    std::thread::spawn(move || {
        let state = if acquire_point.wait(i64::MAX).is_ok() {
            SyncobjCommitBlockerStatus::Released
        } else {
            SyncobjCommitBlockerStatus::Cancelled
        };
        blocker_state.store(state);
        if let Ok(mut pending) = pending_surfaces.lock() {
            pending.push(surface_id);
        }
    });
}

pub(crate) fn drain_drm_syncobj_blockers(st: &mut Halley) {
    let surface_ids = match st.runtime.pending_drm_syncobj_surfaces.lock() {
        Ok(mut pending) => std::mem::take(&mut *pending),
        Err(_) => return,
    };
    let dh = st.platform.display_handle.clone();

    for surface_id in surface_ids {
        let Ok(client) = dh.get_client(surface_id) else {
            continue;
        };
        let Some(client_state) = client.get_data::<ClientState>() else {
            continue;
        };
        client_state.compositor_state.blocker_cleared(st, &dh);
    }
}

pub(crate) fn configure_dmabuf_importer(
    st: &mut Halley,
    importer: Rc<dyn DmabufImportBackend>,
    main_device: Option<rustix::fs::Dev>,
) {
    let formats = importer.dmabuf_formats();
    if formats.is_empty() {
        return;
    }

    let global = match main_device {
        Some(device) => {
            let feedback = DmabufFeedbackBuilder::new(device, formats.iter().copied())
                .build()
                .expect("renderer dmabuf feedback should be constructible");
            st.platform
                .dmabuf_state
                .create_global_with_default_feedback::<Halley>(
                    &st.platform.display_handle,
                    &feedback,
                )
        }
        None => st
            .platform
            .dmabuf_state
            .create_global::<Halley>(&st.platform.display_handle, formats.iter().copied()),
    };

    st.platform.dmabuf_importer = Some(importer);
    st.platform.dmabuf_global = Some(global);
}

pub(crate) fn configure_dmabuf_output_feedbacks(
    st: &mut Halley,
    output_feedbacks: HashMap<String, DmabufFeedback>,
) {
    st.platform.dmabuf_output_feedbacks = output_feedbacks;
}

pub(crate) fn configure_dmabuf_importer_for_fd<Fd: AsFd>(
    st: &mut Halley,
    importer: Rc<dyn DmabufImportBackend>,
    device_fd: Fd,
) {
    let main_device = rustix::fs::fstat(device_fd).ok().map(|stat| stat.st_rdev);
    configure_dmabuf_importer(st, importer, main_device);
}

#[inline]
pub fn note_input_activity(st: &mut Halley) {
    st.platform
        .idle_notifier_state
        .notify_activity(&st.platform.seat);
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SyncobjCommitBlockerStatus {
    Pending,
    Released,
    Cancelled,
}

#[derive(Clone, Debug)]
struct SyncobjCommitBlockerState(std::sync::Arc<std::sync::atomic::AtomicU8>);

impl Default for SyncobjCommitBlockerState {
    fn default() -> Self {
        Self(std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
            SyncobjCommitBlockerStatus::Pending as u8,
        )))
    }
}

impl SyncobjCommitBlockerState {
    fn store(&self, status: SyncobjCommitBlockerStatus) {
        self.0
            .store(status as u8, std::sync::atomic::Ordering::SeqCst);
    }

    fn load(&self) -> SyncobjCommitBlockerStatus {
        match self.0.load(std::sync::atomic::Ordering::SeqCst) {
            1 => SyncobjCommitBlockerStatus::Released,
            2 => SyncobjCommitBlockerStatus::Cancelled,
            _ => SyncobjCommitBlockerStatus::Pending,
        }
    }
}

#[derive(Clone, Debug)]
struct SyncobjCommitBlocker {
    state: SyncobjCommitBlockerState,
}

impl smithay::wayland::compositor::Blocker for SyncobjCommitBlocker {
    fn state(&self) -> smithay::wayland::compositor::BlockerState {
        match self.state.load() {
            SyncobjCommitBlockerStatus::Pending => {
                smithay::wayland::compositor::BlockerState::Pending
            }
            SyncobjCommitBlockerStatus::Released => {
                smithay::wayland::compositor::BlockerState::Released
            }
            SyncobjCommitBlockerStatus::Cancelled => {
                smithay::wayland::compositor::BlockerState::Cancelled
            }
        }
    }
}