halley-wl 0.3.0

Wayland backend and rendering implementation for the Halley Wayland compositor.
use std::collections::HashMap;

use halley_core::cluster_layout::{ClusterWorkspaceLayoutKind, cluster_visible_limit};
use halley_core::field::NodeId;
use smithay::reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface};
use smithay::wayland::compositor::get_parent;

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

pub(crate) fn is_active_cluster_workspace_member(
    st: &Halley,
    node_id: halley_core::field::NodeId,
) -> bool {
    st.model
        .field
        .cluster_id_for_member_public(node_id)
        .zip(st.model.monitor_state.node_monitor.get(&node_id))
        .is_some_and(|(cid, monitor)| st.active_cluster_workspace_for_monitor(monitor) == Some(cid))
}

pub(crate) fn active_stacking_front_member_for_monitor(
    st: &Halley,
    monitor: &str,
) -> Option<halley_core::field::NodeId> {
    if !matches!(
        st.runtime.tuning.cluster_layout_kind(),
        ClusterWorkspaceLayoutKind::Stacking
    ) {
        return None;
    }
    let cid = st.active_cluster_workspace_for_monitor(monitor)?;
    st.model.field.cluster(cid)?.members().first().copied()
}

pub(crate) fn active_stacking_visible_members_for_monitor(
    st: &Halley,
    monitor: &str,
) -> Vec<halley_core::field::NodeId> {
    if !matches!(
        st.runtime.tuning.cluster_layout_kind(),
        ClusterWorkspaceLayoutKind::Stacking
    ) {
        return Vec::new();
    }
    let Some(cid) = st.active_cluster_workspace_for_monitor(monitor) else {
        return Vec::new();
    };
    let Some(cluster) = st.model.field.cluster(cid) else {
        return Vec::new();
    };
    let visible_len = cluster_visible_limit(
        ClusterWorkspaceLayoutKind::Stacking,
        st.runtime.tuning.active_cluster_visible_limit(),
    )
    .min(cluster.members().len());
    cluster
        .members()
        .iter()
        .take(visible_len)
        .copied()
        .collect()
}

pub(crate) fn is_active_stacking_workspace_member(
    st: &Halley,
    node_id: halley_core::field::NodeId,
) -> bool {
    let Some(monitor) = st.model.monitor_state.node_monitor.get(&node_id) else {
        return false;
    };
    active_stacking_visible_members_for_monitor(st, monitor.as_str()).contains(&node_id)
}

pub(crate) fn node_allows_interactive_resize(st: &Halley, node_id: NodeId) -> bool {
    st.model
        .field
        .node(node_id)
        .is_some_and(|node| node.state == halley_core::field::NodeState::Active)
        && !node_blocks_interactive_transform(st, node_id)
        && !crate::compositor::workspace::state::node_in_maximize_session(st, node_id)
        && !is_active_stacking_workspace_member(st, node_id)
        && !(matches!(
            st.runtime.tuning.cluster_layout_kind(),
            ClusterWorkspaceLayoutKind::Tiling
        ) && is_active_cluster_workspace_member(st, node_id))
}

pub(crate) fn node_blocks_interactive_transform(st: &Halley, node_id: NodeId) -> bool {
    st.model.field.node(node_id).is_some_and(|node| node.pinned)
        || st.fullscreen_monitor_for_node(node_id).is_some()
        || active_pointer_constraint_belongs_to_node(st, node_id)
        || (crate::window::node_is_game_like(st, node_id)
            && node_covers_assigned_output(st, node_id))
}

fn active_pointer_constraint_belongs_to_node(st: &Halley, node_id: NodeId) -> bool {
    crate::compositor::interaction::pointer::active_constrained_pointer_surface(st)
        .and_then(|(surface, _)| root_surface_node_id(st, &surface))
        == Some(node_id)
}

fn root_surface_node_id(st: &Halley, surface: &WlSurface) -> Option<NodeId> {
    let mut root = surface.clone();
    while let Some(parent) = get_parent(&root) {
        root = parent;
    }
    st.model.surface_to_node.get(&root.id()).copied()
}

fn node_covers_assigned_output(st: &Halley, node_id: NodeId) -> bool {
    let Some(node) = st.model.field.node(node_id) else {
        return false;
    };
    if node.kind != halley_core::field::NodeKind::Surface || !st.model.field.is_visible(node_id) {
        return false;
    }
    let Some(monitor_name) = st.model.monitor_state.node_monitor.get(&node_id) else {
        return false;
    };
    let Some(space) = st.model.monitor_state.monitors.get(monitor_name.as_str()) else {
        return false;
    };

    let tolerance = 4.0;
    let output_size = space.viewport.size;
    let output_center = space.viewport.center;
    let size = node.footprint;

    size.x >= output_size.x - tolerance
        && size.y >= output_size.y - tolerance
        && (node.pos.x - output_center.x).abs() <= tolerance
        && (node.pos.y - output_center.y).abs() <= tolerance
}

pub(crate) fn stacking_render_order_map(
    members: &[halley_core::field::NodeId],
    max_visible: usize,
) -> HashMap<halley_core::field::NodeId, usize> {
    let visible_len =
        cluster_visible_limit(ClusterWorkspaceLayoutKind::Stacking, max_visible).min(members.len());
    members
        .iter()
        .take(visible_len)
        .enumerate()
        .map(|(index, &node_id)| (node_id, visible_len.saturating_sub(index + 1)))
        .collect()
}

pub(crate) fn active_stacking_render_order_for_monitor(
    st: &Halley,
    monitor: &str,
) -> HashMap<halley_core::field::NodeId, usize> {
    if !matches!(
        st.runtime.tuning.cluster_layout_kind(),
        ClusterWorkspaceLayoutKind::Stacking
    ) {
        return HashMap::new();
    }
    let Some(cid) = st.active_cluster_workspace_for_monitor(monitor) else {
        return HashMap::new();
    };
    let Some(cluster) = st.model.field.cluster(cid) else {
        return HashMap::new();
    };
    stacking_render_order_map(
        cluster.members(),
        st.runtime.tuning.active_cluster_visible_limit(),
    )
}

pub(crate) fn stack_focus_target_for_node(
    st: &Halley,
    node_id: halley_core::field::NodeId,
) -> Option<halley_core::field::NodeId> {
    let monitor = st.model.monitor_state.node_monitor.get(&node_id)?;
    let cid = st.model.field.cluster_id_for_member_public(node_id)?;
    (st.active_cluster_workspace_for_monitor(monitor.as_str()) == Some(cid))
        .then(|| active_stacking_front_member_for_monitor(st, monitor.as_str()))
        .flatten()
}

#[cfg(test)]
mod tests {
    use super::*;
    use halley_core::field::Vec2;
    use smithay::reexports::wayland_server::Display;
    use std::time::Instant;

    #[test]
    fn active_cluster_workspace_member_matches_current_monitor_workspace() {
        let dh = Display::<Halley>::new().expect("display").handle();
        let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());

        let monitor = st.model.monitor_state.current_monitor.clone();
        let master = st.model.field.spawn_surface(
            "master",
            Vec2 { x: 100.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        let stack = st.model.field.spawn_surface(
            "stack",
            Vec2 { x: 500.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        st.assign_node_to_monitor(master, monitor.as_str());
        st.assign_node_to_monitor(stack, monitor.as_str());

        let cid = st.create_cluster(vec![master, stack]).expect("cluster");
        let core = st.collapse_cluster(cid).expect("core");
        st.assign_node_to_monitor(core, monitor.as_str());

        assert!(!is_active_cluster_workspace_member(&st, master));
        assert!(st.toggle_cluster_workspace_by_core(core, Instant::now()));
        assert!(is_active_cluster_workspace_member(&st, master));
        assert!(is_active_cluster_workspace_member(&st, stack));
        assert!(!is_active_cluster_workspace_member(&st, core));
    }

    #[test]
    fn active_stacking_helpers_report_front_member_and_render_order() {
        let dh = Display::<Halley>::new().expect("display").handle();
        let mut tuning = halley_config::RuntimeTuning::default();
        tuning.stacking_max_visible = 3;
        let mut st = Halley::new_for_test(&dh, tuning);

        let monitor = st.model.monitor_state.current_monitor.clone();
        let a = st.model.field.spawn_surface(
            "A",
            Vec2 { x: 100.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        let b = st.model.field.spawn_surface(
            "B",
            Vec2 { x: 120.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        let c = st.model.field.spawn_surface(
            "C",
            Vec2 { x: 140.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        let d = st.model.field.spawn_surface(
            "D",
            Vec2 { x: 160.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        for id in [a, b, c, d] {
            st.assign_node_to_monitor(id, monitor.as_str());
        }

        let cid = st.create_cluster(vec![a, b, c, d]).expect("cluster");
        let core = st.collapse_cluster(cid).expect("core");
        st.assign_node_to_monitor(core, monitor.as_str());
        assert!(st.toggle_cluster_workspace_by_core(core, Instant::now()));

        assert_eq!(
            active_stacking_front_member_for_monitor(&st, monitor.as_str()),
            Some(a)
        );

        let ranks = active_stacking_render_order_for_monitor(&st, monitor.as_str());
        assert_eq!(ranks.get(&a), Some(&2));
        assert_eq!(ranks.get(&b), Some(&1));
        assert_eq!(ranks.get(&c), Some(&0));
        assert_eq!(ranks.get(&d), None);
        assert_eq!(stack_focus_target_for_node(&st, c), Some(a));
    }

    #[test]
    fn maximize_session_blocks_interactive_resize() {
        let dh = Display::<Halley>::new().expect("display").handle();
        let mut tuning = halley_config::RuntimeTuning::default();
        tuning.animations.maximize.enabled = false;
        let mut st = Halley::new_for_test(&dh, tuning);

        let monitor = st.model.monitor_state.current_monitor.clone();
        let window = st.model.field.spawn_surface(
            "window",
            Vec2 { x: 100.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        st.assign_node_to_monitor(window, monitor.as_str());

        assert!(
            crate::compositor::actions::window::toggle_node_maximize_state(
                &mut st,
                window,
                Instant::now(),
                monitor.as_str(),
            )
        );

        assert!(!node_allows_interactive_resize(&st, window));
    }

    #[test]
    fn output_sized_game_like_surface_blocks_interactive_resize() {
        let dh = Display::<Halley>::new().expect("display").handle();
        let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
        let monitor = st.model.monitor_state.current_monitor.clone();
        let space = st
            .model
            .monitor_state
            .monitors
            .get(monitor.as_str())
            .expect("monitor")
            .clone();

        let window = st.model.field.spawn_surface(
            "borderless-game",
            space.viewport.center,
            space.viewport.size,
        );
        st.assign_node_to_monitor(window, monitor.as_str());
        st.model
            .node_app_ids
            .insert(window, "steam_app_123".to_string());

        assert!(node_blocks_interactive_transform(&st, window));
        assert!(!node_allows_interactive_resize(&st, window));
    }

    #[test]
    fn output_sized_ordinary_surface_allows_interactive_resize() {
        let dh = Display::<Halley>::new().expect("display").handle();
        let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
        let monitor = st.model.monitor_state.current_monitor.clone();
        let space = st
            .model
            .monitor_state
            .monitors
            .get(monitor.as_str())
            .expect("monitor")
            .clone();

        let window =
            st.model
                .field
                .spawn_surface("firefox", space.viewport.center, space.viewport.size);
        st.assign_node_to_monitor(window, monitor.as_str());
        st.model.node_app_ids.insert(window, "firefox".to_string());

        assert!(!node_blocks_interactive_transform(&st, window));
        assert!(node_allows_interactive_resize(&st, window));
    }

    #[test]
    fn ordinary_active_surface_allows_interactive_resize() {
        let dh = Display::<Halley>::new().expect("display").handle();
        let mut st = Halley::new_for_test(&dh, halley_config::RuntimeTuning::default());
        let monitor = st.model.monitor_state.current_monitor.clone();
        let window = st.model.field.spawn_surface(
            "window",
            Vec2 { x: 100.0, y: 100.0 },
            Vec2 { x: 320.0, y: 240.0 },
        );
        st.assign_node_to_monitor(window, monitor.as_str());

        assert!(!node_blocks_interactive_transform(&st, window));
        assert!(node_allows_interactive_resize(&st, window));
    }
}