halley-wl 0.3.1

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

use halley_capit::CaptureCrop;
use halley_core::field::NodeId;
use halley_ipc::CaptureMode;

use crate::compositor::root::Halley;
use crate::compositor::screenshot::state::{ScreenshotRegionDragMode, ScreenshotRegionResizeDir};
use crate::compositor::surface::active_stacking_visible_members_for_monitor;
use crate::input::{active_node_screen_rect, active_node_surface_transform_screen_details};
use crate::window::active_window_frame_pad_px;

pub(super) const SCREENSHOT_HANDLE_SIZE: i32 = 12;
pub(super) const SCREENSHOT_HANDLE_HIT: i32 = 14;
pub(super) const SCREENSHOT_MIN_W: i32 = 8;
pub(super) const SCREENSHOT_MIN_H: i32 = 8;

pub(super) fn screenshot_desktop_bounds(st: &Halley) -> (i32, i32, i32, i32) {
    st.model.monitor_state.monitors.values().fold(
        (i32::MAX, i32::MAX, i32::MIN, i32::MIN),
        |(min_x, min_y, max_x, max_y), space| {
            (
                min_x.min(space.offset_x),
                min_y.min(space.offset_y),
                max_x.max(space.offset_x + space.width),
                max_y.max(space.offset_y + space.height),
            )
        },
    )
}

fn screenshot_window_matches_monitor(st: &Halley, node_id: NodeId, monitor: &str) -> bool {
    st.model.field.node(node_id).is_some_and(|node| {
        node.state == halley_core::field::NodeState::Active
            && st.model.field.is_visible(node_id)
            && st
                .model
                .monitor_state
                .node_monitor
                .get(&node_id)
                .map(|owner| owner.as_str())
                .unwrap_or(st.model.monitor_state.current_monitor.as_str())
                == monitor
    })
}

pub(super) fn screenshot_window_crop_for_node(
    st: &mut Halley,
    node_id: NodeId,
    monitor: &str,
) -> Option<CaptureCrop> {
    if !screenshot_window_matches_monitor(st, node_id, monitor) {
        return None;
    }
    let (offset_x, offset_y, width, height) = {
        let space = st.model.monitor_state.monitors.get(monitor)?;
        (space.offset_x, space.offset_y, space.width, space.height)
    };
    let previous_monitor = st.begin_temporary_render_monitor(monitor);
    let now = Instant::now();
    let rect = screenshot_window_capture_screen_rect(st, node_id, monitor, width, height, now);
    st.end_temporary_render_monitor(previous_monitor);
    let (left, top, right, bottom) = rect?;
    Some(CaptureCrop {
        x: offset_x + left.min(right).round() as i32,
        y: offset_y + top.min(bottom).round() as i32,
        w: (right - left).abs().round().max(1.0) as i32,
        h: (bottom - top).abs().round().max(1.0) as i32,
    })
}

fn screenshot_window_capture_screen_rect(
    st: &Halley,
    node_id: NodeId,
    monitor: &str,
    width: i32,
    height: i32,
    now: Instant,
) -> Option<(f32, f32, f32, f32)> {
    let (mut left, mut top, mut right, mut bottom) =
        active_node_screen_rect(st, width, height, node_id, now, None)?;
    let fullscreen_on_monitor = st.fullscreen_monitor_for_node(node_id) == Some(monitor);
    let stacked_on_monitor = active_stacking_visible_members_for_monitor(st, monitor)
        .iter()
        .any(|&visible_id| visible_id == node_id);
    if fullscreen_on_monitor || stacked_on_monitor {
        return Some((left, top, right, bottom));
    }

    let scale =
        active_node_surface_transform_screen_details(st, width, height, node_id, now, None)?.scale;
    (left, top, right, bottom) = inflate_window_capture_rect_by_frame_pad(
        (left, top, right, bottom),
        active_window_frame_pad_px(&st.runtime.tuning) as f32,
        scale,
    );
    Some((left, top, right, bottom))
}

fn inflate_window_capture_rect_by_frame_pad(
    rect: (f32, f32, f32, f32),
    frame_pad_px: f32,
    scale: f32,
) -> (f32, f32, f32, f32) {
    let scaled_pad = frame_pad_px.max(0.0) * scale.max(0.0);
    (
        rect.0 - scaled_pad,
        rect.1 - scaled_pad,
        rect.2 + scaled_pad,
        rect.3 + scaled_pad,
    )
}

fn screenshot_window_target_for_monitor(st: &Halley, monitor: &str) -> Option<NodeId> {
    [
        st.last_input_surface_node_for_monitor(monitor),
        st.last_focused_surface_node_for_monitor(monitor),
        st.model.focus_state.primary_interaction_focus,
    ]
    .into_iter()
    .flatten()
    .find(|&node_id| screenshot_window_matches_monitor(st, node_id, monitor))
    .or_else(|| {
        active_stacking_visible_members_for_monitor(st, monitor)
            .into_iter()
            .find(|&node_id| screenshot_window_matches_monitor(st, node_id, monitor))
    })
}

pub(super) fn initial_screenshot_selection(
    st: &mut Halley,
    mode: CaptureMode,
    monitor: &str,
) -> (Option<NodeId>, Option<CaptureCrop>) {
    match mode {
        CaptureMode::Region => {
            let Some(space) = st.model.monitor_state.monitors.get(monitor) else {
                return (None, None);
            };
            (
                None,
                Some(CaptureCrop {
                    x: space.offset_x
                        + (space.width - (space.width / 2).clamp(260, space.width.max(1))) / 2,
                    y: space.offset_y
                        + (space.height - (space.height / 2).clamp(180, space.height.max(1))) / 2,
                    w: (space.width / 2)
                        .clamp(260, space.width.max(1))
                        .max(SCREENSHOT_MIN_W),
                    h: (space.height / 2)
                        .clamp(180, space.height.max(1))
                        .max(SCREENSHOT_MIN_H),
                }),
            )
        }
        CaptureMode::Window => {
            let selected_window = screenshot_window_target_for_monitor(st, monitor);
            let selection_rect = selected_window
                .and_then(|node_id| screenshot_window_crop_for_node(st, node_id, monitor));
            (selected_window, selection_rect)
        }
        CaptureMode::Menu | CaptureMode::Screen => (None, None),
    }
}

pub(super) fn screenshot_menu_modes() -> [CaptureMode; 3] {
    [
        CaptureMode::Region,
        CaptureMode::Screen,
        CaptureMode::Window,
    ]
}

pub(super) fn screenshot_menu_index(mode: CaptureMode) -> Option<usize> {
    screenshot_menu_modes()
        .iter()
        .position(|candidate| *candidate == mode)
}

pub(super) fn screenshot_session_monitor(st: &Halley, output: Option<&str>) -> String {
    output
        .and_then(|name| {
            st.model
                .monitor_state
                .monitors
                .contains_key(name)
                .then_some(name.to_string())
        })
        .or_else(|| {
            st.input
                .interaction_state
                .last_pointer_screen_global
                .and_then(|(sx, sy)| st.monitor_for_screen(sx, sy))
        })
        .or_else(|| {
            st.model
                .monitor_state
                .monitors
                .contains_key(st.interaction_monitor())
                .then(|| st.interaction_monitor().to_string())
        })
        .unwrap_or_else(|| st.model.monitor_state.current_monitor.clone())
}

fn screenshot_contains(rect: CaptureCrop, px: i32, py: i32) -> bool {
    px >= rect.x && py >= rect.y && px < rect.x + rect.w && py < rect.y + rect.h
}

fn screenshot_dist2(ax: i32, ay: i32, bx: i32, by: i32) -> i64 {
    let dx = i64::from(ax - bx);
    let dy = i64::from(ay - by);
    dx * dx + dy * dy
}

fn screenshot_corner_hit(
    selection: CaptureCrop,
    px: i32,
    py: i32,
) -> Option<ScreenshotRegionResizeDir> {
    let rad = SCREENSHOT_HANDLE_HIT.max(SCREENSHOT_HANDLE_SIZE / 2);
    let rad2 = (rad as i64) * (rad as i64);
    let tl = screenshot_dist2(px, py, selection.x, selection.y);
    let tr = screenshot_dist2(px, py, selection.x + selection.w, selection.y);
    let bl = screenshot_dist2(px, py, selection.x, selection.y + selection.h);
    let br = screenshot_dist2(px, py, selection.x + selection.w, selection.y + selection.h);
    let mut best = (i64::MAX, 0);
    for (d, idx) in [(tl, 0), (tr, 1), (bl, 2), (br, 3)] {
        if d < best.0 {
            best = (d, idx);
        }
    }
    if best.0 > rad2 {
        return None;
    }
    Some(match best.1 {
        0 => ScreenshotRegionResizeDir {
            left: true,
            right: false,
            top: true,
            bottom: false,
        },
        1 => ScreenshotRegionResizeDir {
            left: false,
            right: true,
            top: true,
            bottom: false,
        },
        2 => ScreenshotRegionResizeDir {
            left: true,
            right: false,
            top: false,
            bottom: true,
        },
        _ => ScreenshotRegionResizeDir {
            left: false,
            right: true,
            top: false,
            bottom: true,
        },
    })
}

pub(super) fn screenshot_region_hit_test(
    selection: CaptureCrop,
    px: i32,
    py: i32,
) -> ScreenshotRegionDragMode {
    if let Some(dir) = screenshot_corner_hit(selection, px, py) {
        return ScreenshotRegionDragMode::Resize(dir);
    }

    let left = (px - selection.x).abs() <= SCREENSHOT_HANDLE_HIT
        && py >= selection.y - SCREENSHOT_HANDLE_HIT
        && py <= selection.y + selection.h + SCREENSHOT_HANDLE_HIT;
    let right = (px - (selection.x + selection.w)).abs() <= SCREENSHOT_HANDLE_HIT
        && py >= selection.y - SCREENSHOT_HANDLE_HIT
        && py <= selection.y + selection.h + SCREENSHOT_HANDLE_HIT;
    let top = (py - selection.y).abs() <= SCREENSHOT_HANDLE_HIT
        && px >= selection.x - SCREENSHOT_HANDLE_HIT
        && px <= selection.x + selection.w + SCREENSHOT_HANDLE_HIT;
    let bottom = (py - (selection.y + selection.h)).abs() <= SCREENSHOT_HANDLE_HIT
        && px >= selection.x - SCREENSHOT_HANDLE_HIT
        && px <= selection.x + selection.w + SCREENSHOT_HANDLE_HIT;

    let dir = ScreenshotRegionResizeDir {
        left,
        right,
        top,
        bottom,
    };
    if left || right || top || bottom {
        return ScreenshotRegionDragMode::Resize(dir);
    }
    if screenshot_contains(selection, px, py) {
        ScreenshotRegionDragMode::Move
    } else {
        ScreenshotRegionDragMode::Resize(screenshot_corner_hit(selection, px, py).unwrap_or(
            ScreenshotRegionResizeDir {
                left: px < selection.x + selection.w / 2,
                right: px >= selection.x + selection.w / 2,
                top: py < selection.y + selection.h / 2,
                bottom: py >= selection.y + selection.h / 2,
            },
        ))
    }
}

fn screenshot_crop_clamp_to(rect: &mut CaptureCrop, bounds: (i32, i32, i32, i32)) {
    let (min_x, min_y, max_x, max_y) = bounds;
    rect.w = rect.w.max(SCREENSHOT_MIN_W);
    rect.h = rect.h.max(SCREENSHOT_MIN_H);
    if rect.x < min_x {
        rect.x = min_x;
    }
    if rect.y < min_y {
        rect.y = min_y;
    }
    if rect.x + rect.w > max_x {
        rect.x = (max_x - rect.w).max(min_x);
    }
    if rect.y + rect.h > max_y {
        rect.y = (max_y - rect.h).max(min_y);
    }
}

pub(super) fn screenshot_region_apply_drag(
    drag_mode: ScreenshotRegionDragMode,
    cursor: (i32, i32),
    grab_cursor: (i32, i32),
    grab_rect: CaptureCrop,
    bounds: (i32, i32, i32, i32),
) -> CaptureCrop {
    let (cx, cy) = cursor;
    let (gx, gy) = grab_cursor;
    match drag_mode {
        ScreenshotRegionDragMode::None => grab_rect,
        ScreenshotRegionDragMode::Move => {
            let mut r = CaptureCrop {
                x: grab_rect.x + (cx - gx),
                y: grab_rect.y + (cy - gy),
                w: grab_rect.w.max(SCREENSHOT_MIN_W),
                h: grab_rect.h.max(SCREENSHOT_MIN_H),
            };
            screenshot_crop_clamp_to(&mut r, bounds);
            r
        }
        ScreenshotRegionDragMode::Resize(dir) => {
            let mut left = grab_rect.x;
            let mut top = grab_rect.y;
            let mut right = grab_rect.x + grab_rect.w;
            let mut bottom = grab_rect.y + grab_rect.h;
            if dir.left {
                left = cx;
            }
            if dir.right {
                right = cx;
            }
            if dir.top {
                top = cy;
            }
            if dir.bottom {
                bottom = cy;
            }
            if left > right {
                std::mem::swap(&mut left, &mut right);
            }
            if top > bottom {
                std::mem::swap(&mut top, &mut bottom);
            }
            let mut r = CaptureCrop {
                x: left,
                y: top,
                w: (right - left).max(SCREENSHOT_MIN_W),
                h: (bottom - top).max(SCREENSHOT_MIN_H),
            };
            screenshot_crop_clamp_to(&mut r, bounds);
            r
        }
    }
}

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

    #[test]
    fn window_capture_rect_includes_scaled_frame_pad() {
        let inflated =
            inflate_window_capture_rect_by_frame_pad((100.0, 200.0, 300.0, 500.0), 10.0, 1.5);

        assert_eq!(inflated, (85.0, 185.0, 315.0, 515.0));
    }
}