8 Commits

11 changed files with 2228 additions and 8 deletions
-7
View File
@@ -9,10 +9,3 @@ target/
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
Generated
+1452
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "iwaku"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "iwaku"
path = "src/main.rs"
[dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_Com",
"Win32_System_LibraryLoader",
"Win32_System_Ole",
"Win32_UI_Accessibility",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Magnification",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
tray-icon = "0.17"
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
strip = true
[profile.dev]
opt-level = 0
[profile.dev.package."*"]
opt-level = 3
+2
View File
@@ -2,3 +2,5 @@
What That Claude Do? (WTCD)
A repository of random things I ask Claude to do for me.
In this case it is creating a screenshot tool
+1
View File
@@ -0,0 +1 @@
/usr/bin/bash: line 1: del: command not found
+121
View File
@@ -0,0 +1,121 @@
use std::cell::RefCell;
use windows::Win32::Foundation::{HWND, POINT};
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::System::Com::SAFEARRAY;
use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER};
use windows::Win32::System::Ole::{SafeArrayGetElement, SafeArrayGetLBound, SafeArrayGetUBound};
use windows::Win32::UI::Accessibility::{
CUIAutomation, IUIAutomation, IUIAutomationElement, IUIAutomationTextPattern, UIA_TextPatternId,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetGUIThreadInfo, GetWindowThreadProcessId, GUITHREADINFO,
};
thread_local! {
static AUTOMATION: RefCell<Option<IUIAutomation>> = RefCell::new(None);
}
fn get_automation() -> Option<IUIAutomation> {
AUTOMATION.with(|cell| {
let mut borrow = cell.borrow_mut();
if borrow.is_none() {
unsafe {
*borrow = CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER).ok();
}
}
borrow.clone()
})
}
pub fn get_caret_pos() -> Option<POINT> {
// 1. Win32 GUI-thread caret — fast, no COM. Works for classic edit controls,
// Notepad, Office dialogs, conhost-based terminals, etc.
if let Some(pt) = get_caret_via_gui_thread() {
return Some(pt);
}
// 2. UI Automation — works for Word, modern browsers, WPF, UWP apps.
get_caret_via_uia()
}
fn get_caret_via_gui_thread() -> Option<POINT> {
unsafe {
let hwnd: HWND = GetForegroundWindow();
if hwnd.0.is_null() {
return None;
}
let thread_id = GetWindowThreadProcessId(hwnd, None);
let mut gti = GUITHREADINFO {
cbSize: std::mem::size_of::<GUITHREADINFO>() as u32,
..Default::default()
};
GetGUIThreadInfo(thread_id, &mut gti).ok()?;
if gti.hwndCaret.0.is_null() {
return None;
}
let mut pt = POINT {
x: gti.rcCaret.left,
y: gti.rcCaret.bottom,
};
let _ = ClientToScreen(gti.hwndCaret, &mut pt);
Some(pt)
}
}
fn get_caret_via_uia() -> Option<POINT> {
unsafe {
let automation = get_automation()?;
let element = automation.GetFocusedElement().ok()?;
// Try the focused element and walk up to 6 ancestors.
// Some apps expose TextPattern on a parent rather than the focused leaf.
let mut current: Option<IUIAutomationElement> = Some(element);
for _ in 0..7 {
let el = current.as_ref()?;
if let Some(sa) = try_text_pattern(el) {
return point_from_safearray(sa);
}
current = automation
.ControlViewWalker()
.ok()
.and_then(|w| w.GetParentElement(el).ok());
}
None
}
}
unsafe fn try_text_pattern(el: &IUIAutomationElement) -> Option<*mut SAFEARRAY> {
unsafe {
let pattern = el
.GetCurrentPatternAs::<IUIAutomationTextPattern>(UIA_TextPatternId)
.ok()?;
let ranges = pattern.GetSelection().ok()?;
if ranges.Length().ok()? == 0 {
return None;
}
ranges.GetElement(0).ok()?.GetBoundingRectangles().ok()
}
}
unsafe fn point_from_safearray(sa: *mut SAFEARRAY) -> Option<POINT> {
unsafe {
if sa.is_null() {
return None;
}
let lb = SafeArrayGetLBound(sa, 1).ok()?;
let ub = SafeArrayGetUBound(sa, 1).ok()?;
if ub - lb < 3 {
return None;
}
let mut left: f64 = 0.0;
let mut top: f64 = 0.0;
let mut idx0 = lb;
let mut idx1 = lb + 1;
SafeArrayGetElement(sa, &mut idx0, &mut left as *mut f64 as *mut _).ok()?;
SafeArrayGetElement(sa, &mut idx1, &mut top as *mut f64 as *mut _).ok()?;
Some(POINT {
x: left as i32,
y: top as i32,
})
}
}
+100
View File
@@ -0,0 +1,100 @@
use windows::Win32::Foundation::{LPARAM, LRESULT, WPARAM};
use windows::Win32::UI::Input::KeyboardAndMouse::{
GetAsyncKeyState, SendInput, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, VIRTUAL_KEY,
VK_F24, VK_LWIN, VK_RWIN,
};
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HHOOK, MSLLHOOKSTRUCT, WH_MOUSE_LL,
WM_MOUSEWHEEL,
};
use std::sync::atomic::{AtomicIsize, Ordering};
pub const WM_APP_ZOOM_IN: u32 = 0x8001;
pub const WM_APP_ZOOM_OUT: u32 = 0x8002;
static MOUSE_HOOK: AtomicIsize = AtomicIsize::new(0);
static MAIN_HWND: AtomicIsize = AtomicIsize::new(0);
pub fn install(main_hwnd: windows::Win32::Foundation::HWND) {
MAIN_HWND.store(main_hwnd.0 as isize, Ordering::SeqCst);
unsafe {
let mh = SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook_proc), None, 0)
.expect("Failed to install WH_MOUSE_LL hook");
MOUSE_HOOK.store(mh.0 as isize, Ordering::SeqCst);
}
}
pub fn uninstall() {
let raw = MOUSE_HOOK.swap(0, Ordering::SeqCst);
if raw != 0 {
unsafe {
let _ = UnhookWindowsHookEx(HHOOK(raw as *mut _));
}
}
}
unsafe extern "system" fn mouse_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
if code >= 0 && wparam.0 as u32 == WM_MOUSEWHEEL {
unsafe {
let lwin = GetAsyncKeyState(VK_LWIN.0 as i32);
let rwin = GetAsyncKeyState(VK_RWIN.0 as i32);
if (lwin < 0) || (rwin < 0) {
let info = &*(lparam.0 as *const MSLLHOOKSTRUCT);
let delta = (info.mouseData >> 16) as i16;
let hwnd_raw = MAIN_HWND.load(Ordering::SeqCst);
if hwnd_raw != 0 {
let hwnd = windows::Win32::Foundation::HWND(hwnd_raw as *mut _);
let msg = if delta > 0 {
WM_APP_ZOOM_IN
} else {
WM_APP_ZOOM_OUT
};
let _ = windows::Win32::UI::WindowsAndMessaging::PostMessageW(
hwnd,
msg,
WPARAM(0),
LPARAM(0),
);
}
// Inject a dummy F24 down+up between the Win key-down and the
// upcoming Win key-up. Windows only opens the Start menu when
// Win is the *sole* key in a press/release cycle; seeing any
// other key in between suppresses it — no stuck key, no Start
// menu, no keyboard hook required.
inject_f24();
return LRESULT(1); // consume the scroll event
}
}
}
unsafe { CallNextHookEx(None, code, wparam, lparam) }
}
fn inject_f24() {
let inputs = [make_key(VK_F24, false), make_key(VK_F24, true)];
unsafe {
SendInput(&inputs, std::mem::size_of::<INPUT>() as i32);
}
}
fn make_key(vk: VIRTUAL_KEY, key_up: bool) -> INPUT {
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 {
ki: KEYBDINPUT {
wVk: vk,
wScan: 0,
dwFlags: if key_up {
KEYEVENTF_KEYUP
} else {
Default::default()
},
time: 0,
dwExtraInfo: 0,
},
},
}
}
+76
View File
@@ -0,0 +1,76 @@
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Gdi::{
GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTOPRIMARY,
};
use windows::Win32::UI::Magnification::{
MagInitialize, MagSetFullscreenTransform, MagUninitialize,
};
use crate::state::AppState;
pub struct Magnifier {
screen_w: i32,
screen_h: i32,
}
impl Magnifier {
pub fn new(screen_w: i32, screen_h: i32) -> Self {
Self { screen_w, screen_h }
}
/// Apply or clear the fullscreen magnification transform.
pub fn update(&self, state: &mut AppState, just_activated: bool) {
unsafe {
if !state.active {
let _ = MagSetFullscreenTransform(1.0, 0, 0);
return;
}
let z = state.zoom;
let src_w = self.screen_w as f32 / z;
let src_h = self.screen_h as f32 / z;
state.update_viewport(
src_w,
src_h,
self.screen_w as f32,
self.screen_h as f32,
just_activated,
);
let x_off = state.viewport_x as i32;
let y_off = state.viewport_y as i32;
let _ = MagSetFullscreenTransform(z, x_off, y_off);
}
}
}
pub fn create() -> Magnifier {
unsafe {
MagInitialize().expect("MagInitialize failed");
let (sw, sh) = monitor_dimensions();
Magnifier::new(sw, sh)
}
}
pub fn shutdown() {
unsafe {
let _ = MagSetFullscreenTransform(1.0, 0, 0);
let _ = MagUninitialize();
}
}
fn monitor_dimensions() -> (i32, i32) {
unsafe {
let hmon = MonitorFromWindow(HWND(std::ptr::null_mut()), MONITOR_DEFAULTTOPRIMARY);
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
let _ = GetMonitorInfoW(hmon, &mut info);
let sw = info.rcMonitor.right - info.rcMonitor.left;
let sh = info.rcMonitor.bottom - info.rcMonitor.top;
(sw, sh)
}
}
+177
View File
@@ -0,0 +1,177 @@
#![windows_subsystem = "windows"]
mod caret;
mod hook;
mod magnifier;
mod state;
mod tray;
use state::AppState;
use windows::core::w;
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED};
use windows::Win32::UI::HiDpi::{
SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
};
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use windows::Win32::UI::WindowsAndMessaging::WINDOW_EX_STYLE;
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, PostQuitMessage,
RegisterClassExW, SetTimer, TranslateMessage, CS_HREDRAW, CS_VREDRAW, MSG, WM_DESTROY,
WM_TIMER, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
};
static mut APP_STATE: Option<AppState> = None;
static mut MAGNIFIER: Option<magnifier::Magnifier> = None;
const TIMER_ID: usize = 1;
const TIMER_MS: u32 = 16; // ~60 fps
fn main() {
unsafe {
// DPI awareness — must be set before any window is created.
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
// COM initialisation needed for UI Automation (caret tracking).
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
APP_STATE = Some(AppState::new());
let instance = get_instance();
register_msg_class(instance);
// Invisible message-only window to receive WM_APP zoom messages and WM_TIMER.
let msg_hwnd = CreateWindowExW(
WINDOW_EX_STYLE(0),
w!("IwakuMsg"),
w!(""),
WS_OVERLAPPEDWINDOW,
0,
0,
0,
0,
None,
None,
instance,
None,
)
.expect("Failed to create message window");
// Global Win+Scroll hook.
hook::install(msg_hwnd);
// Fullscreen magnifier — no overlay window needed with MagSetFullscreenTransform.
MAGNIFIER = Some(magnifier::create());
// System tray icon.
let tray = tray::Tray::new();
// 16 ms refresh timer.
SetTimer(msg_hwnd, TIMER_ID, TIMER_MS, None);
// Message loop.
let mut msg = MSG::default();
loop {
if tray.process_events() {
break;
}
let ret = GetMessageW(&mut msg, None, 0, 0);
if ret.0 == 0 || ret.0 == -1 {
break;
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
hook::uninstall();
magnifier::shutdown();
CoUninitialize();
}
}
/// Window procedure for the invisible message window.
unsafe extern "system" fn wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
match msg {
WM_TIMER => {
if wparam.0 == TIMER_ID {
tick();
}
LRESULT(0)
}
m if m == hook::WM_APP_ZOOM_IN => {
if let Some(state) = (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() {
state.zoom_in();
}
LRESULT(0)
}
m if m == hook::WM_APP_ZOOM_OUT => {
if let Some(state) = (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() {
state.zoom_out();
}
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
/// Called every ~16 ms: update inputs, recalculate priority, push to magnifier.
unsafe fn tick() {
unsafe {
let state = match (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() {
Some(s) => s,
None => return,
};
// Remember whether the magnifier was active before this tick so we
// can detect the exact frame it turns on and snap the smooth position.
let was_active = state.active;
// 1. Update cursor and caret.
let mut pt = windows::Win32::Foundation::POINT::default();
if GetCursorPos(&mut pt).is_ok() {
state.cursor = pt;
}
state.caret = caret::get_caret_pos();
// 2. Advance state.
let just_activated = state.active && !was_active;
state.update(just_activated);
// 3. Push the new transform.
if let Some(mag) = (*std::ptr::addr_of_mut!(MAGNIFIER)).as_mut() {
mag.update(state, just_activated);
}
}
}
unsafe fn get_instance() -> HINSTANCE {
unsafe {
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
GetModuleHandleW(None).unwrap().into()
}
}
unsafe fn register_msg_class(instance: HINSTANCE) {
unsafe {
let wc = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: instance,
lpszClassName: w!("IwakuMsg"),
..Default::default()
};
RegisterClassExW(&wc);
}
}
+192
View File
@@ -0,0 +1,192 @@
use windows::Win32::Foundation::POINT;
pub const ZOOM_MIN: f32 = 1.5;
pub const ZOOM_MAX: f32 = 10.0;
pub const ZOOM_STEP: f32 = 0.5;
const EDGE_MARGIN: f32 = 0.10;
const PAN_ALPHA: f32 = 0.20;
const ZOOM_ALPHA: f32 = 0.18;
const ZOOM_EPSILON: f32 = 0.01;
const CURSOR_ALPHA: f32 = 0.35;
const MOUSE_MOVE_THRESHOLD_SQ: i32 = 6 * 6;
const MOUSE_TAKEOVER_TICKS: u32 = 4;
/// Ticks of mouse stillness before caret re-takes focus (if caret present). ~240 ms.
const CARET_REFOCUS_TICKS: u32 = 15;
pub struct AppState {
pub zoom: f32,
target_zoom: f32,
prev_zoom: f32,
pub active: bool,
pub cursor: POINT,
prev_cursor: POINT,
smooth_cursor_x: f32,
smooth_cursor_y: f32,
pub caret: Option<POINT>,
prev_caret: Option<POINT>,
pub caret_active: bool,
mouse_move_ticks: u32,
mouse_still_ticks: u32,
pub viewport_x: f32,
pub viewport_y: f32,
}
impl AppState {
pub fn new() -> Self {
Self {
zoom: 1.0,
target_zoom: 1.0,
prev_zoom: 1.0,
active: false,
cursor: POINT { x: 0, y: 0 },
prev_cursor: POINT { x: 0, y: 0 },
smooth_cursor_x: 0.0,
smooth_cursor_y: 0.0,
caret: None,
prev_caret: None,
caret_active: false,
mouse_move_ticks: 0,
mouse_still_ticks: 0,
viewport_x: 0.0,
viewport_y: 0.0,
}
}
pub fn update(&mut self, _just_activated: bool) {
// Animate zoom.
if self.active {
let diff = self.target_zoom - self.zoom;
if diff.abs() < ZOOM_EPSILON {
self.zoom = self.target_zoom;
if self.zoom <= 1.0 {
self.zoom = 1.0;
self.active = false;
}
} else {
self.zoom += diff * ZOOM_ALPHA;
}
}
// Smooth cursor.
self.smooth_cursor_x += (self.cursor.x as f32 - self.smooth_cursor_x) * CURSOR_ALPHA;
self.smooth_cursor_y += (self.cursor.y as f32 - self.smooth_cursor_y) * CURSOR_ALPHA;
// Caret / mouse priority.
let dx = self.cursor.x - self.prev_cursor.x;
let dy = self.cursor.y - self.prev_cursor.y;
let intentional = dx * dx + dy * dy >= MOUSE_MOVE_THRESHOLD_SQ;
let caret_moved = match (self.caret, self.prev_caret) {
(Some(c), Some(p)) => c.x != p.x || c.y != p.y,
(Some(_), None) => true,
_ => false,
};
if intentional {
self.mouse_move_ticks += 1;
self.mouse_still_ticks = 0;
} else {
self.mouse_move_ticks = 0;
self.mouse_still_ticks += 1;
}
if self.mouse_move_ticks >= MOUSE_TAKEOVER_TICKS {
self.caret_active = false;
self.mouse_move_ticks = 0;
}
// Re-engage caret once mouse has been still long enough.
if self.mouse_still_ticks >= CARET_REFOCUS_TICKS && self.caret.is_some() {
self.caret_active = true;
}
if caret_moved {
self.caret_active = true;
self.mouse_move_ticks = 0;
}
self.prev_cursor = self.cursor;
self.prev_caret = self.caret;
}
pub fn update_viewport(
&mut self,
src_w: f32,
src_h: f32,
screen_w: f32,
screen_h: f32,
just_activated: bool,
) {
let (fx, fy) = self.focus_point();
let zoom_changed = (self.zoom - self.prev_zoom).abs() > f32::EPSILON;
self.prev_zoom = self.zoom;
if just_activated {
self.viewport_x = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0));
self.viewport_y = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0));
return;
}
let (target_x, target_y) = if self.caret_active {
// Centre on caret while typing.
let tx = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0));
let ty = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0));
(tx, ty)
} else if zoom_changed {
let tx = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0));
let ty = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0));
(tx, ty)
} else {
let mx = EDGE_MARGIN * src_w;
let my = EDGE_MARGIN * src_h;
let tx = self
.viewport_x
.clamp(fx - src_w + mx, fx - mx)
.clamp(0.0, (screen_w - src_w).max(0.0));
let ty = self
.viewport_y
.clamp(fy - src_h + my, fy - my)
.clamp(0.0, (screen_h - src_h).max(0.0));
(tx, ty)
};
self.viewport_x += (target_x - self.viewport_x) * PAN_ALPHA;
self.viewport_y += (target_y - self.viewport_y) * PAN_ALPHA;
// Hard-clamp every tick so the viewport never overshoots screen bounds
// even while the lerp is still catching up (e.g. fast zoom-out).
self.viewport_x = self.viewport_x.clamp(0.0, (screen_w - src_w).max(0.0));
self.viewport_y = self.viewport_y.clamp(0.0, (screen_h - src_h).max(0.0));
}
pub fn zoom_in(&mut self) {
if !self.active {
self.target_zoom = ZOOM_MIN;
self.zoom = 1.0;
self.active = true;
} else {
self.target_zoom = (self.target_zoom + ZOOM_STEP).min(ZOOM_MAX);
}
}
pub fn zoom_out(&mut self) {
if !self.active {
return;
}
self.target_zoom -= ZOOM_STEP;
if self.target_zoom < ZOOM_MIN {
self.target_zoom = 1.0;
}
}
fn focus_point(&self) -> (f32, f32) {
if self.caret_active {
if let Some(c) = self.caret {
return (c.x as f32, c.y as f32);
}
}
(self.smooth_cursor_x, self.smooth_cursor_y)
}
}
+70
View File
@@ -0,0 +1,70 @@
use tray_icon::{
menu::{Menu, MenuEvent, MenuId, MenuItem},
TrayIcon, TrayIconBuilder,
};
pub struct Tray {
_icon: TrayIcon,
exit_id: MenuId,
}
impl Tray {
pub fn new() -> Self {
let toggle_item = MenuItem::new("Toggle", true, None);
let exit_item = MenuItem::new("Exit", true, None);
let exit_id = exit_item.id().clone();
let menu = Menu::new();
menu.append(&toggle_item)
.expect("Failed to append toggle item");
menu.append(&exit_item).expect("Failed to append exit item");
let icon = make_icon();
let tray = TrayIconBuilder::new()
.with_menu(Box::new(menu))
.with_tooltip("iwaku magnifier")
.with_icon(icon)
.build()
.expect("Failed to build tray icon");
Self {
_icon: tray,
exit_id,
}
}
/// Returns `true` if the app should exit.
pub fn process_events(&self) -> bool {
if let Ok(event) = MenuEvent::receiver().try_recv() {
if event.id == self.exit_id {
return true;
}
}
false
}
}
fn make_icon() -> tray_icon::Icon {
const SIZE: usize = 16;
let mut rgba = vec![0u8; SIZE * SIZE * 4];
for y in 0..SIZE {
for x in 0..SIZE {
let idx = (y * SIZE + x) * 4;
let cx = x as f32 - 7.5;
let cy = y as f32 - 7.5;
let dist = (cx * cx + cy * cy).sqrt();
if (dist - 5.5).abs() < 1.5 {
rgba[idx] = 255;
rgba[idx + 1] = 255;
rgba[idx + 2] = 255;
rgba[idx + 3] = 255;
}
}
}
tray_icon::Icon::from_rgba(rgba, SIZE as u32, SIZE as u32)
.expect("Failed to create tray icon from RGBA")
}