Skip to main content

automata_windows/
overlay.rs

1// ── Non-Windows stub ──────────────────────────────────────────────────────────
2
3#[cfg(not(target_os = "windows"))]
4use crate::Result;
5
6#[cfg(not(target_os = "windows"))]
7pub struct Overlay;
8
9#[cfg(not(target_os = "windows"))]
10impl Overlay {
11    pub fn new() -> Result<Self> {
12        anyhow::bail!("Windows only")
13    }
14    pub fn show_at(&self, _x: i32, _y: i32, _w: i32, _h: i32) {}
15}
16
17// ── Windows implementation ────────────────────────────────────────────────────
18
19#[cfg(target_os = "windows")]
20pub use win::Overlay;
21
22#[cfg(target_os = "windows")]
23mod win {
24    use std::sync::{Arc, Mutex};
25    use std::thread;
26
27    use windows::Win32::Foundation::{COLORREF, HWND, LPARAM, LRESULT, WPARAM};
28    use windows::Win32::Graphics::Gdi::{
29        CreatePen, CreateSolidBrush, DeleteObject, FillRect, GetDC, HGDIOBJ, PS_SOLID, Rectangle,
30        ReleaseDC, SelectObject,
31    };
32    use windows::Win32::System::LibraryLoader::GetModuleHandleW;
33    use windows::Win32::UI::WindowsAndMessaging::{
34        CreateWindowExW, DefWindowProcW, DispatchMessageW, IDC_ARROW, LWA_COLORKEY, LoadCursorW,
35        MSG, MoveWindow, PM_REMOVE, PeekMessageW, PostQuitMessage, RegisterClassExW,
36        SW_SHOWNOACTIVATE, SetLayeredWindowAttributes, ShowWindow, WM_DESTROY, WNDCLASSEXW,
37        WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT,
38        WS_POPUP,
39    };
40    use windows::core::PCWSTR;
41
42    use crate::Result;
43
44    unsafe extern "system" fn wnd_proc(
45        hwnd: HWND,
46        msg: u32,
47        wparam: WPARAM,
48        lparam: LPARAM,
49    ) -> LRESULT {
50        match msg {
51            WM_DESTROY => {
52                unsafe { PostQuitMessage(0) };
53                LRESULT(0)
54            }
55            _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },
56        }
57    }
58
59    fn draw_border(hwnd: HWND, width: i32, height: i32) {
60        const BORDER: i32 = 3;
61        const COLOR: u32 = 0x00FF00; // green (BGR)
62
63        unsafe {
64            let hdc = GetDC(Some(hwnd));
65            if hdc.is_invalid() {
66                return;
67            }
68
69            let black_brush = CreateSolidBrush(COLORREF(0x000000));
70            FillRect(
71                hdc,
72                &windows::Win32::Foundation::RECT {
73                    left: 0,
74                    top: 0,
75                    right: width,
76                    bottom: height,
77                },
78                black_brush,
79            );
80            let _ = DeleteObject(black_brush.into());
81
82            let pen = CreatePen(PS_SOLID, BORDER, COLORREF(COLOR));
83            let old_pen = SelectObject(hdc, HGDIOBJ(pen.0));
84            let fill = CreateSolidBrush(COLORREF(0x000000));
85            let old_brush = SelectObject(hdc, HGDIOBJ(fill.0));
86
87            let _ = Rectangle(hdc, 1, 1, width - 1, height - 1);
88
89            SelectObject(hdc, old_pen);
90            SelectObject(hdc, old_brush);
91            let _ = DeleteObject(HGDIOBJ(pen.0));
92            let _ = DeleteObject(fill.into());
93
94            ReleaseDC(Some(hwnd), hdc);
95        }
96    }
97
98    /// A transparent, always-on-top overlay window that draws a green border.
99    /// Runs its own message loop on a background thread; call `show_at` to reposition.
100    pub struct Overlay {
101        bounds: Arc<Mutex<Option<(i32, i32, i32, i32)>>>,
102        _thread: thread::JoinHandle<()>,
103    }
104
105    impl Overlay {
106        pub fn new() -> Result<Self> {
107            let bounds: Arc<Mutex<Option<(i32, i32, i32, i32)>>> = Arc::new(Mutex::new(None));
108            let bounds_clone = bounds.clone();
109
110            let handle = thread::spawn(move || unsafe {
111                use windows::Win32::System::Com::{COINIT_MULTITHREADED, CoInitializeEx};
112                let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
113
114                const CLASS: PCWSTR = windows::core::w!("UiCoreOverlay");
115                let instance = GetModuleHandleW(None).expect("GetModuleHandleW");
116
117                let wc = WNDCLASSEXW {
118                    cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
119                    lpfnWndProc: Some(wnd_proc),
120                    hInstance: instance.into(),
121                    hCursor: LoadCursorW(None, IDC_ARROW).unwrap_or_default(),
122                    lpszClassName: CLASS,
123                    ..Default::default()
124                };
125                let _ = RegisterClassExW(&wc);
126
127                let hwnd = CreateWindowExW(
128                    WS_EX_LAYERED
129                        | WS_EX_TRANSPARENT
130                        | WS_EX_TOPMOST
131                        | WS_EX_TOOLWINDOW
132                        | WS_EX_NOACTIVATE,
133                    CLASS,
134                    windows::core::w!("overlay"),
135                    WS_POPUP,
136                    0,
137                    0,
138                    1,
139                    1,
140                    None,
141                    None,
142                    Some(instance.into()),
143                    None,
144                )
145                .expect("CreateWindowExW");
146
147                SetLayeredWindowAttributes(hwnd, COLORREF(0x000000), 255, LWA_COLORKEY)
148                    .expect("SetLayeredWindowAttributes");
149                let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
150
151                let mut msg = MSG::default();
152                let mut last: Option<(i32, i32, i32, i32)> = None;
153
154                loop {
155                    while PeekMessageW(&mut msg, Some(hwnd), 0, 0, PM_REMOVE).as_bool() {
156                        if msg.message == WM_DESTROY {
157                            return;
158                        }
159                        let _ = DispatchMessageW(&msg);
160                    }
161
162                    let current = *bounds_clone.lock().unwrap();
163                    if current != last {
164                        if let Some((x, y, w, h)) = current {
165                            if w > 0 && h > 0 {
166                                let _ = MoveWindow(hwnd, x, y, w, h, true);
167                                draw_border(hwnd, w, h);
168                            }
169                        }
170                        last = current;
171                    }
172
173                    thread::sleep(std::time::Duration::from_millis(33));
174                }
175            });
176
177            Ok(Self {
178                bounds,
179                _thread: handle,
180            })
181        }
182
183        /// Move the overlay border to cover `(x, y, w, h)`.
184        pub fn show_at(&self, x: i32, y: i32, w: i32, h: i32) {
185            *self.bounds.lock().unwrap() = Some((x, y, w, h));
186        }
187    }
188}