Files
claude-code-usage-bubble/src/platform/windows.rs
T
tiennm99 8d7a07b237 feat: hide the bubbles while another app is fullscreen
A 1.5 s thread asks the platform whether the foreground window is a
fullscreen window of another process and feeds the result through the
visibility state machine. Windows keeps the proven evaluation with an
own-process check in place of the bubble class check; Linux reads the
EWMH active window state over X11; native Wayland and macOS report false.
2026-10-21 15:58:02 +07:00

669 lines
24 KiB
Rust

//! Windows-only helpers. This is the only module that talks to Win32 APIs.
use std::io;
use std::path::Path;
use std::time::Duration;
use webview2_com::Microsoft::Web::WebView2::Win32::{
GetAvailableCoreWebView2BrowserVersionString, ICoreWebView2Settings3,
};
use windows::core::{Interface, HSTRING, PCWSTR, PWSTR};
use windows::Win32::Foundation::{
CloseHandle, GetLastError, SetLastError, HWND, LPARAM, LRESULT, RECT, WAIT_OBJECT_0,
WIN32_ERROR, WPARAM,
};
use windows::Win32::Graphics::Dwm::{
DwmGetWindowAttribute, DwmSetWindowAttribute, DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS,
DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_ROUND, DWM_WINDOW_CORNER_PREFERENCE,
};
use windows::Win32::Graphics::Gdi::{
GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST,
};
use windows::Win32::System::Threading::{OpenProcess, WaitForSingleObject, PROCESS_SYNCHRONIZE};
use windows::Win32::UI::Shell::{
DefSubclassProc, RemoveWindowSubclass, SHAppBarMessage,
SetCurrentProcessExplicitAppUserModelID, SetWindowSubclass, ABE_BOTTOM, ABE_LEFT, ABE_RIGHT,
ABE_TOP, ABM_GETTASKBARPOS, APPBARDATA,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetClassNameW, GetForegroundWindow, GetWindowLongPtrW, GetWindowRect, GetWindowThreadProcessId,
IsIconic, IsWindowVisible, MessageBoxW, SetForegroundWindow, SetWindowLongPtrW, SetWindowPos,
ShowWindow, GWL_EXSTYLE, GWL_STYLE, MA_NOACTIVATE, MB_ICONERROR, MB_OK, MB_SETFOREGROUND,
SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE,
SW_SHOWNOACTIVATE, WM_DISPLAYCHANGE, WM_EXITSIZEMOVE, WM_MOUSEACTIVATE, WM_NCDESTROY,
WM_SETTINGCHANGE, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
};
use crate::core::geom::{self, Edge, Rect};
use crate::core::proxy::{self, SystemProxy};
const INTERNET_SETTINGS_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
const PERSONALIZE_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
const AUMID_KEY_PREFIX: &str = r"Software\Classes\AppUserModelId\";
/// The static proxy from `HKCU\…\Internet Settings`, or `None` when
/// `ProxyEnable` is not 1 or no usable `ProxyServer` entry exists.
/// PAC/WPAD (`AutoConfigURL`, auto-detect) is deliberately not evaluated.
pub fn system_proxy() -> Option<SystemProxy> {
let key = windows_registry::CURRENT_USER
.open(INTERNET_SETTINGS_KEY)
.ok()?;
let enable = key.get_u32("ProxyEnable").ok();
let server = key.get_string("ProxyServer").ok();
let proxy_override = key.get_string("ProxyOverride").ok();
proxy::from_registry_values(enable, server.as_deref(), proxy_override.as_deref())
}
/// Whether the apps theme is dark. `SystemUsesLightTheme` is 1 for light;
/// a missing value means dark, as on a fresh Windows 11 install.
pub fn is_dark() -> bool {
let light = windows_registry::CURRENT_USER
.open(PERSONALIZE_KEY)
.and_then(|key| key.get_u32("SystemUsesLightTheme"));
!matches!(light, Ok(1))
}
/// Wait until process `pid` has exited, at most `timeout`. Returns true when
/// it is gone (or never existed), false on timeout or a failed wait.
pub fn wait_pid(pid: u32, timeout: Duration) -> bool {
// SAFETY: plain FFI call; the returned handle is closed below.
let Ok(handle) = (unsafe { OpenProcess(PROCESS_SYNCHRONIZE, false, pid) }) else {
// No such process: it already exited.
return true;
};
let ms = u32::try_from(timeout.as_millis()).unwrap_or(u32::MAX);
// SAFETY: `handle` is a valid process handle opened with SYNCHRONIZE.
let result = unsafe { WaitForSingleObject(handle, ms) };
// SAFETY: `handle` came from OpenProcess and is closed exactly once.
if let Err(err) = unsafe { CloseHandle(handle) } {
log::debug!("CloseHandle for pid {pid} failed: {err}");
}
result == WAIT_OBJECT_0
}
/// Whether the Microsoft Edge WebView2 runtime is installed.
pub fn webview2_available() -> bool {
let mut version = PWSTR::null();
// SAFETY: a null folder asks for the installed Evergreen runtime; the
// out pointer is valid for the call.
let result =
unsafe { GetAvailableCoreWebView2BrowserVersionString(PCWSTR::null(), &mut version) };
// Frees the CoTaskMem string; a null pointer yields "".
let version = webview2_com::take_pwstr(version);
match result {
Ok(()) if !version.is_empty() => {
log::info!("WebView2 runtime {version}");
true
}
Ok(()) => false,
Err(err) => {
log::warn!("WebView2 runtime lookup failed: {err}");
false
}
}
}
/// Show a blocking error message box with no owner window.
pub fn message_box_error(title: &str, text: &str) {
// SAFETY: both strings outlive the call; no owner window is passed.
let _ = unsafe {
MessageBoxW(
None,
&HSTRING::from(text),
&HSTRING::from(title),
MB_OK | MB_ICONERROR | MB_SETFOREGROUND,
)
};
}
/// Register `aumid` so toasts from this portable exe are shown although no
/// Start-menu shortcut carries the id: write
/// `HKCU\Software\Classes\AppUserModelId\<aumid>` with `DisplayName` and
/// `IconUri` (`icon_png` written to `icon_path`), then make it the process
/// id. Each step is attempted; the first error is returned after all ran.
pub fn register_aumid(
aumid: &str,
display_name: &str,
icon_png: &[u8],
icon_path: &Path,
) -> io::Result<()> {
let icon = write_if_changed(icon_path, icon_png);
if let Err(err) = &icon {
log::warn!("AUMID icon {} not written: {err}", icon_path.display());
}
let key = windows_registry::CURRENT_USER
.create(format!("{AUMID_KEY_PREFIX}{aumid}"))
.and_then(|key| {
key.set_string("DisplayName", display_name)?;
if icon.is_ok() {
key.set_string("IconUri", icon_path.to_string_lossy().as_ref())?;
}
Ok(())
})
.map_err(io::Error::from);
// SAFETY: the HSTRING outlives the call.
let process = unsafe { SetCurrentProcessExplicitAppUserModelID(&HSTRING::from(aumid)) }
.map_err(io::Error::from);
key.and(process).and(icon)
}
/// Write `bytes` to `path` unless it already holds exactly them.
fn write_if_changed(path: &Path, bytes: &[u8]) -> io::Result<()> {
if std::fs::read(path).is_ok_and(|current| current == bytes) {
return Ok(());
}
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(path, bytes)
}
/// The current user's start-at-login entry, `HKCU\…\Run\<name>`. Its data
/// is the quoted exe path, as every earlier release wrote it, so a path with
/// spaces cannot resolve to a planted prefix. HKLM is never written, so an
/// elevated run cannot leave an entry that a normal run cannot remove.
/// `is_enabled` honours a Task Manager disable in `StartupApproved\Run`.
pub fn autostart_entry(name: &str) -> io::Result<auto_launch::AutoLaunch> {
let exe = std::env::current_exe()?;
Ok(auto_launch::AutoLaunch::new(
name,
&quoted_path(&exe),
auto_launch::WindowsEnableMode::CurrentUser,
&[] as &[&str],
))
}
/// `path` wrapped in double quotes, the form a `Run` value needs.
fn quoted_path(path: &Path) -> String {
format!("\"{}\"", path.display())
}
/// The window handle of `window`.
fn hwnd_of(window: &tauri::WebviewWindow) -> io::Result<HWND> {
window
.hwnd()
.map_err(|err| io::Error::other(format!("no HWND for {}: {err}", window.label())))
}
/// Make a bubble a tool window that never activates: no Alt-Tab entry and
/// no focus taken on a click or a show. Run it before the first show.
pub fn apply_no_activate(window: &tauri::WebviewWindow) -> io::Result<()> {
add_ex_style(window, (WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW).0)
}
/// Make the details panel a tool window, so it has no Alt-Tab entry
/// (`skip_taskbar` only removes the taskbar button). It still activates:
/// the panel takes focus so that losing it can dismiss the panel. Run it
/// before the first show.
pub fn apply_toolwindow(window: &tauri::WebviewWindow) -> io::Result<()> {
add_ex_style(window, WS_EX_TOOLWINDOW.0)
}
/// OR `bits` into a window's extended style and apply the frame change.
fn add_ex_style(window: &tauri::WebviewWindow, bits: u32) -> io::Result<()> {
let hwnd = hwnd_of(window)?;
let wanted = bits as isize;
// SAFETY: `hwnd` is a live top-level window of this process.
let current = unsafe { GetWindowLongPtrW(hwnd, GWL_EXSTYLE) };
if current & wanted == wanted {
return Ok(());
}
// SAFETY: as above. A zero return is only an error when the last error
// is set, so it is cleared first.
unsafe { SetLastError(WIN32_ERROR(0)) };
let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_EXSTYLE, current | wanted) };
if previous == 0 {
let err = io::Error::last_os_error();
if err.raw_os_error().unwrap_or(0) != 0 {
return Err(err);
}
}
// Extended style changes take effect after a frame change.
// SAFETY: as above; nothing moves, resizes or activates.
unsafe {
SetWindowPos(
hwnd,
None,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED,
)
}
.map_err(io::Error::from)
}
/// Ask DWM for rounded window corners. Only Windows 11 knows the
/// attribute; Windows 10 returns an error and keeps square corners. DWM
/// ignores a repeated identical value, so calling this on every show is
/// cheap.
pub fn round_corners(window: &tauri::WebviewWindow) -> io::Result<()> {
let hwnd = hwnd_of(window)?;
let preference = DWMWCP_ROUND;
// SAFETY: `hwnd` is a live window of this process; the pointer and size
// describe `preference`, which outlives the call.
unsafe {
DwmSetWindowAttribute(
hwnd,
DWMWA_WINDOW_CORNER_PREFERENCE,
std::ptr::from_ref(&preference).cast(),
std::mem::size_of::<DWM_WINDOW_CORNER_PREFERENCE>() as u32,
)
}
.map_err(io::Error::from)
}
/// Turn off WebView2's browser accelerator keys (F5, Ctrl+R, Ctrl+P,
/// Ctrl+F, zoom, F12 and the like) for `window`'s webview, then reload it,
/// because the setting applies from the next navigation. Call it only after
/// the first navigation has finished, or the reload can replace that
/// navigation. The work runs on the main thread; a failure inside the
/// webview is logged there.
pub fn disable_browser_accelerator_keys(window: &tauri::WebviewWindow) -> io::Result<()> {
let label = window.label().to_string();
window
.with_webview(move |webview| {
// SAFETY: plain COM calls on the live controller Tauri handed
// over, on the main thread that owns it.
let result = unsafe {
webview.controller().CoreWebView2().and_then(|core| {
let settings: ICoreWebView2Settings3 = core.Settings()?.cast()?;
settings.SetAreBrowserAcceleratorKeysEnabled(false)?;
core.Reload()
})
};
if let Err(err) = result {
log::warn!("{label} keeps the browser accelerator keys: {err}");
}
})
.map_err(|err| io::Error::other(format!("no webview for {}: {err}", window.label())))
}
/// Show a bubble without activating it. Bubbles are shown and hidden only
/// through this and [`hide`], never through Tauri's `show`/`hide`.
pub fn show_no_activate(window: &tauri::WebviewWindow) -> io::Result<()> {
let hwnd = hwnd_of(window)?;
// SAFETY: `hwnd` is a live window of this process. The return value is
// the previous visibility, not an error.
let _ = unsafe { ShowWindow(hwnd, SW_SHOWNOACTIVATE) };
Ok(())
}
/// Hide a bubble (see [`show_no_activate`]).
pub fn hide(window: &tauri::WebviewWindow) -> io::Result<()> {
let hwnd = hwnd_of(window)?;
// SAFETY: as in `show_no_activate`.
let _ = unsafe { ShowWindow(hwnd, SW_HIDE) };
Ok(())
}
/// A bubble window message the app reacts to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BubbleHookEvent {
/// An OS move loop ended (`WM_EXITSIZEMOVE`): the user dropped the bubble.
MoveEnd,
/// A system setting changed (`WM_SETTINGCHANGE`): theme, taskbar, work area.
SettingChange,
/// The display layout or resolution changed (`WM_DISPLAYCHANGE`).
DisplayChange,
}
type BubbleHookCallback = Box<dyn Fn(BubbleHookEvent) + Send + 'static>;
/// Subclass id of the bubble hook ("BUBB").
const BUBBLE_SUBCLASS_ID: usize = 0x4255_4242;
/// Subclass a bubble window so `callback` hears about move ends, setting
/// changes and display changes. The callback runs inside the window
/// procedure on the main thread, after the default handling; it must not
/// block, and should hand any Tauri work to another thread. The hook also
/// answers `WM_MOUSEACTIVATE` with `MA_NOACTIVATE`, so a click never
/// activates the bubble. Must be called on the main thread (the window's
/// thread); the hook removes itself when the window is destroyed.
pub fn install_bubble_hook(
window: &tauri::WebviewWindow,
callback: impl Fn(BubbleHookEvent) + Send + 'static,
) -> io::Result<()> {
let hwnd = hwnd_of(window)?;
let boxed: Box<BubbleHookCallback> = Box::new(Box::new(callback));
let data = Box::into_raw(boxed);
// SAFETY: `hwnd` belongs to this thread; `data` stays valid until the
// hook frees it on WM_NCDESTROY.
let installed = unsafe {
SetWindowSubclass(
hwnd,
Some(bubble_subclass_proc),
BUBBLE_SUBCLASS_ID,
data as usize,
)
};
if installed.as_bool() {
Ok(())
} else {
// SAFETY: the hook was not installed, so nothing else owns `data`.
drop(unsafe { Box::from_raw(data) });
Err(io::Error::other(format!(
"SetWindowSubclass failed for {}",
window.label()
)))
}
}
unsafe extern "system" fn bubble_subclass_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
_id: usize,
data: usize,
) -> LRESULT {
match msg {
WM_MOUSEACTIVATE => return LRESULT(MA_NOACTIVATE as isize),
WM_NCDESTROY => {
// SAFETY: the hook was installed on this window with this proc
// and id; `data` is the box `install_bubble_hook` leaked, freed
// exactly once here after the hook is gone.
unsafe {
let _ = RemoveWindowSubclass(hwnd, Some(bubble_subclass_proc), BUBBLE_SUBCLASS_ID);
drop(Box::from_raw(data as *mut BubbleHookCallback));
return DefSubclassProc(hwnd, msg, wparam, lparam);
}
}
_ => {}
}
// SAFETY: forwarding the message down the subclass chain.
let result = unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) };
let event = match msg {
WM_EXITSIZEMOVE => Some(BubbleHookEvent::MoveEnd),
WM_SETTINGCHANGE => Some(BubbleHookEvent::SettingChange),
WM_DISPLAYCHANGE => Some(BubbleHookEvent::DisplayChange),
_ => None,
};
if let Some(event) = event {
// SAFETY: `data` stays valid until WM_NCDESTROY, handled above.
let callback = unsafe { &*(data as *const BubbleHookCallback) };
callback(event);
}
result
}
/// The taskbar's docked edge and rect, when the shell reports one.
pub fn taskbar() -> Option<(Edge, Rect)> {
let mut data = APPBARDATA {
cbSize: std::mem::size_of::<APPBARDATA>() as u32,
..Default::default()
};
// SAFETY: `data` is a correctly sized APPBARDATA.
if unsafe { SHAppBarMessage(ABM_GETTASKBARPOS, &mut data) } == 0 {
return None;
}
let edge = match data.uEdge {
ABE_LEFT => Edge::Left,
ABE_TOP => Edge::Top,
ABE_RIGHT => Edge::Right,
ABE_BOTTOM => Edge::Bottom,
_ => return None,
};
let rc = data.rc;
Some((edge, Rect::new(rc.left, rc.top, rc.right, rc.bottom)))
}
/// Bring `window` to the foreground. A popup menu needs its owner in the
/// foreground, or it does not dismiss on a click elsewhere.
pub fn set_foreground(window: &tauri::WebviewWindow) {
match window.hwnd() {
// SAFETY: the handle belongs to a live window of this process.
Ok(hwnd) => {
if !unsafe { SetForegroundWindow(hwnd) }.as_bool() {
log::debug!("SetForegroundWindow({}) refused", window.label());
}
}
Err(err) => log::warn!("no HWND for {}: {err}", window.label()),
}
}
// ---------- Fullscreen detection ----------
/// Shell windows that cover a monitor without being a fullscreen app.
const IGNORED_FULLSCREEN_CLASSES: [&str; 3] = ["Progman", "WorkerW", "Shell_TrayWnd"];
struct FullscreenEvaluation {
is_fullscreen: bool,
hwnd: HWND,
class_name: String,
bounds: Option<Rect>,
bounds_source: &'static str,
monitor: Option<Rect>,
reason: &'static str,
}
/// Whether the foreground window covers its whole monitor as a fullscreen
/// app: not a window of this process (bubbles, panel, popup menu, WebView
/// children), not a shell window, visible, not minimized or cloaked, its
/// visible bounds within 2 px of the monitor rect and its style borderless
/// or popup.
pub fn foreground_is_fullscreen() -> super::FullscreenCheck {
// SAFETY: plain FFI call with no arguments.
let fg = unsafe { GetForegroundWindow() };
let evaluation = evaluate_foreground_fullscreen(fg);
super::FullscreenCheck {
fullscreen: evaluation.is_fullscreen,
reason: evaluation.reason,
detail: format!(
"fg=0x{:X} class={} bounds_source={} bounds={} monitor={}",
evaluation.hwnd.0 as usize,
if evaluation.class_name.is_empty() {
"<unknown>"
} else {
evaluation.class_name.as_str()
},
evaluation.bounds_source,
format_rect(evaluation.bounds.as_ref()),
format_rect(evaluation.monitor.as_ref()),
),
}
}
fn evaluate_foreground_fullscreen(fg: HWND) -> FullscreenEvaluation {
let mut evaluation = FullscreenEvaluation {
is_fullscreen: false,
hwnd: fg,
class_name: String::new(),
bounds: None,
bounds_source: "none",
monitor: None,
reason: "not evaluated",
};
if fg.is_invalid() {
evaluation.reason = "no foreground window";
return evaluation;
}
evaluation.class_name = window_class_name(fg);
if window_process_id(fg) == Some(std::process::id()) {
evaluation.reason = "foreground window is ours";
return evaluation;
}
if is_ignored_fullscreen_class(&evaluation.class_name) {
evaluation.reason = "ignored foreground class";
return evaluation;
}
// SAFETY: plain queries on a window handle; a stale one returns false.
if unsafe { !IsWindowVisible(fg).as_bool() || IsIconic(fg).as_bool() } {
evaluation.reason = "foreground invisible or minimized";
return evaluation;
}
if is_dwm_cloaked(fg) {
evaluation.reason = "foreground cloaked";
return evaluation;
}
let Some((bounds, source)) = visible_window_bounds(fg) else {
evaluation.reason = "foreground bounds unavailable";
return evaluation;
};
evaluation.bounds = Some(bounds);
evaluation.bounds_source = source;
// SAFETY: as above; NEAREST always yields a monitor for a live window.
let monitor = unsafe { MonitorFromWindow(fg, MONITOR_DEFAULTTONEAREST) };
if monitor.is_invalid() {
evaluation.reason = "foreground monitor unavailable";
return evaluation;
}
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
// SAFETY: `info` is a correctly sized MONITORINFO.
if unsafe { !GetMonitorInfoW(monitor, &mut info).as_bool() } {
evaluation.reason = "foreground monitor info unavailable";
return evaluation;
}
let monitor = rect_from(&info.rcMonitor);
evaluation.monitor = Some(monitor);
if !geom::rect_covers_monitor(&bounds, &monitor) {
evaluation.reason = "visible bounds do not cover monitor";
return evaluation;
}
if !window_style_allows_fullscreen(fg) {
evaluation.reason = "standard framed window";
return evaluation;
}
evaluation.is_fullscreen = true;
evaluation.reason = "visible bounds cover monitor";
evaluation
}
/// The id of the process that created `hwnd`; `None` for a stale handle.
fn window_process_id(hwnd: HWND) -> Option<u32> {
let mut pid = 0u32;
// SAFETY: `pid` outlives the call; a stale handle returns thread id 0.
let thread = unsafe { GetWindowThreadProcessId(hwnd, Some(std::ptr::from_mut(&mut pid))) };
(thread != 0).then_some(pid)
}
/// The visible bounds of `hwnd`: the DWM extended frame (without the
/// invisible resize borders), else the window rect.
fn visible_window_bounds(hwnd: HWND) -> Option<(Rect, &'static str)> {
let mut rect = RECT::default();
// SAFETY: the pointer and size describe `rect`, which outlives the call.
let dwm_ok = unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
std::ptr::from_mut(&mut rect).cast(),
std::mem::size_of::<RECT>() as u32,
)
.is_ok()
};
if dwm_ok && !rect_is_empty(&rect) {
return Some((rect_from(&rect), "dwm-extended-frame"));
}
// SAFETY: as above.
if unsafe { GetWindowRect(hwnd, &mut rect) }.is_err() {
return None;
}
if rect_is_empty(&rect) {
None
} else {
Some((rect_from(&rect), "window-rect"))
}
}
/// Whether DWM cloaks `hwnd` (a suspended UWP app or a window on another
/// virtual desktop).
fn is_dwm_cloaked(hwnd: HWND) -> bool {
let mut cloaked = 0u32;
// SAFETY: the pointer and size describe `cloaked`, which outlives the call.
unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
std::ptr::from_mut(&mut cloaked).cast(),
std::mem::size_of::<u32>() as u32,
)
.is_ok()
&& cloaked != 0
}
}
fn window_class_name(hwnd: HWND) -> String {
let mut buf = [0u16; 256];
// SAFETY: the slice bounds the write.
let len = unsafe { GetClassNameW(hwnd, &mut buf) };
match usize::try_from(len) {
Ok(len) if len > 0 => String::from_utf16_lossy(&buf[..len]),
_ => String::new(),
}
}
fn is_ignored_fullscreen_class(class_name: &str) -> bool {
IGNORED_FULLSCREEN_CLASSES
.iter()
.any(|ignored| class_name.eq_ignore_ascii_case(ignored))
}
fn window_style_allows_fullscreen(hwnd: HWND) -> bool {
// SAFETY: a zero style is only an error when the last error is set,
// so it is cleared first.
unsafe { SetLastError(WIN32_ERROR(0)) };
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) };
if style == 0 && unsafe { GetLastError() } != WIN32_ERROR(0) {
return false;
}
// The style is a 32-bit value in a pointer-sized return.
geom::window_style_bits_allow_fullscreen(style as u32)
}
fn rect_from(rect: &RECT) -> Rect {
Rect::new(rect.left, rect.top, rect.right, rect.bottom)
}
fn rect_is_empty(rect: &RECT) -> bool {
rect.right <= rect.left || rect.bottom <= rect.top
}
fn format_rect(rect: Option<&Rect>) -> String {
match rect {
Some(r) => format!("({},{} {}x{})", r.l, r.t, r.r - r.l, r.b - r.t),
None => String::from("<none>"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_classes_are_ignored_case_insensitively() {
assert!(is_ignored_fullscreen_class("Progman"));
assert!(is_ignored_fullscreen_class("workerw"));
assert!(is_ignored_fullscreen_class("SHELL_TRAYWND"));
assert!(!is_ignored_fullscreen_class("Chrome_WidgetWin_1"));
assert!(!is_ignored_fullscreen_class(""));
}
#[test]
fn a_null_window_has_no_process() {
assert_eq!(window_process_id(HWND::default()), None);
}
#[test]
fn run_value_path_is_quoted() {
assert_eq!(
quoted_path(Path::new(r"D:\My Tools\claude-code-usage-bubble.exe")),
r#""D:\My Tools\claude-code-usage-bubble.exe""#
);
}
}