halley-wl 0.3.1

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

use crate::compositor::interaction::{PointerState, ResizeCtx};
use crate::compositor::root::Halley;
use crate::compositor::surface::{request_toplevel_resize_mode, toplevel_min_size_for_node};

use super::resize_rect_nearly_eq;

fn resize_smoothing_alpha(st: &Halley, dt: Duration) -> f32 {
    if !st.runtime.tuning.smooth_resize_enabled() {
        return 1.0;
    }

    let dt_secs = dt.as_secs_f32().clamp(0.0, 0.25);
    if dt_secs <= f32::EPSILON {
        return 0.0;
    }
    let duration_secs = (st.runtime.tuning.smooth_resize_duration_ms().max(1) as f32) / 1000.0;
    (1.0 - 0.1f32.powf(dt_secs / duration_secs.max(0.001))).clamp(0.0, 1.0)
}

fn snap_resize_preview_edges(resize: &mut ResizeCtx) {
    if resize_rect_nearly_eq(resize.preview_left_px, resize.target_left_px) {
        resize.preview_left_px = resize.target_left_px;
    }
    if resize_rect_nearly_eq(resize.preview_right_px, resize.target_right_px) {
        resize.preview_right_px = resize.target_right_px;
    }
    if resize_rect_nearly_eq(resize.preview_top_px, resize.target_top_px) {
        resize.preview_top_px = resize.target_top_px;
    }
    if resize_rect_nearly_eq(resize.preview_bottom_px, resize.target_bottom_px) {
        resize.preview_bottom_px = resize.target_bottom_px;
    }
}

fn resize_preview_settled(resize: &ResizeCtx) -> bool {
    resize_rect_nearly_eq(resize.preview_left_px, resize.target_left_px)
        && resize_rect_nearly_eq(resize.preview_right_px, resize.target_right_px)
        && resize_rect_nearly_eq(resize.preview_top_px, resize.target_top_px)
        && resize_rect_nearly_eq(resize.preview_bottom_px, resize.target_bottom_px)
}

fn resize_settle_velocity_done(resize: &ResizeCtx) -> bool {
    resize.preview_velocity_left_pxps.abs() <= 8.0
        && resize.preview_velocity_right_pxps.abs() <= 8.0
        && resize.preview_velocity_top_pxps.abs() <= 8.0
        && resize.preview_velocity_bottom_pxps.abs() <= 8.0
}

pub(super) fn advance_resize_preview_toward_target(
    st: &Halley,
    resize: &mut ResizeCtx,
    now: Instant,
) {
    if !st.runtime.tuning.smooth_resize_enabled() {
        resize.preview_left_px = resize.target_left_px;
        resize.preview_right_px = resize.target_right_px;
        resize.preview_top_px = resize.target_top_px;
        resize.preview_bottom_px = resize.target_bottom_px;
        resize.preview_velocity_left_pxps = 0.0;
        resize.preview_velocity_right_pxps = 0.0;
        resize.preview_velocity_top_pxps = 0.0;
        resize.preview_velocity_bottom_pxps = 0.0;
        resize.last_smooth_tick_at = now;
        return;
    }

    let dt = now.saturating_duration_since(resize.last_smooth_tick_at);
    resize.last_smooth_tick_at = now;
    let dt_secs = dt.as_secs_f32().clamp(0.0, 0.25);
    let alpha = resize_smoothing_alpha(st, dt);
    let prev_left = resize.preview_left_px;
    let prev_right = resize.preview_right_px;
    let prev_top = resize.preview_top_px;
    let prev_bottom = resize.preview_bottom_px;
    resize.preview_left_px += (resize.target_left_px - resize.preview_left_px) * alpha;
    resize.preview_right_px += (resize.target_right_px - resize.preview_right_px) * alpha;
    resize.preview_top_px += (resize.target_top_px - resize.preview_top_px) * alpha;
    resize.preview_bottom_px += (resize.target_bottom_px - resize.preview_bottom_px) * alpha;
    if dt_secs > f32::EPSILON {
        resize.preview_velocity_left_pxps = (resize.preview_left_px - prev_left) / dt_secs;
        resize.preview_velocity_right_pxps = (resize.preview_right_px - prev_right) / dt_secs;
        resize.preview_velocity_top_pxps = (resize.preview_top_px - prev_top) / dt_secs;
        resize.preview_velocity_bottom_pxps = (resize.preview_bottom_px - prev_bottom) / dt_secs;
    }
    snap_resize_preview_edges(resize);
}

fn advance_resize_preview_toward_stop(st: &Halley, resize: &mut ResizeCtx, now: Instant) {
    let dt = now.saturating_duration_since(resize.last_smooth_tick_at);
    resize.last_smooth_tick_at = now;
    let dt_secs = dt.as_secs_f32().clamp(0.0, 0.25);
    if dt_secs <= f32::EPSILON {
        return;
    }

    let duration_secs = (st.runtime.tuning.smooth_resize_duration_ms().max(1) as f32) / 1000.0;
    let decay = 0.01f32.powf(dt_secs / duration_secs.max(0.001));
    resize.preview_left_px += resize.preview_velocity_left_pxps * dt_secs;
    resize.preview_right_px += resize.preview_velocity_right_pxps * dt_secs;
    resize.preview_top_px += resize.preview_velocity_top_pxps * dt_secs;
    resize.preview_bottom_px += resize.preview_velocity_bottom_pxps * dt_secs;
    resize.preview_velocity_left_pxps *= decay;
    resize.preview_velocity_right_pxps *= decay;
    resize.preview_velocity_top_pxps *= decay;
    resize.preview_velocity_bottom_pxps *= decay;
    resize.target_left_px = resize.preview_left_px;
    resize.target_right_px = resize.preview_right_px;
    resize.target_top_px = resize.preview_top_px;
    resize.target_bottom_px = resize.preview_bottom_px;
    if resize_settle_velocity_done(resize) {
        resize.preview_velocity_left_pxps = 0.0;
        resize.preview_velocity_right_pxps = 0.0;
        resize.preview_velocity_top_pxps = 0.0;
        resize.preview_velocity_bottom_pxps = 0.0;
    }
}

fn apply_resize_preview_state(st: &mut Halley, resize: &mut ResizeCtx) -> bool {
    if resize_preview_settled(resize) {
        resize.preview_left_px = resize.target_left_px;
        resize.preview_right_px = resize.target_right_px;
        resize.preview_top_px = resize.target_top_px;
        resize.preview_bottom_px = resize.target_bottom_px;
    }

    let (min_lw, min_lh) = toplevel_min_size_for_node(st, resize.node_id);
    let cam_scale = st.camera_render_scale();
    let min_w = (min_lw as f32 * cam_scale).max(96.0);
    let min_h = (min_lh as f32 * cam_scale).max(72.0);
    let preview_visual_w = (resize.preview_right_px - resize.preview_left_px)
        .round()
        .max(min_w) as i32;
    let preview_visual_h = (resize.preview_bottom_px - resize.preview_top_px)
        .round()
        .max(min_h) as i32;
    let visual_delta_w = preview_visual_w - resize.start_visual_w;
    let visual_delta_h = preview_visual_h - resize.start_visual_h;
    let logical_delta_w = (visual_delta_w as f32 / cam_scale.max(0.001)).round() as i32;
    let logical_delta_h = (visual_delta_h as f32 / cam_scale.max(0.001)).round() as i32;
    let min_logical_w = (min_w / cam_scale.max(0.001)).round() as i32;
    let min_logical_h = (min_h / cam_scale.max(0.001)).round() as i32;
    let target_w = (resize.start_surface_w + logical_delta_w).max(min_logical_w);
    let target_h = (resize.start_surface_h + logical_delta_h).max(min_logical_h);

    if target_w != resize.last_sent_w || target_h != resize.last_sent_h {
        request_toplevel_resize_mode(st, resize.node_id, target_w, target_h, true);
        resize.last_sent_w = target_w;
        resize.last_sent_h = target_h;
    }

    st.input
        .interaction_state
        .physics_velocity
        .insert(resize.node_id, halley_core::field::Vec2 { x: 0.0, y: 0.0 });

    let center_sx = (resize.preview_left_px + resize.preview_right_px) * 0.5;
    let center_sy = (resize.preview_top_px + resize.preview_bottom_px) * 0.5;
    let center_world = crate::spatial::screen_to_world(
        st,
        resize.workspace_w,
        resize.workspace_h,
        center_sx,
        center_sy,
    );
    if let Some(n) = st.model.field.node_mut(resize.node_id) {
        n.pos = center_world;
    }
    let _ = st.model.field.set_resize_footprint(
        resize.node_id,
        Some(halley_core::field::Vec2 {
            x: target_w as f32,
            y: target_h as f32,
        }),
    );
    let _ = st
        .model
        .field
        .set_decay_level(resize.node_id, halley_core::decay::DecayLevel::Hot);

    resize_preview_settled(resize)
}

fn refresh_resize_preview_state(st: &mut Halley, resize: &mut ResizeCtx, now: Instant) -> bool {
    if resize.settling {
        advance_resize_preview_toward_stop(st, resize, now);
    } else {
        advance_resize_preview_toward_target(st, resize, now);
    }
    apply_resize_preview_state(st, resize)
}

fn finish_resize_interaction(
    st: &mut Halley,
    ps: &mut PointerState,
    resize: ResizeCtx,
    now: Instant,
) {
    ps.resize_trace_node = None;
    ps.resize_trace_until = None;
    ps.resize_trace_last_at = None;
    ps.preview_block_until = Some(now + Duration::from_millis(360));
    ps.resize = None;

    if !resize.drag_started {
        if resize.resize_mode_sent {
            request_toplevel_resize_mode(
                st,
                resize.node_id,
                resize.last_sent_w,
                resize.last_sent_h,
                false,
            );
        }
        st.set_recent_top_node(resize.node_id, now + Duration::from_millis(600));
        st.end_resize_interaction(now);
        st.resolve_overlap_now();
        return;
    }

    let (min_w, min_h) = toplevel_min_size_for_node(st, resize.node_id);
    let final_w = resize.last_sent_w.max(min_w).max(96);
    let final_h = resize.last_sent_h.max(min_h).max(72);
    let final_bbox_w =
        ((resize.start_bbox_w as f32) + ((final_w - resize.start_surface_w) as f32)).max(1.0);
    let final_bbox_h =
        ((resize.start_bbox_h as f32) + ((final_h - resize.start_surface_h) as f32)).max(1.0);
    request_toplevel_resize_mode(st, resize.node_id, final_w, final_h, true);
    request_toplevel_resize_mode(st, resize.node_id, final_w, final_h, false);
    if let Some(n) = st.model.field.node_mut(resize.node_id) {
        n.intrinsic_size.x = final_bbox_w;
        n.intrinsic_size.y = final_bbox_h;
    }
    let _ = st
        .model
        .field
        .sync_active_footprint_to_intrinsic(resize.node_id);
    st.set_last_active_size_now(
        resize.node_id,
        halley_core::field::Vec2 {
            x: final_bbox_w,
            y: final_bbox_h,
        },
    );
    st.set_recent_top_node(resize.node_id, now + Duration::from_millis(600));
    st.end_resize_interaction(now);
    st.resolve_landmarks_overlapped_by_active_window(resize.node_id);
    st.resolve_overlap_now();
}

pub(crate) fn advance_resize_anim(
    st: &mut Halley,
    ps: &mut PointerState,
    now: Instant,
) -> Option<halley_core::field::NodeId> {
    let Some(mut resize) = ps.resize.take() else {
        return None;
    };
    if !resize.drag_started {
        ps.resize = Some(resize);
        return None;
    }
    if !st.runtime.tuning.smooth_resize_enabled() && !resize.settling {
        ps.resize = Some(resize);
        return None;
    }

    let settled = refresh_resize_preview_state(st, &mut resize, now);
    let node_id = resize.node_id;
    if resize.settling && settled && resize_settle_velocity_done(&resize) {
        finish_resize_interaction(st, ps, resize, now);
        return Some(node_id);
    }

    ps.resize = Some(resize);
    Some(node_id)
}

pub(super) fn finish_resize_now(
    st: &mut Halley,
    ps: &mut PointerState,
    resize: ResizeCtx,
    now: Instant,
) {
    finish_resize_interaction(st, ps, resize, now)
}

pub(super) fn refresh_resize_now(st: &mut Halley, resize: &mut ResizeCtx, now: Instant) -> bool {
    refresh_resize_preview_state(st, resize, now)
}

pub(super) fn apply_resize_now(st: &mut Halley, resize: &mut ResizeCtx) -> bool {
    apply_resize_preview_state(st, resize)
}