From 131502f1f8634d96722021188054353e61bcac50 Mon Sep 17 00:00:00 2001 From: sigoden Date: Wed, 23 Oct 2024 07:23:35 +0800 Subject: [PATCH] feat: add rounded corners to match Windows 11 design (#141) * refactor: add rounded corner to UI * switch to GDI+ to avoid jagged edge on rounded corner * anti-aliasing app icons * add rounded corner only in win11 * split is_light_theme to utils * replace old painter --- Cargo.toml | 2 + src/app.rs | 132 ++------ src/painter.rs | 583 +++++++++++++++++++++++------------ src/utils/mod.rs | 4 + src/utils/window.rs | 30 +- src/utils/windows_theme.rs | 13 + src/utils/windows_version.rs | 26 ++ 7 files changed, 484 insertions(+), 306 deletions(-) create mode 100644 src/utils/windows_theme.rs create mode 100644 src/utils/windows_version.rs diff --git a/Cargo.toml b/Cargo.toml index 1b1a0dc..08cfaa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ features = [ "Win32_UI_Accessibility", "Win32_Graphics_Dwm", "Win32_Graphics_Gdi", + "Win32_Graphics_GdiPlus", "Win32_Security", "Win32_Security_Authorization", "Win32_System_Console", @@ -31,6 +32,7 @@ features = [ "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_Storage_FileSystem", + "Wdk_System_SystemServices", ] [build-dependencies] diff --git a/src/app.rs b/src/app.rs index c128445..0a7f677 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,7 @@ use crate::config::{edit_config_file, Config}; use crate::foreground::ForegroundWatcher; use crate::keyboard::KeyboardListener; +use crate::painter::{find_clicked_app_index, GdiAAPainter}; use crate::startup::Startup; use crate::trayicon::TrayIcon; use crate::utils::{ @@ -9,28 +10,19 @@ use crate::utils::{ is_running_as_admin, list_windows, set_foreground_window, set_window_user_data, }; -use crate::painter::{GdiAAPainter, ICON_BORDER_SIZE, WINDOW_BORDER_SIZE}; use anyhow::{anyhow, Result}; use indexmap::IndexSet; use std::collections::HashMap; use windows::core::w; use windows::core::PCWSTR; -use windows::Win32::Foundation::{ - GetLastError, HINSTANCE, HMODULE, HWND, LPARAM, LRESULT, POINT, WPARAM, -}; -use windows::Win32::Graphics::Gdi::{ - GetMonitorInfoW, MonitorFromPoint, RedrawWindow, HRGN, MONITORINFO, MONITOR_DEFAULTTONEAREST, - RDW_ERASE, RDW_INVALIDATE, -}; +use windows::Win32::Foundation::{GetLastError, HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}; use windows::Win32::System::LibraryLoader::GetModuleHandleW; -use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus; use windows::Win32::UI::WindowsAndMessaging::{ - CreateWindowExW, DefWindowProcW, DestroyIcon, DispatchMessageW, GetCursorPos, GetMessageW, - GetWindowLongPtrW, LoadCursorW, PostMessageW, PostQuitMessage, RegisterClassW, - RegisterWindowMessageW, SetCursor, SetWindowLongPtrW, SetWindowPos, ShowWindow, - TranslateMessage, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, GWL_STYLE, HICON, HWND_TOPMOST, - IDC_ARROW, MSG, SWP_SHOWWINDOW, SW_HIDE, WINDOW_STYLE, WM_COMMAND, WM_ERASEBKGND, WM_LBUTTONUP, - WM_PAINT, WM_RBUTTONUP, WNDCLASSW, WS_CAPTION, WS_EX_TOOLWINDOW, + CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, GetWindowLongPtrW, LoadCursorW, + PostMessageW, PostQuitMessage, RegisterClassW, RegisterWindowMessageW, SetWindowLongPtrW, + TranslateMessage, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, GWL_STYLE, HICON, HTCLIENT, IDC_ARROW, + MSG, WINDOW_STYLE, WM_COMMAND, WM_ERASEBKGND, WM_LBUTTONUP, WM_NCHITTEST, WM_RBUTTONUP, + WNDCLASSW, WS_CAPTION, WS_EX_LAYERED, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, }; pub const NAME: PCWSTR = w!("Window Switcher"); @@ -67,7 +59,7 @@ pub struct App { impl App { pub fn start(config: &Config) -> Result<()> { let hwnd = Self::create_window()?; - let painter = GdiAAPainter::new(hwnd, 6); + let painter = GdiAAPainter::new(hwnd)?; let _foreground_watcher = ForegroundWatcher::init(&config.switch_windows_blacklist)?; let _keyboard_listener = KeyboardListener::init(hwnd, &config.to_hotkeys())?; @@ -130,7 +122,11 @@ impl App { let hinstance = unsafe { GetModuleHandleW(None) } .map_err(|err| anyhow!("Failed to get current module handle, {err}"))?; + let hcursor = unsafe { LoadCursorW(None, IDC_ARROW) } + .map_err(|err| anyhow!("Failed to load arrow cursor, {err}"))?; + let window_class = WNDCLASSW { + hCursor: hcursor, hInstance: HINSTANCE(hinstance.0), lpszClassName: NAME, style: CS_HREDRAW | CS_VREDRAW, @@ -143,7 +139,7 @@ impl App { let hwnd = unsafe { CreateWindowExW( - WS_EX_TOOLWINDOW, + WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW, PCWSTR(atom as _), NAME, WINDOW_STYLE(0), @@ -224,9 +220,6 @@ impl App { let app = get_app(hwnd)?; let reverse = lparam.0 == 1; app.switch_apps(reverse)?; - unsafe { - let _ = RedrawWindow(hwnd, None, HRGN::default(), RDW_ERASE | RDW_INVALIDATE); - } if let Some(state) = &app.switch_apps_state { app.painter.paint(state); } @@ -257,11 +250,12 @@ impl App { let app = get_app(hwnd)?; app.switch_windows_state.modifier_released = true; } + WM_NCHITTEST => { + return Ok(LRESULT(HTCLIENT as _)); + } WM_LBUTTONUP => { let app = get_app(hwnd)?; - let xpos = ((lparam.0 as usize) & 0xFFFF) as u16 as i32; - let ypos = (((lparam.0 as usize) & 0xFFFF_0000) >> 16) as u16 as i32; - app.click(xpos, ypos)?; + app.click(); } WM_COMMAND => { let value = wparam.0 as u32; @@ -291,10 +285,6 @@ impl App { WM_ERASEBKGND => { return Ok(LRESULT(0)); } - WM_PAINT => { - let app = get_app(hwnd)?; - app.paint()?; - } _ if msg == WM_USER_REGISTER_TRAYICON || unsafe { msg == WM_TASKBARCREATED } => { let app = get_app(hwnd)?; app.set_trayicon(); @@ -403,7 +393,6 @@ impl App { debug!("switch apps: new index:{}", state.index); return Ok(()); } - let hwnd = self.hwnd; let windows = list_windows(self.config.switch_apps_ignore_minimal)?; let mut apps = vec![]; for (module_path, hwnds) in windows.iter() { @@ -431,44 +420,6 @@ impl App { if num_apps == 0 { return Ok(()); } - let mut mi = MONITORINFO { - cbSize: std::mem::size_of::() as u32, - ..MONITORINFO::default() - }; - unsafe { - let mut cursor = POINT::default(); - let _ = GetCursorPos(&mut cursor); - - let hmonitor = MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST); - let _ = GetMonitorInfoW(hmonitor, &mut mi); - } - - let monitor_rect = mi.rcMonitor; - let monitor_width = monitor_rect.right - monitor_rect.left; - let monitor_height = monitor_rect.bottom - monitor_rect.top; - let (icon_size, window_width, window_height) = - self.painter.init(monitor_width, apps.len() as i32); - - // Calculate the position to center the window - let x = monitor_rect.left + (monitor_width - window_width) / 2; - let y = monitor_rect.top + (monitor_height - window_height) / 2; - - unsafe { - // Change busy cursor to array cursor - if let Ok(hcursor) = LoadCursorW(HMODULE::default(), IDC_ARROW) { - SetCursor(hcursor); - } - let _ = SetFocus(hwnd); - let _ = SetWindowPos( - hwnd, - HWND_TOPMOST, - x, - y, - window_width, - window_height, - SWP_SHOWWINDOW, - ); - } let index = if apps.len() == 1 { 0 @@ -478,65 +429,33 @@ impl App { 1 }; - self.switch_apps_state = Some(SwitchAppsState { - apps, - index, - icon_size, - }); + let state = SwitchAppsState { apps, index }; + self.switch_apps_state = Some(state); debug!("switch apps, new state:{:?}", self.switch_apps_state); Ok(()) } - fn paint(&mut self) -> Result<()> { - self.painter.display(); - Ok(()) - } - - fn click(&mut self, xpos: i32, ypos: i32) -> Result<()> { + fn click(&mut self) { if let Some(state) = self.switch_apps_state.as_mut() { - let cy = WINDOW_BORDER_SIZE + ICON_BORDER_SIZE; - let item_size = state.icon_size + 2 * ICON_BORDER_SIZE; - for (i, (_, _)) in state.apps.iter().enumerate() { - let cx = WINDOW_BORDER_SIZE + item_size * (i as i32) + ICON_BORDER_SIZE; - if xpos >= cx - && xpos <= cx + state.icon_size - && ypos >= cy - && ypos <= cy + state.icon_size - { - state.index = i; - self.do_switch_app(); - break; - } + if let Some(i) = find_clicked_app_index(state) { + state.index = i; + self.do_switch_app(); } } - - Ok(()) } fn do_switch_app(&mut self) { - let hwnd = self.hwnd; if let Some(state) = self.switch_apps_state.take() { if let Some((_, id)) = state.apps.get(state.index) { set_foreground_window(*id); } - for (hicon, _) in state.apps { - let _ = unsafe { DestroyIcon(hicon) }; - } - unsafe { - let _ = ShowWindow(hwnd, SW_HIDE); - } + self.painter.unpaint(state); } } fn cancel_switch_app(&mut self) { - let hwnd = self.hwnd; if let Some(state) = self.switch_apps_state.take() { - for (hicon, _) in state.apps { - let _ = unsafe { DestroyIcon(hicon) }; - } - unsafe { - let _ = ShowWindow(hwnd, SW_HIDE); - } + self.painter.unpaint(state); } } } @@ -560,5 +479,4 @@ struct SwitchWindowsState { pub struct SwitchAppsState { pub apps: Vec<(HICON, HWND)>, pub index: usize, - pub icon_size: i32, } diff --git a/src/painter.rs b/src/painter.rs index 5af3381..bfef34b 100644 --- a/src/painter.rs +++ b/src/painter.rs @@ -1,236 +1,433 @@ use crate::app::SwitchAppsState; -use crate::utils::RegKey; -use anyhow::Result; -use windows::core::w; -use windows::Win32::Foundation::{COLORREF, RECT}; +use crate::utils::{check_error, get_moinitor_rect, is_light_theme, is_win11}; + +use anyhow::{Context, Result}; +use windows::Win32::Foundation::{COLORREF, POINT, RECT, SIZE}; use windows::Win32::Graphics::Gdi::{ - BeginPaint, BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, CreateSolidBrush, DeleteDC, - DeleteObject, EndPaint, FillRect, SelectObject, SetStretchBltMode, StretchBlt, HALFTONE, - HBITMAP, HBRUSH, HDC, PAINTSTRUCT, SRCCOPY, + CreateCompatibleBitmap, CreateCompatibleDC, CreateRoundRectRgn, CreateSolidBrush, DeleteDC, + DeleteObject, FillRect, FillRgn, ReleaseDC, SelectObject, SetStretchBltMode, StretchBlt, + AC_SRC_ALPHA, AC_SRC_OVER, BLENDFUNCTION, HALFTONE, HBITMAP, HBRUSH, HDC, HPALETTE, SRCCOPY, +}; +use windows::Win32::Graphics::GdiPlus::{ + FillModeAlternate, GdipAddPathArc, GdipClosePathFigure, GdipCreateBitmapFromHBITMAP, + GdipCreateFromHDC, GdipCreatePath, GdipCreatePen1, GdipDeleteBrush, GdipDeleteGraphics, + GdipDeletePath, GdipDeletePen, GdipDisposeImage, GdipDrawImageRect, GdipFillPath, + GdipFillRectangle, GdipGetPenBrushFill, GdipSetInterpolationMode, GdipSetSmoothingMode, + GdiplusShutdown, GdiplusStartup, GdiplusStartupInput, GpBitmap, GpBrush, GpGraphics, GpImage, + GpPath, GpPen, InterpolationModeHighQualityBicubic, SmoothingModeAntiAlias, Unit, +}; +use windows::Win32::UI::Input::KeyboardAndMouse::SetFocus; +use windows::Win32::UI::WindowsAndMessaging::{ + DestroyIcon, DrawIconEx, GetCursorPos, ShowWindow, UpdateLayeredWindow, DI_NORMAL, SW_HIDE, + SW_SHOW, ULW_ALPHA, }; -use windows::Win32::UI::WindowsAndMessaging::{DrawIconEx, DI_NORMAL}; use windows::Win32::{Foundation::HWND, Graphics::Gdi::GetDC}; -// window background color in dark theme -pub const BG_DARK_COLOR: COLORREF = COLORREF(0x3b3b3b); -// selected icon box color in dark theme -pub const FG_DARK_COLOR: COLORREF = COLORREF(0x4c4c4c); -// window background color in light theme -pub const BG_LIGHT_COLOR: COLORREF = COLORREF(0xf2f2f2); -// selected icon box color in light theme -pub const FG_LIGHT_COLOR: COLORREF = COLORREF(0xe0e0e0); -// minimum icon size +pub const BG_DARK_COLOR: u32 = 0x4c4c4c; +pub const FG_DARK_COLOR: u32 = 0x3b3b3b; +pub const BG_LIGHT_COLOR: u32 = 0xe0e0e0; +pub const FG_LIGHT_COLOR: u32 = 0xf2f2f2; +pub const ALPHA_MASK: u32 = 0xff000000; pub const ICON_SIZE: i32 = 64; -// window padding pub const WINDOW_BORDER_SIZE: i32 = 10; -// icon border pub const ICON_BORDER_SIZE: i32 = 4; +pub const SCALE_FACTOR: i32 = 6; // GDI Antialiasing Painter pub struct GdiAAPainter { - // memory - mem_hdc: HDC, - mem_map: HBITMAP, - // scaled - scaled_hdc: HDC, - scaled_map: HBITMAP, - // windows handle + token: usize, hwnd: HWND, - // content size - width: i32, - height: i32, - size: i32, - // scale - scale: i32, - // color - fg_color: COLORREF, - bg_color: COLORREF, + hdc_screen: HDC, + rounded_corner: bool, + light: bool, + show: bool, } impl GdiAAPainter { - /// Creates a new [GdiAAPainter] instance. - /// - /// The `scale` must be a multiple of 2, for example 2, 4, 6, 8, 12 ... - pub fn new(hwnd: HWND, scale: i32) -> Self { - let light_theme = match is_light_theme() { - Ok(v) => v, - Err(_) => { - warn!("Fail to get system theme"); - false - } - }; - let (fg_color, bg_color) = match light_theme { - true => (FG_LIGHT_COLOR, BG_LIGHT_COLOR), - false => (FG_DARK_COLOR, BG_DARK_COLOR), + pub fn new(hwnd: HWND) -> Result { + let startup_input = GdiplusStartupInput { + GdiplusVersion: 1, + ..Default::default() }; - GdiAAPainter { - mem_hdc: Default::default(), - mem_map: Default::default(), - scaled_hdc: Default::default(), - scaled_map: Default::default(), + let mut token: usize = 0; + check_error(|| unsafe { GdiplusStartup(&mut token, &startup_input, std::ptr::null_mut()) }) + .context("Failed to initialize GDI+")?; + + let hdc_screen = unsafe { GetDC(hwnd) }; + let rounded_corner = is_win11(); + let light = is_light_theme(); + + Ok(Self { + token, hwnd, - width: 0, - height: 0, - size: 0, - scale, - fg_color, - bg_color, - } + hdc_screen, + rounded_corner, + light, + show: false, + }) } - /// Initial this painter. - /// - /// Returns (icon_size, width, height) - pub fn init(&mut self, monitor_width: i32, num_apps: i32) -> (i32, i32, i32) { - let icon_size = ((monitor_width - 2 * WINDOW_BORDER_SIZE) / num_apps - - ICON_BORDER_SIZE * 2) - .min(ICON_SIZE); + pub fn paint(&mut self, state: &SwitchAppsState) { + let Coordinate { + x, + y, + width, + height, + icon_size, + item_size, + } = Coordinate::new(state.apps.len() as i32); - let item_size = icon_size + ICON_BORDER_SIZE * 2; - let width = item_size * num_apps + WINDOW_BORDER_SIZE * 2; - let height = item_size + WINDOW_BORDER_SIZE * 2; - let size = width * height; - if size == self.size { - return (icon_size, width, height); - } + let corner_radius = if self.rounded_corner { + item_size / 4 + } else { + 0 + }; + + let hwnd = self.hwnd; + let hdc_screen = self.hdc_screen; + + let (fg_color, bg_color) = theme_color(self.light); unsafe { - self.width = width; - self.height = height; - self.size = size; - - let _ = DeleteDC(self.mem_hdc); - let _ = DeleteObject(self.mem_map); - let _ = DeleteDC(self.scaled_hdc); - let _ = DeleteObject(self.scaled_map); - - let hdc = GetDC(self.hwnd); - let mem_dc = CreateCompatibleDC(hdc); - let mem_map = CreateCompatibleBitmap(hdc, width, height); - SelectObject(mem_dc, mem_map); - - let brush = CreateSolidBrush(self.fg_color); - let rect = RECT { - left: 0, - top: 0, - right: width, - bottom: height, - }; - FillRect(mem_dc, &rect as _, brush); - - let scaled_dc = CreateCompatibleDC(hdc); - let scaled_map = CreateCompatibleBitmap(hdc, width * self.scale, height * self.scale); - SelectObject(scaled_dc, scaled_map); - let rect = RECT { - left: 0, - top: 0, - right: width * self.scale, - bottom: height * self.scale, + let hdc_mem = CreateCompatibleDC(hdc_screen); + let bitmap_mem = CreateCompatibleBitmap(hdc_screen, width, height); + SelectObject(hdc_mem, bitmap_mem); + + let mut graphics = GpGraphics::default(); + let mut graphics_ptr: *mut GpGraphics = &mut graphics; + GdipCreateFromHDC(hdc_mem, &mut graphics_ptr as _); + GdipSetSmoothingMode(graphics_ptr, SmoothingModeAntiAlias); + GdipSetInterpolationMode(graphics_ptr, InterpolationModeHighQualityBicubic); + + let mut bg_pen = GpPen::default(); + let mut bg_pen_ptr: *mut GpPen = &mut bg_pen; + GdipCreatePen1(ALPHA_MASK | bg_color, 0.0, Unit(0), &mut bg_pen_ptr as _); + + let mut bg_brush = GpBrush::default(); + let mut bg_brush_ptr: *mut GpBrush = &mut bg_brush; + GdipGetPenBrushFill(bg_pen_ptr, &mut bg_brush_ptr as _); + + if self.rounded_corner { + draw_round_rect( + graphics_ptr, + bg_brush_ptr, + 0.0, + 0.0, + width as f32, + height as f32, + corner_radius as f32, + ); + } else { + GdipFillRectangle( + graphics_ptr, + bg_brush_ptr, + 0.0, + 0.0, + width as f32, + height as f32, + ); + } + + let icons_width = item_size * state.apps.len() as i32; + let icons_height = item_size; + let bitmap_icons = draw_icons( + state, + hdc_screen, + icon_size, + icons_width, + icons_height, + corner_radius, + fg_color, + bg_color, + ); + + let mut bitmap = GpBitmap::default(); + let mut bitmap_ptr: *mut GpBitmap = &mut bitmap as _; + GdipCreateBitmapFromHBITMAP(bitmap_icons, HPALETTE::default(), &mut bitmap_ptr as _); + + let image_ptr: *mut GpImage = bitmap_ptr as *mut GpImage; + GdipDrawImageRect( + graphics_ptr, + image_ptr, + WINDOW_BORDER_SIZE as f32, + WINDOW_BORDER_SIZE as f32, + icons_width as f32, + icons_height as f32, + ); + + let blend = BLENDFUNCTION { + BlendOp: AC_SRC_OVER as _, + SourceConstantAlpha: 255, + AlphaFormat: AC_SRC_ALPHA as _, + ..Default::default() }; - FillRect(scaled_dc, &rect as _, brush); + let _ = UpdateLayeredWindow( + hwnd, + hdc_screen, + Some(&POINT { x, y }), + Some(&SIZE { + cx: width, + cy: height, + }), + hdc_mem, + Some(&POINT::default()), + COLORREF(0), + Some(&blend), + ULW_ALPHA, + ); + + GdipDisposeImage(image_ptr); + GdipDeleteBrush(bg_brush_ptr); + GdipDeletePen(bg_pen_ptr); + GdipDeleteGraphics(graphics_ptr); - self.mem_hdc = mem_dc; - self.mem_map = mem_map; - self.scaled_hdc = scaled_dc; - self.scaled_map = scaled_map; + let _ = DeleteObject(bitmap_icons); + let _ = DeleteObject(bitmap_mem); + let _ = DeleteDC(hdc_mem); } - (icon_size, width, height) + if self.show { + return; + } + unsafe { + let _ = ShowWindow(self.hwnd, SW_SHOW); + let _ = SetFocus(self.hwnd); + } + self.show = true; } - /// Draw state onto hdc in memory - pub fn paint(&mut self, state: &SwitchAppsState) { - self.paint0(state); + pub fn unpaint(&mut self, state: SwitchAppsState) { unsafe { - SetStretchBltMode(self.mem_hdc, HALFTONE); - let _ = StretchBlt( - self.mem_hdc, - 0, - 0, - self.width, - self.height, - self.scaled_hdc, - 0, - 0, - self.width * self.scale, - self.height * self.scale, - SRCCOPY, - ); + let _ = ShowWindow(self.hwnd, SW_HIDE); + } + for (hicon, _) in state.apps { + let _ = unsafe { DestroyIcon(hicon) }; } + self.show = false; } +} - pub fn display(&mut self) { +impl Drop for GdiAAPainter { + fn drop(&mut self) { unsafe { - let mut ps = PAINTSTRUCT::default(); - let hdc = BeginPaint(self.hwnd, &mut ps); - let _ = BitBlt( - hdc, - 0, - 0, - self.width, - self.height, - self.mem_hdc, - 0, - 0, - SRCCOPY, - ); - let _ = EndPaint(self.hwnd, &ps); + ReleaseDC(self.hwnd, self.hdc_screen); + GdiplusShutdown(self.token); } } +} - fn paint0(&mut self, state: &SwitchAppsState) { - unsafe { - // draw background - let rect = RECT { - left: 0, - top: 0, - right: self.width * self.scale, - bottom: self.width * self.scale, - }; - FillRect(self.scaled_hdc, &rect as _, CreateSolidBrush(self.fg_color)); - - let cy = (WINDOW_BORDER_SIZE + ICON_BORDER_SIZE) * self.scale; - let brush_icon = HBRUSH::default(); - let item_size = (state.icon_size + ICON_BORDER_SIZE * 2) * self.scale; - - for (i, (icon, _)) in state.apps.iter().enumerate() { - // draw the box for selected icon - if i == state.index { - let left = item_size * (i as i32) + WINDOW_BORDER_SIZE * self.scale; - let top = WINDOW_BORDER_SIZE * self.scale; - let right = left + item_size; - let bottom = top + item_size; - let rect = RECT { - left, - top, - right, - bottom, - }; - FillRect(self.scaled_hdc, &rect as _, CreateSolidBrush(self.bg_color)); - } - - let cx = cy + item_size * (i as i32); - let _ = DrawIconEx( - self.scaled_hdc, - cx, - cy, - *icon, - state.icon_size * self.scale, - state.icon_size * self.scale, - 0, - brush_icon, - DI_NORMAL, +pub fn find_clicked_app_index(state: &SwitchAppsState) -> Option { + let Coordinate { + x, y, item_size, .. + } = Coordinate::new(state.apps.len() as i32); + + let mut cursor_pos = POINT::default(); + let _ = unsafe { GetCursorPos(&mut cursor_pos) }; + + let xpos = cursor_pos.x - x; + let ypos = cursor_pos.y - y; + + let cy = WINDOW_BORDER_SIZE; + for (i, _) in state.apps.iter().enumerate() { + let cx = WINDOW_BORDER_SIZE + item_size * (i as i32); + if xpos >= cx && xpos < cx + item_size && ypos >= cy && ypos < cy + item_size { + return Some(i); + } + } + None +} + +const fn theme_color(light_theme: bool) -> (u32, u32) { + match light_theme { + true => (FG_LIGHT_COLOR, BG_LIGHT_COLOR), + false => (FG_DARK_COLOR, BG_DARK_COLOR), + } +} + +unsafe fn draw_round_rect( + graphic_ptr: *mut GpGraphics, + brush_ptr: *mut GpBrush, + left: f32, + top: f32, + right: f32, + bottom: f32, + corner_radius: f32, +) { + unsafe { + let mut path = GpPath::default(); + let mut path_ptr: *mut GpPath = &mut path; + GdipCreatePath(FillModeAlternate, &mut path_ptr as _); + GdipAddPathArc( + path_ptr, + left, + top, + corner_radius, + corner_radius, + 180.0, + 90.0, + ); + GdipAddPathArc( + path_ptr, + right - corner_radius, + top, + corner_radius, + corner_radius, + 270.0, + 90.0, + ); + GdipAddPathArc( + path_ptr, + right - corner_radius, + bottom - corner_radius, + corner_radius, + corner_radius, + 0.0, + 90.0, + ); + GdipAddPathArc( + path_ptr, + left, + bottom - corner_radius, + corner_radius, + corner_radius, + 90.0, + 90.0, + ); + GdipClosePathFigure(path_ptr); + GdipFillPath(graphic_ptr, brush_ptr, path_ptr); + GdipDeletePath(path_ptr); + } +} + +#[allow(clippy::too_many_arguments)] +fn draw_icons( + state: &SwitchAppsState, + hdc_screen: HDC, + icon_size: i32, + width: i32, + height: i32, + corner_radius: i32, + fg_color: u32, + bg_color: u32, +) -> HBITMAP { + let scaled_width = width * SCALE_FACTOR; + let scaled_height = height * SCALE_FACTOR; + let scaled_corner_radius = corner_radius * SCALE_FACTOR; + let scaled_border_size = ICON_BORDER_SIZE * SCALE_FACTOR; + let scaled_icon_inner_size = icon_size * SCALE_FACTOR; + let scaled_icon_outer_size = scaled_icon_inner_size + scaled_border_size * 2; + + unsafe { + let hdc_tmp = CreateCompatibleDC(hdc_screen); + let bitmap_tmp = CreateCompatibleBitmap(hdc_screen, width, height); + SelectObject(hdc_tmp, bitmap_tmp); + + let hdc_scaled = CreateCompatibleDC(hdc_screen); + let bitmap_scaled = CreateCompatibleBitmap(hdc_screen, scaled_width, scaled_height); + SelectObject(hdc_scaled, bitmap_scaled); + + let fg_brush = CreateSolidBrush(COLORREF(fg_color)); + let bg_brush = CreateSolidBrush(COLORREF(bg_color)); + + let rect = RECT { + left: 0, + top: 0, + right: scaled_width, + bottom: scaled_height, + }; + + FillRect(hdc_scaled, &rect, bg_brush); + + for (i, (icon, _)) in state.apps.iter().enumerate() { + // draw the box for selected icon + if i == state.index { + let left = scaled_icon_outer_size * (i as i32); + let top = 0; + let right = left + scaled_icon_outer_size; + let bottom = top + scaled_icon_outer_size; + let rgn = CreateRoundRectRgn( + left, + top, + right, + bottom, + scaled_corner_radius, + scaled_corner_radius, ); + let _ = FillRgn(hdc_scaled, rgn, fg_brush); + let _ = DeleteObject(rgn); } + + let cx = scaled_border_size + scaled_icon_outer_size * (i as i32); + let _ = DrawIconEx( + hdc_scaled, + cx, + scaled_border_size, + *icon, + scaled_icon_inner_size, + scaled_icon_inner_size, + 0, + HBRUSH::default(), + DI_NORMAL, + ); } + + SetStretchBltMode(hdc_tmp, HALFTONE); + let _ = StretchBlt( + hdc_tmp, + 0, + 0, + width, + height, + hdc_scaled, + 0, + 0, + scaled_width, + scaled_height, + SRCCOPY, + ); + + let _ = DeleteObject(fg_brush); + let _ = DeleteObject(bg_brush); + let _ = DeleteObject(bitmap_scaled); + let _ = DeleteDC(hdc_scaled); + let _ = DeleteDC(hdc_tmp); + + bitmap_tmp } } -fn is_light_theme() -> Result { - let reg_key = RegKey::new_hkcu( - w!("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"), - w!("SystemUsesLightTheme"), - )?; - let value = reg_key.get_int()?; - Ok(value == 1) +struct Coordinate { + x: i32, + y: i32, + width: i32, + height: i32, + icon_size: i32, + item_size: i32, +} + +impl Coordinate { + fn new(num_apps: i32) -> Self { + let monitor_rect = get_moinitor_rect(); + let monitor_width = monitor_rect.right - monitor_rect.left; + let monitor_height = monitor_rect.bottom - monitor_rect.top; + + let icon_size = ((monitor_width - 2 * WINDOW_BORDER_SIZE) / num_apps + - ICON_BORDER_SIZE * 2) + .min(ICON_SIZE); + + let item_size = icon_size + ICON_BORDER_SIZE * 2; + let width = item_size * num_apps + WINDOW_BORDER_SIZE * 2; + let height = item_size + WINDOW_BORDER_SIZE * 2; + let x = monitor_rect.left + (monitor_width - width) / 2; + let y = monitor_rect.top + (monitor_height - height) / 2; + + Self { + x, + y, + width, + height, + icon_size, + item_size, + } + } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0fb7867..23de7c7 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -6,6 +6,8 @@ mod scheduled_task; mod single_instance; mod window; mod windows_icon; +mod windows_theme; +mod windows_version; pub use admin::*; pub use check_error::*; @@ -15,6 +17,8 @@ pub use scheduled_task::*; pub use single_instance::*; pub use window::*; pub use windows_icon::get_module_icon_ex; +pub use windows_theme::*; +pub use windows_version::*; pub fn to_wstring(value: &str) -> Vec { value.encode_utf16().chain(Some(0)).collect::>() diff --git a/src/utils/window.rs b/src/utils/window.rs index ab26e73..3682cd0 100644 --- a/src/utils/window.rs +++ b/src/utils/window.rs @@ -1,8 +1,11 @@ use anyhow::{anyhow, Result}; use indexmap::IndexMap; use windows::core::PWSTR; -use windows::Win32::Foundation::{BOOL, HWND, LPARAM, MAX_PATH, TRUE, WPARAM}; +use windows::Win32::Foundation::{BOOL, HWND, LPARAM, MAX_PATH, POINT, RECT, TRUE, WPARAM}; use windows::Win32::Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED}; +use windows::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITOR_DEFAULTTONEAREST, +}; use windows::Win32::System::Console::{AllocConsole, FreeConsole, GetConsoleWindow}; use windows::Win32::System::LibraryLoader::GetModuleFileNameW; use windows::Win32::System::Threading::{ @@ -10,11 +13,11 @@ use windows::Win32::System::Threading::{ PROCESS_VM_READ, }; use windows::Win32::UI::WindowsAndMessaging::{ - CreateIconFromResourceEx, EnumWindows, GetForegroundWindow, GetWindow, GetWindowLongPtrW, - GetWindowPlacement, GetWindowTextW, GetWindowThreadProcessId, IsIconic, IsWindowVisible, - LoadIconW, SendMessageW, SetForegroundWindow, SetWindowPos, ShowWindow, GCL_HICON, GWL_EXSTYLE, - GWL_USERDATA, GW_OWNER, HICON, ICON_BIG, IDI_APPLICATION, LR_DEFAULTCOLOR, SWP_NOZORDER, - SW_RESTORE, WINDOWPLACEMENT, WM_GETICON, WS_EX_TOPMOST, + CreateIconFromResourceEx, EnumWindows, GetCursorPos, GetForegroundWindow, GetWindow, + GetWindowLongPtrW, GetWindowPlacement, GetWindowTextW, GetWindowThreadProcessId, IsIconic, + IsWindowVisible, LoadIconW, SendMessageW, SetForegroundWindow, SetWindowPos, ShowWindow, + GCL_HICON, GWL_EXSTYLE, GWL_USERDATA, GW_OWNER, HICON, ICON_BIG, IDI_APPLICATION, + LR_DEFAULTCOLOR, SWP_NOZORDER, SW_RESTORE, WINDOWPLACEMENT, WM_GETICON, WS_EX_TOPMOST, }; use std::fs::File; @@ -56,6 +59,21 @@ pub fn is_small_window(hwnd: HWND) -> bool { width < 120 || height < 90 } +pub fn get_moinitor_rect() -> RECT { + unsafe { + let mut mi = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..MONITORINFO::default() + }; + let mut cursor = POINT::default(); + let _ = GetCursorPos(&mut cursor); + + let hmonitor = MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST); + let _ = GetMonitorInfoW(hmonitor, &mut mi); + mi.rcMonitor + } +} + pub fn get_window_size(hwnd: HWND) -> (i32, i32) { let mut placement = WINDOWPLACEMENT::default(); let _ = unsafe { GetWindowPlacement(hwnd, &mut placement) }; diff --git a/src/utils/windows_theme.rs b/src/utils/windows_theme.rs new file mode 100644 index 0000000..faa7f51 --- /dev/null +++ b/src/utils/windows_theme.rs @@ -0,0 +1,13 @@ +use windows::core::w; + +use super::RegKey; + +pub fn is_light_theme() -> bool { + let Ok(reg_key) = RegKey::new_hkcu( + w!("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"), + w!("SystemUsesLightTheme"), + ) else { + return false; + }; + reg_key.get_int().map(|v| v == 1).unwrap_or(false) +} diff --git a/src/utils/windows_version.rs b/src/utils/windows_version.rs new file mode 100644 index 0000000..e6dd28f --- /dev/null +++ b/src/utils/windows_version.rs @@ -0,0 +1,26 @@ +use windows::{ + Wdk::System::SystemServices::RtlGetVersion, + Win32::System::SystemInformation::{OSVERSIONINFOEXW, OSVERSIONINFOW}, +}; + +pub fn os_version_info() -> Option { + let mut info = OSVERSIONINFOW { + dwOSVersionInfoSize: std::mem::size_of::() as _, + ..Default::default() + }; + + let status = unsafe { RtlGetVersion(&mut info) }; + if status.is_ok() { + Some(info) + } else { + None + } +} + +pub fn is_win11() -> bool { + if let Some(info) = os_version_info() { + info.dwBuildNumber >= 22000 + } else { + false + } +}