halley-wl 0.1.0

Wayland backend and rendering implementation for the Halley Wayland compositor.
use crate::compositor::root::Halley;
use eventline::debug;
use halley_config::{ClickCollapsedOutsideFocusMode, ClickCollapsedPanMode};
use halley_core::decay::DecayLevel;
use halley_core::viewport::FocusZone;
use halley_ipc::{NodeMoveDirection, TrailDirection};
use std::time::Instant;

pub(crate) fn promote_node_level(
    st: &mut Halley,
    node_id: halley_core::field::NodeId,
    now: Instant,
) -> bool {
    let Some(n) = st.model.field.node(node_id) else {
        return false;
    };
    if n.kind != halley_core::field::NodeKind::Surface {
        return false;
    }
    if n.state != halley_core::field::NodeState::Node {
        return false;
    }
    let target_pos = n.pos;
    let target_monitor = st.monitor_for_node_or_current(node_id);
    let focus_center = st.view_center_for_monitor(target_monitor.as_str());
    let focus_ring = st.focus_ring_for_monitor(target_monitor.as_str());

    let in_focus_ring = focus_ring.zone(focus_center, target_pos) == FocusZone::Inside;

    if in_focus_ring {
        // This is a deliberate promote, not a stale auto-resurrect.
        st.model
            .workspace_state
            .manual_collapsed_nodes
            .remove(&node_id);

        let _ = st.model.field.set_decay_level(node_id, DecayLevel::Hot);
        crate::compositor::workspace::state::mark_active_transition(st, node_id, now, 360);

        st.set_interaction_focus(Some(node_id), 30_000, now);
        return true;
    }

    st.set_interaction_focus(Some(node_id), 30_000, now);
    st.set_pan_restore_focus_target(node_id);
    st.animate_viewport_center_to(target_pos, now)
}

pub(crate) fn activate_collapsed_node_from_click(
    st: &mut Halley,
    node_id: halley_core::field::NodeId,
    now: Instant,
) -> bool {
    let Some(n) = st.model.field.node(node_id) else {
        return false;
    };
    if n.kind != halley_core::field::NodeKind::Surface {
        return false;
    }
    if n.state != halley_core::field::NodeState::Node {
        return false;
    }

    let target_monitor = st.monitor_for_node_or_current(node_id);
    let focus_center = st.view_center_for_monitor(target_monitor.as_str());
    let focus_ring = st.focus_ring_for_monitor(target_monitor.as_str());
    let in_focus_ring = focus_ring.zone(focus_center, n.pos) == FocusZone::Inside;

    if in_focus_ring
        || st.runtime.tuning.click_collapsed_outside_focus == ClickCollapsedOutsideFocusMode::Ignore
    {
        return promote_node_level(st, node_id, now);
    }

    st.model
        .workspace_state
        .manual_collapsed_nodes
        .remove(&node_id);
    let _ = st.model.field.set_decay_level(node_id, DecayLevel::Hot);
    crate::compositor::workspace::state::mark_active_transition(st, node_id, now, 360);
    st.set_interaction_focus(Some(node_id), 30_000, now);

    match st.runtime.tuning.click_collapsed_pan {
        ClickCollapsedPanMode::Never => false,
        ClickCollapsedPanMode::IfOffscreen => {
            if st.surface_is_fully_visible_on_monitor(target_monitor.as_str(), node_id) {
                false
            } else {
                st.minimal_reveal_center_for_surface_on_monitor(target_monitor.as_str(), node_id)
                    .map(|target| st.animate_viewport_center_to(target, now))
                    .unwrap_or(false)
            }
        }
        ClickCollapsedPanMode::Always => st
            .minimal_reveal_center_for_surface_on_monitor(target_monitor.as_str(), node_id)
            .map(|target| st.animate_viewport_center_to(target, now))
            .unwrap_or(false),
    }
}

pub(crate) fn focus_or_reveal_surface_node(
    st: &mut Halley,
    node_id: halley_core::field::NodeId,
    now: Instant,
) -> bool {
    let Some(node) = st.model.field.node(node_id).cloned() else {
        return false;
    };
    if node.kind != halley_core::field::NodeKind::Surface {
        return false;
    }

    let target_monitor = st.monitor_for_node_or_current(node_id);
    if st.focused_monitor() != target_monitor {
        st.focus_monitor_view(target_monitor.as_str(), now);
    }

    match node.state {
        halley_core::field::NodeState::Node => promote_node_level(st, node_id, now),
        halley_core::field::NodeState::Active | halley_core::field::NodeState::Drifting => {
            st.set_interaction_focus(Some(node_id), 30_000, now);
            let is_pending_tiled = st
                .model
                .spawn_state
                .pending_tiled_insert_reveal_at_ms
                .contains_key(&node_id);
            let is_pending_reveal = st
                .model
                .spawn_state
                .pending_initial_reveal
                .contains(&node_id);
            if !is_pending_tiled && !is_pending_reveal {
                let _ = st
                    .minimal_reveal_center_for_surface_on_monitor(target_monitor.as_str(), node_id)
                    .map(|target| st.animate_viewport_center_to(target, now));
            }
            true
        }
        halley_core::field::NodeState::Core => false,
    }
}

pub(crate) fn latest_surface_node(st: &Halley) -> Option<halley_core::field::NodeId> {
    st.last_input_surface_node_for_monitor(st.focused_monitor())
        .or_else(|| st.last_input_surface_node())
        .or_else(|| {
            st.model
                .surface_to_node
                .values()
                .copied()
                .max_by_key(|id| id.as_u64())
        })
}

pub(crate) fn move_latest_node(st: &mut Halley, dx: f32, dy: f32) -> bool {
    let Some(id) = latest_surface_node(st) else {
        return false;
    };
    if crate::compositor::surface::is_active_stacking_workspace_member(st, id) {
        return false;
    }
    let Some(n) = st.model.field.node(id) else {
        return false;
    };
    let to = halley_core::field::Vec2 {
        x: n.pos.x + dx,
        y: n.pos.y + dy,
    };
    let _ = st.model.field.set_pinned(id, false);
    crate::compositor::carry::system::begin_carry_state_tracking(st, id);
    if st.carry_surface_non_overlap(id, to, false) {
        crate::compositor::carry::system::update_carry_state_preview(st, id, Instant::now());
        crate::compositor::carry::system::end_carry_state_tracking(st, id);
        st.set_interaction_focus(Some(id), 30_000, Instant::now());
        if let Some(nn) = st.model.field.node(id) {
            debug!(
                "moved node id={} to ({:.0},{:.0}) state={:?}",
                id.as_u64(),
                nn.pos.x,
                nn.pos.y,
                nn.state
            );
        }
        return true;
    }
    crate::compositor::carry::system::end_carry_state_tracking(st, id);
    false
}

pub(crate) fn move_latest_node_direction(st: &mut Halley, direction: NodeMoveDirection) -> bool {
    const STEP_NODE: f32 = 80.0;

    match direction {
        NodeMoveDirection::Left => move_latest_node(st, -STEP_NODE, 0.0),
        NodeMoveDirection::Right => move_latest_node(st, STEP_NODE, 0.0),
        NodeMoveDirection::Up => move_latest_node(st, 0.0, -STEP_NODE),
        NodeMoveDirection::Down => move_latest_node(st, 0.0, STEP_NODE),
    }
}

pub(crate) fn step_window_trail(st: &mut Halley, direction: TrailDirection) -> bool {
    st.navigate_window_trail(direction, Instant::now())
}

pub(crate) fn toggle_focused_active_node_state(st: &mut Halley) -> bool {
    let now = Instant::now();
    let focused_monitor = st.focused_monitor().to_string();

    let Some(id) = st
        .focused_node_for_monitor(focused_monitor.as_str())
        .filter(|&id| st.model.field.node(id).is_some() && st.model.field.is_visible(id))
        .or(st
            .model
            .focus_state
            .primary_interaction_focus
            .filter(|&id| st.model.field.node(id).is_some() && st.model.field.is_visible(id)))
        .or_else(|| st.last_focused_surface_node_for_monitor(focused_monitor.as_str()))
        .or_else(|| st.last_focused_surface_node())
    else {
        return false;
    };

    toggle_node_state(st, id, now, focused_monitor.as_str())
}

pub(crate) fn toggle_node_state(
    st: &mut Halley,
    id: halley_core::field::NodeId,
    now: Instant,
    focused_monitor: &str,
) -> bool {
    let Some(n) = st.model.field.node(id) else {
        return false;
    };

    if n.kind == halley_core::field::NodeKind::Core
        && n.state == halley_core::field::NodeState::Core
    {
        return st.toggle_cluster_workspace_by_core(id, now);
    }

    if n.kind != halley_core::field::NodeKind::Surface {
        return false;
    }

    if let Some(cid) = st.model.field.cluster_id_for_member_public(id) {
        if st.active_cluster_workspace_for_monitor(focused_monitor) == Some(cid) {
            return st.collapse_active_cluster_workspace(now);
        }
    }

    match n.state {
        halley_core::field::NodeState::Active => {
            if crate::compositor::workspace::state::start_active_to_node_close_animation(
                st, id, now,
            ) {
                let _ = crate::compositor::workspace::state::finish_manual_collapse(st, id, now);
            } else {
                crate::compositor::workspace::state::queue_pending_manual_collapse(st, id, now);
            }
            true
        }

        halley_core::field::NodeState::Node => {
            st.model.workspace_state.manual_collapsed_nodes.remove(&id);
            st.model
                .workspace_state
                .pending_manual_collapses
                .remove(&id);
            let _ = st
                .model
                .field
                .set_decay_level(id, halley_core::decay::DecayLevel::Hot);
            st.model
                .spawn_state
                .pending_spawn_activate_at_ms
                .remove(&id);
            crate::compositor::workspace::state::mark_active_transition(st, id, now, 360);

            st.set_interaction_focus(Some(id), 30_000, now);
            true
        }

        _ => false,
    }
}