mod geometry;
use std::ptr::{null, null_mut};
use std::sync::mpsc as sync;
use std::thread;
use std::time::{Duration, Instant};
use a7n_proto::v1::Edge;
use tokio::sync::mpsc;
use windows_sys::Win32::Foundation::{COLORREF, HINSTANCE, HWND, LPARAM, POINT};
use windows_sys::Win32::Graphics::Gdi::{
CombineRgn, CreateEllipticRgn, CreateSolidBrush, DeleteObject, GetMonitorInfoW,
MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromPoint, RGN_DIFF, SetWindowRgn,
};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
use windows_sys::Win32::UI::HiDpi::{
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, GetDpiForMonitor, MDT_EFFECTIVE_DPI,
SetThreadDpiAwarenessContext,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DispatchMessageW, GetCursorPos, GetMessageW, HWND_TOPMOST,
KillTimer, LWA_ALPHA, MSG, PostThreadMessageW, RegisterClassW, SPI_GETCLIENTAREAANIMATION,
SW_HIDE, SW_SHOWNOACTIVATE, SWP_NOACTIVATE, SetLayeredWindowAttributes, SetTimer,
SetWindowDisplayAffinity, SetWindowPos, ShowWindow, SystemParametersInfoW,
WDA_EXCLUDEFROMCAPTURE, WM_APP, WM_QUIT, WM_TIMER, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE,
WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_POPUP,
};
use windows_sys::core::{BOOL, PCWSTR, w};
use self::geometry::{Rect, glow_alpha, glow_rect, landing_alpha};
use super::{Command, Feedback};
const WM_FEEDBACK: u32 = WM_APP;
const CLASS_NAME: PCWSTR = w!("ApeirogonFeedback");
const ACCENT: COLORREF = 0x00EA_5B2F;
const BASE_DPI: u32 = 96;
const GLOW_THICKNESS_DIP: i32 = 6;
const LANDING_DIAMETER_DIP: i32 = 56;
const LANDING_RING_DIP: i32 = 5;
const FADE_TIMER_ID: usize = 1;
const FADE_FRAME_MS: u32 = 16;
pub(super) fn start() -> Option<Feedback> {
let (feedback, commands) = super::channel();
let (ready_tx, ready_rx) = sync::channel();
thread::Builder::new()
.name("a7n-windows-feedback".into())
.spawn(move || run_ui_thread(&ready_tx))
.ok()?;
let ui_thread = ready_rx.recv().ok()??;
let relay = thread::Builder::new()
.name("a7n-windows-feedback-relay".into())
.spawn(move || relay_commands(commands, ui_thread));
if relay.is_err() {
post_quit(ui_thread);
return None;
}
Some(feedback)
}
fn relay_commands(mut commands: mpsc::Receiver<Command>, ui_thread: u32) {
while let Some(command) = commands.blocking_recv() {
let raw = Box::into_raw(Box::new(command));
if unsafe { PostThreadMessageW(ui_thread, WM_FEEDBACK, 0, raw as LPARAM) } == 0 {
drop(unsafe { Box::from_raw(raw) });
}
}
post_quit(ui_thread);
}
fn post_quit(ui_thread: u32) {
unsafe { PostThreadMessageW(ui_thread, WM_QUIT, 0, 0) };
}
fn run_ui_thread(ready: &sync::Sender<Option<u32>>) {
unsafe { SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) };
let Some(mut ui) = Ui::new() else {
let _ = ready.send(None);
return;
};
let thread_id = unsafe { GetCurrentThreadId() };
if ready.send(Some(thread_id)).is_ok() {
ui.run();
}
}
struct Ui {
glow: HWND,
landing: HWND,
landing_started: Instant,
animate: bool,
}
impl Ui {
fn new() -> Option<Self> {
let module = unsafe { GetModuleHandleW(null()) };
register_class(module);
Some(Self {
glow: create_overlay(module)?,
landing: create_overlay(module)?,
landing_started: Instant::now(),
animate: true,
})
}
fn run(&mut self) {
let mut message = MSG::default();
while unsafe { GetMessageW(&raw mut message, null_mut(), 0, 0) } > 0 {
if message.hwnd.is_null() && message.message == WM_FEEDBACK {
let command = *unsafe { Box::from_raw(message.lParam as *mut Command) };
self.handle(command);
} else if message.hwnd == self.landing
&& message.message == WM_TIMER
&& message.wParam == FADE_TIMER_ID
{
self.fade_landing();
} else {
unsafe { DispatchMessageW(&raw const message) };
}
}
}
fn handle(&mut self, command: Command) {
match command {
Command::Edge {
display,
edge,
progress,
} => self.show_glow(Rect::from_bounds(display), edge, progress),
Command::Clear => hide(self.glow),
Command::Landing { x, y } => self.show_landing((x, y)),
Command::Tick => {}
}
}
fn show_glow(&self, display: Rect, edge: Edge, progress: u16) {
let pointer = cursor_position().unwrap_or_else(|| center(display));
let monitor = monitor_at(pointer);
let area = monitor
.and_then(|monitor| monitor.rect.intersect(display))
.unwrap_or(display);
let thickness = scale_dip(GLOW_THICKNESS_DIP, monitor.map_or(BASE_DPI, |m| m.dpi));
match glow_rect(area, edge, progress, thickness, pointer) {
Some(rect) => show(self.glow, rect, glow_alpha(progress)),
None => hide(self.glow),
}
}
fn show_landing(&mut self, point: (i32, i32)) {
let dpi = monitor_at(point).map_or(BASE_DPI, |monitor| monitor.dpi);
let diameter = scale_dip(LANDING_DIAMETER_DIP, dpi);
let rect = Rect {
left: point.0 - diameter / 2,
top: point.1 - diameter / 2,
right: point.0 - diameter / 2 + diameter,
bottom: point.1 - diameter / 2 + diameter,
};
set_ring_shape(self.landing, diameter, scale_dip(LANDING_RING_DIP, dpi));
self.animate = animations_enabled();
self.landing_started = Instant::now();
show(
self.landing,
rect,
landing_alpha(Duration::ZERO, self.animate),
);
unsafe { SetTimer(self.landing, FADE_TIMER_ID, FADE_FRAME_MS, None) };
}
fn fade_landing(&self) {
let alpha = landing_alpha(self.landing_started.elapsed(), self.animate);
if alpha == 0 {
unsafe { KillTimer(self.landing, FADE_TIMER_ID) };
hide(self.landing);
return;
}
set_alpha(self.landing, alpha);
}
}
fn register_class(module: HINSTANCE) {
let accent = unsafe { CreateSolidBrush(ACCENT) };
let class = WNDCLASSW {
lpfnWndProc: Some(DefWindowProcW),
hInstance: module,
lpszClassName: CLASS_NAME,
hbrBackground: accent,
..WNDCLASSW::default()
};
unsafe { RegisterClassW(&raw const class) };
}
fn create_overlay(module: HINSTANCE) -> Option<HWND> {
let style =
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST;
let hwnd = unsafe {
CreateWindowExW(
style,
CLASS_NAME,
w!(""),
WS_POPUP,
0,
0,
0,
0,
null_mut(),
null_mut(),
module,
null(),
)
};
if hwnd.is_null() {
return None;
}
set_alpha(hwnd, 0);
unsafe { SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) };
Some(hwnd)
}
fn show(hwnd: HWND, rect: Rect, alpha: u8) {
unsafe {
SetWindowPos(
hwnd,
HWND_TOPMOST,
rect.left,
rect.top,
rect.width(),
rect.height(),
SWP_NOACTIVATE,
);
}
set_alpha(hwnd, alpha);
unsafe { ShowWindow(hwnd, SW_SHOWNOACTIVATE) };
}
fn hide(hwnd: HWND) {
unsafe { ShowWindow(hwnd, SW_HIDE) };
}
fn set_alpha(hwnd: HWND, alpha: u8) {
unsafe { SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA) };
}
fn set_ring_shape(hwnd: HWND, diameter: i32, ring: i32) {
unsafe {
let ring_region = CreateEllipticRgn(0, 0, diameter, diameter);
let hole = CreateEllipticRgn(ring, ring, diameter - ring, diameter - ring);
CombineRgn(ring_region, ring_region, hole, RGN_DIFF);
DeleteObject(hole);
if SetWindowRgn(hwnd, ring_region, 0) == 0 {
DeleteObject(ring_region);
}
}
}
#[derive(Clone, Copy)]
struct MonitorGeometry {
rect: Rect,
dpi: u32,
}
fn monitor_at(point: (i32, i32)) -> Option<MonitorGeometry> {
let monitor = unsafe {
MonitorFromPoint(
POINT {
x: point.0,
y: point.1,
},
MONITOR_DEFAULTTONEAREST,
)
};
let mut info = MONITORINFO {
cbSize: u32::try_from(size_of::<MONITORINFO>()).ok()?,
..MONITORINFO::default()
};
if unsafe { GetMonitorInfoW(monitor, &raw mut info) } == 0 {
return None;
}
let (mut dpi_x, mut dpi_y) = (BASE_DPI, BASE_DPI);
unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &raw mut dpi_x, &raw mut dpi_y) };
Some(MonitorGeometry {
rect: Rect {
left: info.rcMonitor.left,
top: info.rcMonitor.top,
right: info.rcMonitor.right,
bottom: info.rcMonitor.bottom,
},
dpi: dpi_x,
})
}
fn cursor_position() -> Option<(i32, i32)> {
let mut point = POINT::default();
(unsafe { GetCursorPos(&raw mut point) } != 0).then_some((point.x, point.y))
}
fn center(area: Rect) -> (i32, i32) {
(area.left + area.width() / 2, area.top + area.height() / 2)
}
fn scale_dip(dip: i32, dpi: u32) -> i32 {
dip * i32::try_from(dpi).unwrap_or(i32::MAX) / i32::try_from(BASE_DPI).unwrap_or(i32::MAX)
}
fn animations_enabled() -> bool {
let mut enabled: BOOL = 1;
let read = unsafe {
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, (&raw mut enabled).cast(), 0)
};
read == 0 || enabled != 0
}