master
John Lauer 1.0.3: 1.0.3: placement. kicad_placement_state, kicad_move_footprint (a batch of moves as one native undo step, refused… 2569119 1mo ago
//! Window enumeration and per-window facts: EnumWindows, EnumChildWindows, class names,
//! titles that never hang, rects, owner, monitor test, the GL canvas child.
//! Python: close_windows.py `_win_find_windows_by_pid`, `_win_title_nohang`,
//! `_win_class_name`, `_win_get_dialog_body_text`; kicad_ui.py `_gl_canvas_rect`;
//! win_focus.py `_enum_kicad_hwnds`, `_visible_on_any_monitor`;
//! open_symbol_editor.py `_find_child_by_class_and_parent`.

use std::collections::HashMap;

use windows::core::BOOL;
use windows::Win32::Foundation::{HWND, LPARAM, RECT};
use windows::Win32::Graphics::Gdi::{GetMonitorInfoW, MonitorFromRect, MONITORINFO, MONITOR_DEFAULTTONULL};
use windows::Win32::UI::WindowsAndMessaging::{
    EnumChildWindows, EnumWindows, GetClassNameW, GetParent, GetWindow, GetWindowRect, GetWindowThreadProcessId,
    InternalGetWindowText, IsIconic, IsWindowVisible, IsZoomed, GW_OWNER, WM_GETTEXT,
};

use super::{from_wide, hwnd_u64, is_kicad_exe, responsive, send_timeout, Sent};
use crate::WindowInfo;

/// Enumeration callback: push every handle into the Vec behind LPARAM.
unsafe extern "system" fn collect(h: HWND, l: LPARAM) -> BOOL {
    // SAFETY: LPARAM carries a `*mut Vec<HWND>` that the enumerating function keeps alive
    // for the whole (synchronous) enumeration; no other reference exists meanwhile.
    let v = unsafe { &mut *(l.0 as *mut Vec<HWND>) };
    v.push(h);
    BOOL(1)
}

/// Every top-level window, visible or not, in z-order (topmost first).
pub fn top_level_windows() -> Vec<HWND> {
    let mut out: Vec<HWND> = Vec::with_capacity(256);
    // SAFETY: `out` outlives the call; `collect` only pushes into it.
    let _ = unsafe { EnumWindows(Some(collect), LPARAM(&mut out as *mut _ as isize)) };
    out
}

/// Every descendant of `parent` (EnumChildWindows walks the whole subtree, which is what
/// the Python callers relied on: the symbol editor's filter `Edit` sits two levels down).
pub fn descendants(parent: HWND) -> Vec<HWND> {
    let mut out: Vec<HWND> = Vec::with_capacity(64);
    // SAFETY: as above; EnumChildWindows is synchronous.
    let _ = unsafe { EnumChildWindows(Some(parent), Some(collect), LPARAM(&mut out as *mut _ as isize)) };
    out
}

pub fn class_name(h: HWND) -> String {
    let mut buf = [0u16; 256];
    // SAFETY: buffer length is passed with the buffer; GetClassNameW reads no cross-thread state.
    let n = unsafe { GetClassNameW(h, &mut buf) };
    from_wide(&buf[..n.max(0) as usize])
}

/// A window's caption without a cross-thread message: `InternalGetWindowText` reads the
/// window's own text store, so a frame frozen behind a modal still yields its title
/// (winvm 2026-07: GetWindowTextW hung the dialog scan). Empty when the store is empty,
/// which is the case for a standard Edit control (its text lives in the control).
pub fn title_nohang(h: HWND) -> String {
    let mut buf = [0u16; 512];
    // SAFETY: buffer length passed with the buffer.
    let n = unsafe { InternalGetWindowText(h, &mut buf) };
    from_wide(&buf[..n.max(0) as usize])
}

/// Text via a time-boxed WM_GETTEXT (`SMTO_ABORTIFHUNG`). Empty on timeout or hang.
pub fn text_timeout(h: HWND, timeout_ms: u32) -> Option<String> {
    let mut buf = vec![0u16; 4096];
    match send_timeout(h, WM_GETTEXT, buf.len() - 1, buf.as_mut_ptr() as isize, timeout_ms) {
        Sent::Answered(n) => Some(from_wide(&buf[..n.min(buf.len() - 1)])),
        _ => None,
    }
}

pub fn pid_of(h: HWND) -> u32 {
    let mut pid = 0u32;
    // SAFETY: `pid` outlives the call.
    unsafe { GetWindowThreadProcessId(h, Some(&mut pid)) };
    pid
}

pub fn thread_of(h: HWND) -> u32 {
    // SAFETY: no out pointer requested.
    unsafe { GetWindowThreadProcessId(h, None) }
}

/// (x, y, width, height) in physical pixels (the process is per-monitor DPI aware).
pub fn rect(h: HWND) -> (i32, i32, i32, i32) {
    let mut r = RECT::default();
    // SAFETY: `r` outlives the call.
    if unsafe { GetWindowRect(h, &mut r) }.is_err() {
        return (0, 0, 0, 0);
    }
    (r.left, r.top, r.right - r.left, r.bottom - r.top)
}

pub fn owner(h: HWND) -> u64 {
    // SAFETY: GetWindow reads the window manager's tables only.
    unsafe { GetWindow(h, GW_OWNER) }.map(hwnd_u64).unwrap_or(0)
}

pub fn parent(h: HWND) -> u64 {
    // SAFETY: as above.
    unsafe { GetParent(h) }.map(hwnd_u64).unwrap_or(0)
}

pub fn visible(h: HWND) -> bool {
    // SAFETY: table read.
    unsafe { IsWindowVisible(h).as_bool() }
}

pub fn minimized(h: HWND) -> bool {
    // SAFETY: table read.
    unsafe { IsIconic(h).as_bool() }
}

pub fn maximized(h: HWND) -> bool {
    // SAFETY: table read.
    unsafe { IsZoomed(h).as_bool() }
}

/// Cache of pid -> executable base name so a full enumeration opens each process once.
#[derive(Default)]
pub struct ExeCache(HashMap<u32, String>);

impl ExeCache {
    pub fn exe(&mut self, pid: u32) -> String {
        self.0.entry(pid).or_insert_with(|| super::process::exe_base_name(pid)).clone()
    }
}

/// Assemble a `WindowInfo` for a top-level window. `probe_ms` > 0 runs the WM_NULL
/// responsiveness probe (only worth paying for on KiCad windows during a bulk listing).
pub fn info(h: HWND, cache: &mut ExeCache, probe_ms: u32) -> WindowInfo {
    let pid = pid_of(h);
    let exe = cache.exe(pid);
    let hung = probe_ms > 0 && !responsive(h, probe_ms);
    WindowInfo {
        hwnd: hwnd_u64(h),
        title: title_nohang(h),
        class_name: class_name(h),
        pid,
        exe,
        visible: visible(h),
        owner: owner(h),
        rect: rect(h),
        minimized: minimized(h),
        maximized: maximized(h),
        hung,
    }
}

/// Every visible top-level window. KiCad windows get the 150 ms WM_NULL probe so `hung`
/// means something; other applications' threads are left alone.
/// The first visible, non-minimized, titled top-level window in Z order (EnumWindows walks
/// top to bottom), skipping the shell's own windows. 0 when nothing qualifies.
pub fn z_top() -> u64 {
    for h in top_level_windows() {
        if !visible(h) || minimized(h) {
            continue;
        }
        let t = title_nohang(h);
        if t.is_empty() || t == "Program Manager" || t == "Windows Input Experience" {
            continue;
        }
        let (l, tp, r, b) = rect(h);
        if r - l < 50 || b - tp < 50 {
            continue;
        }
        let c = class_name(h);
        if c == "Shell_TrayWnd" || c == "Shell_SecondaryTrayWnd" || c == "Progman" || c == "WorkerW" || c == "Windows.UI.Core.CoreWindow" {
            continue;
        }
        return h.0 as u64;
    }
    0
}

pub fn list_visible() -> Vec<WindowInfo> {
    let mut cache = ExeCache::default();
    top_level_windows()
        .into_iter()
        .filter(|&h| visible(h))
        .map(|h| {
            let probe = if is_kicad_exe(&cache.exe(pid_of(h))) { 150 } else { 0 };
            info(h, &mut cache, probe)
        })
        .collect()
}

/// Every descendant of a window with class and text, `owner` carrying the PARENT handle
/// so a caller can join "the `Edit` whose parent is titled `searchCtrl`". Text comes from
/// the window's own store first; when the dialog's thread answers a WM_NULL, controls
/// with an empty store (Edit boxes) get a short time-boxed WM_GETTEXT as well.
pub fn children(parent_hwnd: HWND) -> Vec<WindowInfo> {
    let mut cache = ExeCache::default();
    let pumping = responsive(parent_hwnd, 150);
    descendants(parent_hwnd)
        .into_iter()
        .map(|h| {
            let mut w = info(h, &mut cache, 0);
            w.owner = parent(h);
            if w.title.is_empty() && pumping {
                if let Some(t) = text_timeout(h, 150) {
                    w.title = t;
                }
            }
            w
        })
        .collect()
}

/// The wxGLCanvas child's rect relative to the top-left of `frame`'s window rect (the
/// same box `capture_window` renders), as (x, y, width, height). None when no GL canvas
/// exists (Cairo fallback canvas, or not an editor).
pub fn gl_canvas_rect(frame: HWND) -> Option<(i32, i32, i32, i32)> {
    let canvas = descendants(frame).into_iter().find(|&c| class_name(c) == "wxGLCanvas")?;
    let (cx, cy, cw, ch) = rect(canvas);
    let (fx, fy, _, _) = rect(frame);
    Some((cx - fx, cy - fy, cw, ch))
}

/// Is any part of the (x, y, w, h) rect on a monitor? `MONITOR_DEFAULTTONULL`, so a
/// second or third display counts exactly like the primary.
pub fn on_a_monitor(r: (i32, i32, i32, i32)) -> bool {
    let rc = RECT { left: r.0, top: r.1, right: r.0 + r.2, bottom: r.1 + r.3 };
    // SAFETY: `rc` outlives the call.
    let hmon = unsafe { MonitorFromRect(&rc, MONITOR_DEFAULTTONULL) };
    if hmon.is_invalid() {
        return false;
    }
    let mut mi = MONITORINFO { cbSize: std::mem::size_of::<MONITORINFO>() as u32, ..Default::default() };
    // SAFETY: cbSize is set; `mi` outlives the call.
    if !unsafe { GetMonitorInfoW(hmon, &mut mi) }.as_bool() {
        return false;
    }
    let m = mi.rcMonitor;
    let vis_w = rc.right.min(m.right) - rc.left.max(m.left);
    let vis_h = rc.bottom.min(m.bottom) - rc.top.max(m.top);
    vis_w > 0 && vis_h > 0
}