Initial Version of Desktop Icon Hider.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
use windows::Win32::Foundation::ERROR_FILE_NOT_FOUND;
|
||||
use windows::core::HRESULT;
|
||||
use windows_registry::{CURRENT_USER, Result};
|
||||
|
||||
const RUN_KEY_PATH: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
const APP_KEY_PATH: &str = r"Software\DesktopIconHider";
|
||||
const VALUE_NAME: &str = "DesktopIconHider";
|
||||
const FIRST_RUN_MARKER: &str = "AutostartConfigured";
|
||||
|
||||
fn exe_path_quoted() -> String {
|
||||
let exe_path = std::env::current_exe()
|
||||
.expect("cannot resolve current exe path")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
format!("\"{exe_path}\"")
|
||||
}
|
||||
|
||||
fn is_not_found(e: &windows::core::Error) -> bool {
|
||||
e.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND.0)
|
||||
}
|
||||
|
||||
/// True if the Run key currently points at this exe. A missing key or value means
|
||||
/// autostart is simply off (`Ok(false)`), not an error condition.
|
||||
pub fn is_enabled() -> Result<bool> {
|
||||
match CURRENT_USER.open(RUN_KEY_PATH) {
|
||||
Ok(key) => match key.get_string(VALUE_NAME) {
|
||||
Ok(value) => Ok(value == exe_path_quoted()),
|
||||
Err(e) if is_not_found(&e) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Err(e) if is_not_found(&e) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes or removes the Run-key value.
|
||||
pub fn set_enabled(enabled: bool) -> Result<()> {
|
||||
if enabled {
|
||||
CURRENT_USER
|
||||
.create(RUN_KEY_PATH)?
|
||||
.set_string(VALUE_NAME, exe_path_quoted())?;
|
||||
Ok(())
|
||||
} else {
|
||||
match CURRENT_USER.open(RUN_KEY_PATH) {
|
||||
Ok(key) => match key.remove_value(VALUE_NAME) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if is_not_found(&e) => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Err(e) if is_not_found(&e) => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On a genuine first-ever run (no marker yet under our own app key), force-enables
|
||||
/// autostart once and writes the marker — so it defaults on for new installs, but never
|
||||
/// re-forces itself back on after the user explicitly disables it via the tray menu.
|
||||
/// Call once at startup, before reading `is_enabled()` for the menu's initial state.
|
||||
pub fn ensure_first_run_default() -> Result<()> {
|
||||
let app_key = CURRENT_USER.create(APP_KEY_PATH)?;
|
||||
if app_key.get_u32(FIRST_RUN_MARKER).is_err() {
|
||||
set_enabled(true)?;
|
||||
app_key.set_u32(FIRST_RUN_MARKER, 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use windows::Win32::Foundation::{CloseHandle, HWND, LPARAM, POINT, WPARAM};
|
||||
use windows::Win32::Graphics::Gdi::ScreenToClient;
|
||||
use windows::Win32::System::Diagnostics::Debug::{ReadProcessMemory, WriteProcessMemory};
|
||||
use windows::Win32::System::Memory::{
|
||||
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, VirtualAllocEx, VirtualFreeEx,
|
||||
};
|
||||
use windows::Win32::System::Threading::{
|
||||
OpenProcess, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE,
|
||||
};
|
||||
use windows::Win32::UI::Controls::{LVHITTESTINFO, LVHT_NOWHERE, LVM_HITTEST};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
EnumWindows, FindWindowExW, FindWindowW, GA_PARENT, GetAncestor, GetClassNameW,
|
||||
GetWindowThreadProcessId, IsWindowVisible, SMTO_ABORTIFHUNG, SendMessageTimeoutW,
|
||||
SendMessageW, WM_COMMAND, WindowFromPoint,
|
||||
};
|
||||
use windows::core::{BOOL, w};
|
||||
|
||||
/// The documented "Show desktop icons" toggle command, sent as a WM_COMMAND wParam
|
||||
/// to Explorer's desktop view (the same command right-click -> View -> "Show desktop
|
||||
/// icons" sends).
|
||||
const TOGGLE_DESKTOP_ICONS_CMD: usize = 0x7402;
|
||||
|
||||
/// LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON — not exposed as a
|
||||
/// combined constant by the `windows` crate, so it's reconstructed here.
|
||||
const LVHT_ONITEM_MASK: u32 = 0x000E;
|
||||
|
||||
/// True if the given screen point landed on the desktop's *empty* area.
|
||||
///
|
||||
/// When desktop icons are visible, the window under the cursor is the real desktop
|
||||
/// SysListView32 (walked up through SHELLDLL_DefView -> Progman/WorkerW, to rule out
|
||||
/// other SysListView32 controls such as File Explorer's file list); a list-view hit
|
||||
/// test at that point must report "nowhere" rather than an actual icon.
|
||||
///
|
||||
/// When desktop icons are currently hidden, Explorer hides the SysListView32 window
|
||||
/// itself (rather than just its items), so WindowFromPoint skips straight past it to
|
||||
/// its parent SHELLDLL_DefView (or, if that's absent too, Progman/WorkerW directly).
|
||||
/// In that state there are no icons to click at all, so landing on those surfaces
|
||||
/// counts as empty space too — this is what lets a second double-click toggle icons
|
||||
/// back on.
|
||||
///
|
||||
/// Any other mismatch along the way conservatively returns false so we never toggle
|
||||
/// when unsure.
|
||||
pub fn is_desktop_empty_space_click(screen_x: i32, screen_y: i32) -> bool {
|
||||
unsafe {
|
||||
let pt = POINT { x: screen_x, y: screen_y };
|
||||
let hwnd = WindowFromPoint(pt);
|
||||
if hwnd.is_invalid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if class_name_is(hwnd, "SysListView32") {
|
||||
let parent = GetAncestor(hwnd, GA_PARENT);
|
||||
if !class_name_is(parent, "SHELLDLL_DefView") {
|
||||
return false;
|
||||
}
|
||||
let grandparent = GetAncestor(parent, GA_PARENT);
|
||||
if !(class_name_is(grandparent, "Progman") || class_name_is(grandparent, "WorkerW")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut client_pt = pt;
|
||||
if !ScreenToClient(hwnd, &mut client_pt).as_bool() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match remote_hit_test(hwnd, client_pt) {
|
||||
Some(info) => {
|
||||
let flags = info.flags.0;
|
||||
(flags & LVHT_NOWHERE.0) != 0 && (flags & LVHT_ONITEM_MASK) == 0
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
} else if class_name_is(hwnd, "SHELLDLL_DefView") {
|
||||
let parent = GetAncestor(hwnd, GA_PARENT);
|
||||
class_name_is(parent, "Progman") || class_name_is(parent, "WorkerW")
|
||||
} else {
|
||||
class_name_is(hwnd, "Progman") || class_name_is(hwnd, "WorkerW")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends LVM_HITTEST to `hwnd`, a SysListView32 owned by another process (Explorer).
|
||||
///
|
||||
/// SendMessage does not marshal arbitrary struct pointers across a process boundary —
|
||||
/// only a small whitelist of messages (WM_COPYDATA, WM_GETTEXT, etc.) get that
|
||||
/// treatment. Passing a pointer to *our* stack directly, as an earlier version of this
|
||||
/// code did, made Explorer write the hit-test result through an address that's
|
||||
/// meaningless in its own address space: sometimes landing on unmapped memory (crashing
|
||||
/// Explorer, which Windows then restarts) and sometimes silently corrupting or reading
|
||||
/// back garbage. The correct approach is the same one automation tools use for
|
||||
/// cross-process list-view access: allocate a buffer inside Explorer's own process,
|
||||
/// write the input there, send the message, then read the result back.
|
||||
unsafe fn remote_hit_test(hwnd: HWND, client_pt: POINT) -> Option<LVHITTESTINFO> {
|
||||
unsafe {
|
||||
let mut pid = 0u32;
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut pid));
|
||||
if pid == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let process = OpenProcess(
|
||||
PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,
|
||||
false,
|
||||
pid,
|
||||
)
|
||||
.ok()?;
|
||||
|
||||
let size = size_of::<LVHITTESTINFO>();
|
||||
let remote_ptr = VirtualAllocEx(process, None, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if remote_ptr.is_null() {
|
||||
let _ = CloseHandle(process);
|
||||
return None;
|
||||
}
|
||||
|
||||
let input = LVHITTESTINFO {
|
||||
pt: client_pt,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = if WriteProcessMemory(
|
||||
process,
|
||||
remote_ptr,
|
||||
&input as *const LVHITTESTINFO as *const c_void,
|
||||
size,
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
SendMessageW(
|
||||
hwnd,
|
||||
LVM_HITTEST,
|
||||
Some(WPARAM(0)),
|
||||
Some(LPARAM(remote_ptr as isize)),
|
||||
);
|
||||
|
||||
let mut out = LVHITTESTINFO::default();
|
||||
if ReadProcessMemory(
|
||||
process,
|
||||
remote_ptr,
|
||||
&mut out as *mut LVHITTESTINFO as *mut c_void,
|
||||
size,
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Some(out)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let _ = VirtualFreeEx(process, remote_ptr, 0, MEM_RELEASE);
|
||||
let _ = CloseHandle(process);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn class_name_is(hwnd: HWND, expected: &str) -> bool {
|
||||
if hwnd.is_invalid() {
|
||||
return false;
|
||||
}
|
||||
let mut buf = [0u16; 256];
|
||||
let len = unsafe { GetClassNameW(hwnd, &mut buf) };
|
||||
if len <= 0 {
|
||||
return false;
|
||||
}
|
||||
String::from_utf16_lossy(&buf[..len as usize]).eq_ignore_ascii_case(expected)
|
||||
}
|
||||
|
||||
/// Sends the "Show desktop icons" toggle command to Explorer's desktop view, then reads
|
||||
/// back the actual resulting visibility rather than assuming success. Returns
|
||||
/// `Some(true)`/`Some(false)` for icons now shown/hidden, or `None` if the toggle
|
||||
/// couldn't be delivered or confirmed. Reading back (instead of tracking our own
|
||||
/// "current state") avoids drift if the user toggles icons some other way (e.g.
|
||||
/// Explorer's own View menu) between our calls.
|
||||
pub fn toggle_desktop_icons() -> Option<bool> {
|
||||
let defview = unsafe { find_shelldll_defview() }?;
|
||||
|
||||
let sent = unsafe {
|
||||
let mut result = 0usize;
|
||||
SendMessageTimeoutW(
|
||||
defview,
|
||||
WM_COMMAND,
|
||||
WPARAM(TOGGLE_DESKTOP_ICONS_CMD),
|
||||
LPARAM(0),
|
||||
SMTO_ABORTIFHUNG,
|
||||
1000,
|
||||
Some(&mut result),
|
||||
)
|
||||
};
|
||||
if sent.0 == 0 {
|
||||
log::warn!("toggle_desktop_icons: SendMessageTimeoutW failed or timed out");
|
||||
return None;
|
||||
}
|
||||
|
||||
// SendMessageTimeoutW blocks until Explorer's WM_COMMAND handler returns, so the
|
||||
// listview's visibility already reflects the new state by now.
|
||||
let listview = unsafe { FindWindowExW(Some(defview), None, w!("SysListView32"), None) }.ok()?;
|
||||
Some(unsafe { IsWindowVisible(listview) }.as_bool())
|
||||
}
|
||||
|
||||
/// Locates Explorer's desktop-view window. Normally a child of "Progman", but on some
|
||||
/// multi-monitor / wallpaper-slideshow configurations Explorer reparents it under a
|
||||
/// sibling "WorkerW" window instead, so that's checked as a fallback.
|
||||
unsafe fn find_shelldll_defview() -> Option<HWND> {
|
||||
unsafe {
|
||||
if let Ok(progman) = FindWindowW(w!("Progman"), None) {
|
||||
if let Ok(dv) = FindWindowExW(Some(progman), None, w!("SHELLDLL_DefView"), None) {
|
||||
return Some(dv);
|
||||
}
|
||||
}
|
||||
|
||||
let mut found: Option<HWND> = None;
|
||||
let _ = EnumWindows(
|
||||
Some(enum_worker_w),
|
||||
LPARAM(&mut found as *mut Option<HWND> as isize),
|
||||
);
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn enum_worker_w(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
unsafe {
|
||||
if let Ok(dv) = FindWindowExW(Some(hwnd), None, w!("SHELLDLL_DefView"), None) {
|
||||
*(lparam.0 as *mut Option<HWND>) = Some(dv);
|
||||
return BOOL(0); // stop enumerating
|
||||
}
|
||||
}
|
||||
BOOL(1) // continue
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use windows::Win32::Foundation::{LPARAM, LRESULT, WPARAM};
|
||||
use windows::Win32::System::Threading::GetCurrentThreadId;
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::{GetDoubleClickTime};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CallNextHookEx, GetSystemMetrics, PostThreadMessageW, MSLLHOOKSTRUCT, SM_CXDOUBLECLK,
|
||||
SM_CYDOUBLECLK, WM_LBUTTONDOWN,
|
||||
};
|
||||
|
||||
use crate::state::{PAUSED, WM_APP_CANDIDATE_DBLCLICK};
|
||||
|
||||
thread_local! {
|
||||
// (x, y, tick_ms) of the last recorded left-button-down.
|
||||
static LAST_CLICK: Cell<(i32, i32, u32)> = const { Cell::new((0, 0, 0)) };
|
||||
}
|
||||
|
||||
/// Low-level mouse hook callback (`WH_MOUSE_LL`). Runs on the thread that installed
|
||||
/// the hook, for every left-button-down system-wide. Detects double-clicks manually
|
||||
/// (WH_MOUSE_LL never sees a synthesized WM_LBUTTONDBLCLK) and, on a match, posts a
|
||||
/// thread message so the actual hit-test/toggle work happens on the main loop instead
|
||||
/// of inside this callback. Always chains via CallNextHookEx and never swallows the
|
||||
/// click, so double-click-to-open still works normally on real icons.
|
||||
pub unsafe extern "system" fn low_level_mouse_proc(
|
||||
code: i32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
if code >= 0 && wparam.0 as u32 == WM_LBUTTONDOWN && !PAUSED.load(Ordering::Relaxed) {
|
||||
let info = unsafe { &*(lparam.0 as *const MSLLHOOKSTRUCT) };
|
||||
let (x, y, t) = (info.pt.x, info.pt.y, info.time);
|
||||
|
||||
LAST_CLICK.with(|cell| {
|
||||
let (lx, ly, lt) = cell.get();
|
||||
let dt = t.wrapping_sub(lt);
|
||||
let dx = (x - lx).abs();
|
||||
let dy = (y - ly).abs();
|
||||
|
||||
let is_double_click = lt != 0
|
||||
&& dt <= unsafe { GetDoubleClickTime() }
|
||||
&& dx <= unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }
|
||||
&& dy <= unsafe { GetSystemMetrics(SM_CYDOUBLECLK) };
|
||||
|
||||
if is_double_click {
|
||||
// Reset so a third click isn't misread as another double-click.
|
||||
cell.set((0, 0, 0));
|
||||
unsafe {
|
||||
let _ = PostThreadMessageW(
|
||||
GetCurrentThreadId(),
|
||||
WM_APP_CANDIDATE_DBLCLICK,
|
||||
WPARAM(x as usize),
|
||||
LPARAM(y as isize),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cell.set((x, y, t));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unsafe { CallNextHookEx(None, code, wparam, lparam) }
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
mod autostart;
|
||||
mod desktop;
|
||||
mod hook;
|
||||
mod state;
|
||||
mod tray;
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use muda::MenuEvent;
|
||||
use windows::Win32::Foundation::{ERROR_ALREADY_EXISTS, GetLastError, LPARAM, WPARAM};
|
||||
use windows::Win32::System::Threading::{CreateMutexW, GetCurrentThreadId};
|
||||
use windows::Win32::UI::HiDpi::{
|
||||
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
DispatchMessageW, GetMessageW, PostQuitMessage, PostThreadMessageW, SetWindowsHookExW,
|
||||
TranslateMessage, UnhookWindowsHookEx, MSG, WH_MOUSE_LL,
|
||||
};
|
||||
use windows::core::w;
|
||||
|
||||
fn main() {
|
||||
// If another instance already holds this mutex, exit immediately — a second copy
|
||||
// would install a second mouse hook, and every double-click would toggle icons
|
||||
// twice (net no-op). CreateMutexW returns Ok(handle) even when the mutex already
|
||||
// existed, so the only reliable signal is GetLastError(), checked unconditionally.
|
||||
let _single_instance_mutex = unsafe {
|
||||
let handle = CreateMutexW(None, false, w!("DesktopIconHider-9F2E7B1A-SingleInstance"));
|
||||
if GetLastError() == ERROR_ALREADY_EXISTS {
|
||||
return;
|
||||
}
|
||||
handle
|
||||
};
|
||||
|
||||
// Must happen before any window is created (including the tray icon's hidden
|
||||
// window) — otherwise Windows DPI-virtualizes our coordinate space, which
|
||||
// desyncs from the raw physical-pixel coordinates the mouse hook reports
|
||||
// whenever the display scale isn't 100%.
|
||||
unsafe {
|
||||
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
if let Err(e) = autostart::ensure_first_run_default() {
|
||||
log::warn!("failed to configure autostart default: {e}");
|
||||
}
|
||||
|
||||
let tray_handles = tray::build_tray();
|
||||
let pause_item = tray_handles.pause_item.clone();
|
||||
let autostart_item = tray_handles.autostart_item.clone();
|
||||
let (toggle_id, pause_id, autostart_id, exit_id) = (
|
||||
tray_handles.toggle_id.clone(),
|
||||
tray_handles.pause_item.id().clone(),
|
||||
tray_handles.autostart_item.id().clone(),
|
||||
tray_handles.exit_id.clone(),
|
||||
);
|
||||
|
||||
// muda's event handler must be Send + Sync, but MenuItem/CheckMenuItem (needed to
|
||||
// update the pause item's label and correct the autostart checkbox) aren't — both
|
||||
// are Rc-based. So the handler only touches ids, atomics, and plain FFI calls (the
|
||||
// desktop/registry calls don't touch any Rc-based muda type), then posts a
|
||||
// thread message for anything that needs the actual menu item; the main loop, which
|
||||
// owns those items locally, handles the rest.
|
||||
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
|
||||
if event.id == toggle_id {
|
||||
desktop::toggle_desktop_icons();
|
||||
} else if event.id == pause_id {
|
||||
state::PAUSED.fetch_xor(true, Ordering::Relaxed);
|
||||
unsafe {
|
||||
let _ = PostThreadMessageW(
|
||||
GetCurrentThreadId(),
|
||||
state::WM_APP_PAUSE_TOGGLED,
|
||||
WPARAM(0),
|
||||
LPARAM(0),
|
||||
);
|
||||
}
|
||||
} else if event.id == autostart_id {
|
||||
// muda already flipped the native checkmark before this handler ran; this
|
||||
// just performs the actual registry write and reports back the real result
|
||||
// so the main loop can correct the checkbox if the write failed.
|
||||
let desired = !autostart::is_enabled().unwrap_or(false);
|
||||
if let Err(e) = autostart::set_enabled(desired) {
|
||||
log::warn!("failed to update autostart: {e}");
|
||||
}
|
||||
let actual = autostart::is_enabled().unwrap_or(false);
|
||||
unsafe {
|
||||
let _ = PostThreadMessageW(
|
||||
GetCurrentThreadId(),
|
||||
state::WM_APP_AUTOSTART_TOGGLED,
|
||||
WPARAM(actual as usize),
|
||||
LPARAM(0),
|
||||
);
|
||||
}
|
||||
} else if event.id == exit_id {
|
||||
unsafe { PostQuitMessage(0) };
|
||||
}
|
||||
}));
|
||||
|
||||
let hook = unsafe { SetWindowsHookExW(WH_MOUSE_LL, Some(hook::low_level_mouse_proc), None, 0) }
|
||||
.expect("failed to install low-level mouse hook");
|
||||
|
||||
let mut msg = MSG::default();
|
||||
unsafe {
|
||||
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
|
||||
if msg.message == state::WM_APP_CANDIDATE_DBLCLICK {
|
||||
let x = msg.wParam.0 as i32;
|
||||
let y = msg.lParam.0 as i32;
|
||||
if !state::PAUSED.load(Ordering::Relaxed)
|
||||
&& desktop::is_desktop_empty_space_click(x, y)
|
||||
{
|
||||
desktop::toggle_desktop_icons();
|
||||
}
|
||||
} else if msg.message == state::WM_APP_PAUSE_TOGGLED {
|
||||
let now_paused = state::PAUSED.load(Ordering::Relaxed);
|
||||
pause_item.set_text(if now_paused {
|
||||
"Unpause Monitoring"
|
||||
} else {
|
||||
"Pause Monitoring"
|
||||
});
|
||||
} else if msg.message == state::WM_APP_AUTOSTART_TOGGLED {
|
||||
let actual = msg.wParam.0 != 0;
|
||||
autostart_item.set_checked(actual);
|
||||
} else {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = UnhookWindowsHookEx(hook);
|
||||
}
|
||||
|
||||
drop(tray_handles.tray);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_APP;
|
||||
|
||||
/// When true, double-click monitoring is temporarily disabled (tray menu toggle).
|
||||
pub static PAUSED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Posted from the mouse-hook callback to the main thread's message loop when a
|
||||
/// candidate double-click is detected. WPARAM = screen x, LPARAM = screen y.
|
||||
/// Kept out of the hook callback itself so the hook stays cheap.
|
||||
pub const WM_APP_CANDIDATE_DBLCLICK: u32 = WM_APP + 1;
|
||||
|
||||
/// Posted from the (Send + Sync-bound) menu event handler when the pause state
|
||||
/// changes, so the main loop — which owns the actual (non-Send) MenuItem — can update
|
||||
/// its label without needing to move the item into that handler.
|
||||
pub const WM_APP_PAUSE_TOGGLED: u32 = WM_APP + 2;
|
||||
|
||||
/// Posted from the menu event handler after an autostart registry write, carrying the
|
||||
/// actual resulting enabled state in wParam. muda already flips the native checkmark
|
||||
/// itself on click, so the main loop only needs to correct it if the write failed.
|
||||
pub const WM_APP_AUTOSTART_TOGGLED: u32 = WM_APP + 3;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use muda::{CheckMenuItem, Menu, MenuId, MenuItem};
|
||||
use tray_icon::{Icon, TrayIcon, TrayIconBuilder};
|
||||
|
||||
use crate::autostart;
|
||||
|
||||
const TRAY_PNG: &[u8] = include_bytes!("../assets/tray-32.png");
|
||||
|
||||
pub struct TrayHandles {
|
||||
// Kept alive for as long as the tray icon should remain visible.
|
||||
pub tray: TrayIcon,
|
||||
pub toggle_id: MenuId,
|
||||
// Kept as the full item (not just its id) so its label can be flipped between
|
||||
// "Pause Monitoring" / "Unpause Monitoring" as the paused state changes.
|
||||
pub pause_item: MenuItem,
|
||||
// Kept as the full item so its checked state can be corrected if a registry write
|
||||
// fails (muda already flips the native checkmark itself on click).
|
||||
pub autostart_item: CheckMenuItem,
|
||||
pub exit_id: MenuId,
|
||||
}
|
||||
|
||||
pub fn build_tray() -> TrayHandles {
|
||||
let icon = load_icon();
|
||||
let autostart_enabled = autostart::is_enabled().unwrap_or(false);
|
||||
|
||||
let menu = Menu::new();
|
||||
let toggle_item = MenuItem::new("Toggle Icons Now", true, None);
|
||||
let pause_item = MenuItem::new("Pause Monitoring", true, None);
|
||||
let autostart_item = CheckMenuItem::new("Start with Windows", true, autostart_enabled, None);
|
||||
let exit_item = MenuItem::new("Exit", true, None);
|
||||
menu.append(&toggle_item).expect("failed to append menu item");
|
||||
menu.append(&pause_item).expect("failed to append menu item");
|
||||
menu.append(&autostart_item).expect("failed to append menu item");
|
||||
menu.append(&exit_item).expect("failed to append menu item");
|
||||
|
||||
let tray = TrayIconBuilder::new()
|
||||
.with_icon(icon)
|
||||
.with_menu(Box::new(menu))
|
||||
.with_tooltip("Desktop Icon Hider")
|
||||
.build()
|
||||
.expect("failed to create tray icon");
|
||||
|
||||
TrayHandles {
|
||||
tray,
|
||||
toggle_id: toggle_item.id().clone(),
|
||||
pause_item,
|
||||
autostart_item,
|
||||
exit_id: exit_item.id().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon() -> Icon {
|
||||
let img = image::load_from_memory(TRAY_PNG)
|
||||
.expect("embedded tray icon is invalid")
|
||||
.into_rgba8();
|
||||
let (width, height) = img.dimensions();
|
||||
Icon::from_rgba(img.into_raw(), width, height).expect("failed to build tray icon")
|
||||
}
|
||||
Reference in New Issue
Block a user