master
John Lauer 1.0.42: 1.0.42: two switches the user can flip from the dashboard, both applied at once with no restart. backgroundGuar… c27149e 1mo ago
//! Foreground, z-order and placement. The rules here are John's, verbatim from
//! win_focus.py 0.9.181: never minimize (a minimized window cannot be screenshotted),
//! never move a window off screen, never WS_EX_NOACTIVATE (it blocks the user's own
//! click), never install a hook. One z-order demotion (`push_to_background`) and one
//! sanctioned activation (`bring_to_front`), which reports what Windows actually did.
//! Python: `_set_foreground_robust`, `bring_to_user`, `show_without_activating`,
//! `_push_kicad_to_background`, `capture_placement`, `restore_placement`.

use windows::Win32::Foundation::{HWND, RECT};
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
    BringWindowToTop, GetForegroundWindow, GetWindowPlacement, SetForegroundWindow, SetWindowPlacement, SetWindowPos, ShowWindow,
    HWND_BOTTOM, SC_MAXIMIZE, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SW_RESTORE, SW_SHOW, SW_SHOWMAXIMIZED, SW_SHOWNOACTIVATE,
    SW_SHOWMINIMIZED, WINDOWPLACEMENT, WM_SYSCOMMAND, WPF_RESTORETOMAXIMIZED,
};

use super::enumerate::{minimized, thread_of};
use super::messages::post;
use super::{hwnd_u64, require_responsive};
use crate::Placement;

/// Deadline for the WM_NULL gate in front of the calls Windows makes synchronous.
const GATE_MS: u32 = 300;

pub fn foreground() -> u64 {
    // SAFETY: no arguments; reads the foreground handle.
    hwnd_u64(unsafe { GetForegroundWindow() })
}

/// THE backgrounding primitive (0.9.181): exactly one SetWindowPos to HWND_BOTTOM with
/// SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE. Not minimized, not moved, not restyled.
/// Once-per-window bookkeeping belongs to the etiquette layer, not here.
pub fn push_to_background(h: HWND) -> Result<(), String> {
    // SetWindowPos sends WM_WINDOWPOSCHANGING to the target synchronously; gate it.
    require_responsive(h, "push_to_background", GATE_MS)?;
    // SAFETY: valid, responsive window; flags forbid move, size and activation.
    unsafe { SetWindowPos(h, Some(HWND_BOTTOM), 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE) }
        .map_err(|e| format!("SetWindowPos(HWND_BOTTOM) on {}: {e}", hwnd_u64(h)))
}

/// Make a window visible (restoring it if minimized) without activating it. The
/// strongest thing the bridge does to a window on its own initiative.
pub fn show_no_activate(h: HWND) -> Result<(), String> {
    require_responsive(h, "show_no_activate", GATE_MS)?;
    // SAFETY: valid, responsive window. Return value is the previous visibility, not an error.
    let _ = unsafe { ShowWindow(h, SW_SHOWNOACTIVATE) };
    Ok(())
}

/// The one sanctioned foreground. A plain SetForegroundWindow from a background process
/// is refused by the foreground lock, so we attach our input queue to the current
/// foreground thread and the target thread first (the documented AttachThreadInput
/// technique), raise, activate, detach, then MEASURE: the result is whether
/// GetForegroundWindow reports the target afterwards, not whether the calls returned.
/// Minimized windows are restored; anything else keeps its size (SW_RESTORE would
/// un-maximize a maximized editor, which the Python version got wrong).
/// Cancel Windows' orange attention flash on a taskbar button (FLASHW_STOP). Windows starts that
/// flash by itself whenever an app asks for the foreground and is refused, or is sent behind right
/// after taking it, which is what happens to every KiCad window the bridge opens in the background.
/// The Python build cancelled it (handlers/win_taskbar.py); the native build never did until 1.0.42,
/// so the user had to click each button to stop it (John: "whackamole").
pub fn stop_flash(h: HWND) {
    use windows::Win32::UI::WindowsAndMessaging::{FlashWindowEx, FLASHWINFO, FLASHW_STOP};
    if h.is_invalid() {
        return;
    }
    let fi = FLASHWINFO { cbSize: std::mem::size_of::<FLASHWINFO>() as u32, hwnd: h, dwFlags: FLASHW_STOP, uCount: 0, dwTimeout: 0 };
    // SAFETY: a fully initialised FLASHWINFO for a window handle; the call only clears a flag.
    let _ = unsafe { FlashWindowEx(&fi) };
}

/// Ask Windows to make `h` the foreground window: one SetForegroundWindow and nothing else. No
/// AttachThreadInput, no ShowWindow, no BringWindowToTop. Returns whether Windows agreed.
pub fn request_foreground(h: HWND) -> bool {
    if h.is_invalid() || !super::responsive(h, GATE_MS) {
        return false;
    }
    // SAFETY: a plain request on a valid, responsive window handle.
    unsafe { SetForegroundWindow(h) }.as_bool()
}

pub fn bring_to_front(h: HWND) -> Result<bool, String> {
    require_responsive(h, "bring_to_front", GATE_MS)?;
    // SAFETY: reading the foreground handle.
    let fg = unsafe { GetForegroundWindow() };
    if fg == h {
        return Ok(true);
    }
    // SAFETY: valid, responsive window.
    let _ = unsafe { ShowWindow(h, if minimized(h) { SW_RESTORE } else { SW_SHOW }) };

    // SAFETY: thread ids are plain integers; every successful attach is detached below
    // on the same thread before this function returns.
    let attached = unsafe {
        use windows::Win32::System::Threading::AttachThreadInput;
        let cur = GetCurrentThreadId();
        let mut attached = Vec::with_capacity(2);
        let fg_tid = if fg.is_invalid() { 0 } else { thread_of(fg) };
        for tid in [fg_tid, thread_of(h)] {
            if tid != 0 && tid != cur && !attached.contains(&tid) && AttachThreadInput(cur, tid, true).as_bool() {
                attached.push(tid);
            }
        }
        let _ = BringWindowToTop(h);
        let _ = SetForegroundWindow(h);
        attached
    };
    // SAFETY: detaching exactly what was attached above.
    unsafe {
        use windows::Win32::System::Threading::AttachThreadInput;
        let cur = GetCurrentThreadId();
        for tid in attached {
            let _ = AttachThreadInput(cur, tid, false);
        }
    }
    // Let the shell settle before measuring; the activation is processed asynchronously
    // by the target thread.
    std::thread::sleep(std::time::Duration::from_millis(60));
    // SAFETY: reading the foreground handle.
    Ok(unsafe { GetForegroundWindow() } == h)
}

fn read_placement(h: HWND) -> Result<WINDOWPLACEMENT, String> {
    let mut wp = WINDOWPLACEMENT { length: std::mem::size_of::<WINDOWPLACEMENT>() as u32, ..Default::default() };
    // SAFETY: length is set; `wp` outlives the call. GetWindowPlacement reads window state
    // without sending a message.
    unsafe { GetWindowPlacement(h, &mut wp) }.map_err(|e| format!("GetWindowPlacement({}): {e}", hwnd_u64(h)))?;
    Ok(wp)
}

/// The restored (normal) rectangle and the maximized flag, in the workspace coordinates
/// GetWindowPlacement uses, so `set_placement` round-trips exactly across a restart.
pub fn placement(h: HWND) -> Result<Placement, String> {
    let wp = read_placement(h)?;
    let r = wp.rcNormalPosition;
    Ok(Placement {
        x: r.left,
        y: r.top,
        width: r.right - r.left,
        height: r.bottom - r.top,
        // A minimized window remembers whether it was maximized in the flags.
        maximized: wp.showCmd == SW_SHOWMAXIMIZED.0 as u32
            || (wp.showCmd == SW_SHOWMINIMIZED.0 as u32 && wp.flags.contains(WPF_RESTORETOMAXIMIZED)),
    })
}

/// Put a window back where the user had it, without activating it and NEVER minimized
/// (a minimized window blanks PrintWindow, which silently breaks the evidence pipeline,
/// #48 ask 4). The rect lands through SetWindowPlacement with SW_SHOWNOACTIVATE; a
/// maximized placement is then requested from the window's own thread with a posted
/// WM_SYSCOMMAND SC_MAXIMIZE, because every maximize command Windows offers a foreign
/// process activates the window.
pub fn set_placement(h: HWND, p: &Placement) -> Result<(), String> {
    require_responsive(h, "set_placement", GATE_MS)?;
    if p.width < 50 || p.height < 50 {
        return Err(format!("refusing a {}x{} placement (a minimized or parked stub is not a placement)", p.width, p.height));
    }
    let mut wp = read_placement(h)?;
    wp.flags = Default::default();
    wp.showCmd = SW_SHOWNOACTIVATE.0 as u32;
    wp.rcNormalPosition = RECT { left: p.x, top: p.y, right: p.x + p.width, bottom: p.y + p.height };
    // SAFETY: length set by read_placement; `wp` outlives the call; the window is responsive.
    unsafe { SetWindowPlacement(h, &wp) }.map_err(|e| format!("SetWindowPlacement({}): {e}", hwnd_u64(h)))?;
    if p.maximized {
        post(h, WM_SYSCOMMAND, SC_MAXIMIZE as usize, 0)?;
    }
    Ok(())
}