halley-wl 0.3.1

Wayland backend and rendering implementation for the Halley Wayland compositor.
use std::collections::{HashMap, VecDeque};
use std::time::Instant;

use eventline::{debug, warn};
use smithay::reexports::wayland_server::{
    Resource, backend::ObjectId, protocol::wl_surface::WlSurface,
};
use smithay::utils::Serial;

use crate::compositor::root::Halley;

pub(crate) const ACTIVATION_TOKEN_TTL_MS: u64 = 10_000;
const RECENT_INPUT_SERIAL_TTL_MS: u64 = 10_000;

#[derive(Clone, Copy, Debug)]
struct RecentInputSerial {
    serial: Serial,
    at_ms: u64,
}

#[derive(Clone, Copy, Debug)]
struct IssuedExternalToken {
    issued_at_ms: u64,
}

#[derive(Clone, Copy, Debug)]
struct PendingSurfaceActivation {
    requested_at_ms: u64,
}

#[derive(Debug, Default)]
pub(crate) struct ActivationRuntimeState {
    recent_input_serials: VecDeque<RecentInputSerial>,
    issued_external_tokens: HashMap<String, IssuedExternalToken>,
    pending_surface_activations: HashMap<ObjectId, PendingSurfaceActivation>,
}

pub(crate) fn note_input_serial(st: &mut Halley, serial: Serial, now_ms: u64) {
    prune_recent_input_serials(st, now_ms);
    st.runtime
        .activation
        .recent_input_serials
        .retain(|entry| entry.serial != serial);
    st.runtime
        .activation
        .recent_input_serials
        .push_back(RecentInputSerial {
            serial,
            at_ms: now_ms,
        });
}

pub(crate) fn issue_external_token(st: &mut Halley, now_ms: u64) -> String {
    let (token, _) = st.platform.xdg_activation_state.create_external_token(None);
    let token = token.as_str().to_string();
    st.runtime.activation.issued_external_tokens.insert(
        token.clone(),
        IssuedExternalToken {
            issued_at_ms: now_ms,
        },
    );
    token
}

pub(crate) fn consume_pending_surface_activation(st: &mut Halley, surface: &WlSurface) -> bool {
    let now_ms = st.now_ms(Instant::now());
    st.runtime
        .activation
        .pending_surface_activations
        .remove(&surface_tree_root(surface).id())
        .is_some_and(|pending| {
            now_ms.saturating_sub(pending.requested_at_ms) <= ACTIVATION_TOKEN_TTL_MS
        })
}

pub(crate) fn clear_surface_activation(st: &mut Halley, surface: &WlSurface) {
    clear_surface_activation_for_root(st, surface_tree_root(surface).id());
}

pub(crate) fn clear_surface_activation_for_root(st: &mut Halley, surface_id: ObjectId) {
    st.runtime
        .activation
        .pending_surface_activations
        .remove(&surface_id);
}

pub(crate) fn prune_expired(st: &mut Halley, now: Instant, now_ms: u64) {
    prune_recent_input_serials(st, now_ms);
    st.runtime
        .activation
        .issued_external_tokens
        .retain(|_, issued| now_ms.saturating_sub(issued.issued_at_ms) <= ACTIVATION_TOKEN_TTL_MS);
    st.runtime
        .activation
        .pending_surface_activations
        .retain(|_, pending| {
            now_ms.saturating_sub(pending.requested_at_ms) <= ACTIVATION_TOKEN_TTL_MS
        });
    st.platform
        .xdg_activation_state
        .retain_tokens(|_, data| age_ms(now, data.timestamp) <= ACTIVATION_TOKEN_TTL_MS);
}

pub(crate) fn request_surface_activation(
    st: &mut Halley,
    surface: &WlSurface,
    token: &str,
    token_data: &smithay::wayland::xdg_activation::XdgActivationTokenData,
    now: Instant,
) {
    let root = surface_tree_root(surface);
    let now_ms = st.now_ms(now);

    if !activation_token_is_valid(st, token, token_data, now, now_ms) {
        debug!(
            "ignoring invalid activation request token={} surface={:?}",
            token,
            root.id()
        );
        forget_token(st, token);
        return;
    }

    forget_token(st, token);

    if let Some(id) = st.model.surface_to_node.get(&root.id()).copied() {
        let activated =
            crate::compositor::actions::window::focus_surface_node_without_reveal(st, id, now);
        debug!(
            "applied activation request token={} node={} surface={:?} activated={}",
            token,
            id.as_u64(),
            root.id(),
            activated
        );
        return;
    }

    st.runtime.activation.pending_surface_activations.insert(
        root.id(),
        PendingSurfaceActivation {
            requested_at_ms: now_ms,
        },
    );
    debug!(
        "queued activation request token={} for pending surface {:?}",
        token,
        root.id()
    );
}

fn activation_token_is_valid(
    st: &Halley,
    token: &str,
    token_data: &smithay::wayland::xdg_activation::XdgActivationTokenData,
    now: Instant,
    now_ms: u64,
) -> bool {
    if st
        .runtime
        .activation
        .issued_external_tokens
        .get(token)
        .is_some_and(|issued| now_ms.saturating_sub(issued.issued_at_ms) <= ACTIVATION_TOKEN_TTL_MS)
    {
        return true;
    }

    if age_ms(now, token_data.timestamp) > ACTIVATION_TOKEN_TTL_MS {
        return false;
    }

    let Some(serial) = token_data.serial.as_ref().map(|(serial, _seat)| *serial) else {
        return false;
    };

    has_recent_input_serial(st, serial, now_ms)
}

fn has_recent_input_serial(st: &Halley, serial: Serial, now_ms: u64) -> bool {
    st.runtime
        .activation
        .recent_input_serials
        .iter()
        .any(|entry| {
            entry.serial == serial
                && now_ms.saturating_sub(entry.at_ms) <= RECENT_INPUT_SERIAL_TTL_MS
        })
}

fn forget_token(st: &mut Halley, token: &str) {
    st.runtime.activation.issued_external_tokens.remove(token);
    let removed = st
        .platform
        .xdg_activation_state
        .remove_token(&token.to_string().into());
    if !removed {
        warn!("activation token {} was not tracked by smithay", token);
    }
}

fn prune_recent_input_serials(st: &mut Halley, now_ms: u64) {
    while st
        .runtime
        .activation
        .recent_input_serials
        .front()
        .is_some_and(|entry| now_ms.saturating_sub(entry.at_ms) > RECENT_INPUT_SERIAL_TTL_MS)
    {
        st.runtime.activation.recent_input_serials.pop_front();
    }
}

fn age_ms(now: Instant, timestamp: Instant) -> u64 {
    now.checked_duration_since(timestamp)
        .unwrap_or_default()
        .as_millis() as u64
}

fn surface_tree_root(surface: &WlSurface) -> WlSurface {
    let mut root = surface.clone();
    while let Some(parent) = smithay::wayland::compositor::get_parent(&root) {
        root = parent;
    }
    root
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn external_token_is_valid_until_ttl() {
        let mut activation = ActivationRuntimeState::default();
        activation.issued_external_tokens.insert(
            "token".to_string(),
            IssuedExternalToken { issued_at_ms: 100 },
        );

        assert!(
            activation
                .issued_external_tokens
                .get("token")
                .is_some_and(|issued| 10_099u64.saturating_sub(issued.issued_at_ms)
                    <= ACTIVATION_TOKEN_TTL_MS)
        );
        assert!(
            !activation
                .issued_external_tokens
                .get("token")
                .is_some_and(|issued| 10_101u64.saturating_sub(issued.issued_at_ms)
                    <= ACTIVATION_TOKEN_TTL_MS)
        );
    }

    #[test]
    fn recent_input_serial_check_is_age_bounded() {
        let serial = Serial::from(77);
        let recent = RecentInputSerial { serial, at_ms: 300 };

        assert!(recent.serial == serial);
        assert!(9_900u64.saturating_sub(recent.at_ms) <= RECENT_INPUT_SERIAL_TTL_MS);
        assert!(10_301u64.saturating_sub(recent.at_ms) > RECENT_INPUT_SERIAL_TTL_MS);
    }
}