Skip to content

Commit 97ca8f8

Browse files
committed
v1.0.12
- Added support for Windows High DPI Scaling
1 parent 4de95f0 commit 97ca8f8

File tree

3 files changed

+104
-47
lines changed

3 files changed

+104
-47
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ features = [
2424
"Win32_System_Threading",
2525
"Win32_Security",
2626
"Win32_UI_Input_KeyboardAndMouse",
27+
"Win32_UI_HiDpi",
2728
]
2829

2930
[build-dependencies]

build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ fn main() {
1616
let version = version.trim().trim_start_matches('v');
1717
println!("cargo:rustc-env=APP_VERSION={version}");
1818

19+
// Re-run build script whenever HEAD or tags change so the version stays current.
20+
println!("cargo:rerun-if-changed=.git/HEAD");
21+
println!("cargo:rerun-if-changed=.git/refs/tags");
22+
1923
// Embed the application icon into the executable.
2024
let mut res = winres::WindowsResource::new();
2125
res.set_icon("src/icons/icon.ico");

src/window.rs

Lines changed: 99 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::path::PathBuf;
2+
use std::sync::atomic::{AtomicU32, Ordering};
23
use std::sync::{Mutex, MutexGuard};
34
use std::time::Duration;
45

@@ -8,6 +9,7 @@ use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW
89
use windows::Win32::System::Registry::*;
910
use windows::Win32::System::Threading::CreateMutexW;
1011
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
12+
use windows::Win32::UI::HiDpi::*;
1113
use windows::Win32::UI::Input::KeyboardAndMouse::{ReleaseCapture, SetCapture};
1214
use windows::Win32::UI::WindowsAndMessaging::*;
1315
use windows::core::PCWSTR;
@@ -75,6 +77,33 @@ const IDM_RESET_POSITION: u16 = 30;
7577

7678
const DIVIDER_HIT_ZONE: i32 = 13; // LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN
7779

80+
const WM_DPICHANGED_MSG: u32 = 0x02E0;
81+
82+
/// Current system DPI (96 = 100% scaling, 144 = 150%, 192 = 200%, etc.)
83+
static CURRENT_DPI: AtomicU32 = AtomicU32::new(96);
84+
85+
/// Scale a base pixel value (designed at 96 DPI) to the current DPI.
86+
fn sc(px: i32) -> i32 {
87+
let dpi = CURRENT_DPI.load(Ordering::Relaxed);
88+
(px as f64 * dpi as f64 / 96.0).round() as i32
89+
}
90+
91+
/// Re-query the monitor DPI for our window and update the cached value.
92+
/// Uses GetDpiForWindow which returns the live DPI (unlike GetDpiForSystem
93+
/// which is cached at process startup and never changes).
94+
fn refresh_dpi() {
95+
let hwnd = {
96+
let state = lock_state();
97+
state.as_ref().map(|s| s.hwnd.to_hwnd())
98+
};
99+
if let Some(hwnd) = hwnd {
100+
let dpi = unsafe { GetDpiForWindow(hwnd) };
101+
if dpi > 0 {
102+
CURRENT_DPI.store(dpi, Ordering::Relaxed);
103+
}
104+
}
105+
}
106+
78107
unsafe impl Send for AppState {}
79108

80109
static STATE: Mutex<Option<AppState>> = Mutex::new(None);
@@ -269,18 +298,24 @@ const RIGHT_MARGIN: i32 = 1;
269298
const WIDGET_HEIGHT: i32 = 46;
270299

271300
fn total_widget_width() -> i32 {
272-
LEFT_DIVIDER_W
273-
+ DIVIDER_RIGHT_MARGIN
274-
+ LABEL_WIDTH
275-
+ LABEL_RIGHT_MARGIN
276-
+ (SEGMENT_W + SEGMENT_GAP) * SEGMENT_COUNT
277-
- SEGMENT_GAP
278-
+ BAR_RIGHT_MARGIN
279-
+ TEXT_WIDTH
280-
+ RIGHT_MARGIN
301+
sc(LEFT_DIVIDER_W)
302+
+ sc(DIVIDER_RIGHT_MARGIN)
303+
+ sc(LABEL_WIDTH)
304+
+ sc(LABEL_RIGHT_MARGIN)
305+
+ (sc(SEGMENT_W) + sc(SEGMENT_GAP)) * SEGMENT_COUNT
306+
- sc(SEGMENT_GAP)
307+
+ sc(BAR_RIGHT_MARGIN)
308+
+ sc(TEXT_WIDTH)
309+
+ sc(RIGHT_MARGIN)
281310
}
282311

283312
pub fn run() {
313+
// Enable Per-Monitor DPI Awareness V2 for crisp rendering at any scale factor
314+
unsafe {
315+
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
316+
CURRENT_DPI.store(GetDpiForSystem(), Ordering::Relaxed);
317+
}
318+
284319
// Single-instance guard: silently exit if another instance is running
285320
let mutex_name = native_interop::wide_str("Global\\ClaudeCodeUsageMonitor");
286321
let _mutex = unsafe {
@@ -324,7 +359,7 @@ pub fn run() {
324359
0,
325360
0,
326361
total_widget_width(),
327-
WIDGET_HEIGHT,
362+
sc(WIDGET_HEIGHT),
328363
HWND::default(),
329364
HMENU::default(),
330365
hinstance,
@@ -431,6 +466,7 @@ pub fn run() {
431466
/// Renders fully opaque with the actual taskbar background colour so that
432467
/// ClearType sub-pixel font rendering can be used for crisp, OS-native text.
433468
fn render_layered() {
469+
refresh_dpi();
434470
let (hwnd_val, is_dark, embedded, session_pct, session_text, weekly_pct, weekly_text) = {
435471
let state = lock_state();
436472
match state.as_ref() {
@@ -458,7 +494,7 @@ fn render_layered() {
458494
}
459495

460496
let width = total_widget_width();
461-
let height = WIDGET_HEIGHT;
497+
let height = sc(WIDGET_HEIGHT);
462498

463499
let accent = Color::from_hex("#D97757");
464500
let track = if is_dark {
@@ -588,8 +624,9 @@ fn paint_content(
588624
let _ = DeleteObject(bg_brush);
589625

590626
// Left divider
591-
let divider_top = (height - 25) / 2;
592-
let divider_bottom = divider_top + 25;
627+
let divider_h = sc(25);
628+
let divider_top = (height - divider_h) / 2;
629+
let divider_bottom = divider_top + divider_h;
593630

594631
let (div_left, div_right) = if is_dark {
595632
((80, 80, 80), (40, 40, 40))
@@ -601,32 +638,32 @@ fn paint_content(
601638
let left_rect = RECT {
602639
left: 0,
603640
top: divider_top,
604-
right: 2,
641+
right: sc(2),
605642
bottom: divider_bottom,
606643
};
607644
FillRect(hdc, &left_rect, left_brush);
608645
let _ = DeleteObject(left_brush);
609646

610647
let right_brush = CreateSolidBrush(COLORREF(native_interop::colorref(div_right.0, div_right.1, div_right.2)));
611648
let right_rect = RECT {
612-
left: 2,
649+
left: sc(2),
613650
top: divider_top,
614-
right: 3,
651+
right: sc(3),
615652
bottom: divider_bottom,
616653
};
617654
FillRect(hdc, &right_rect, right_brush);
618655
let _ = DeleteObject(right_brush);
619656

620-
let content_x = LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN;
621-
let row1_y = 5;
622-
let row2_y = 5 + SEGMENT_H + 10;
657+
let content_x = sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN);
658+
let row1_y = sc(5);
659+
let row2_y = sc(5) + sc(SEGMENT_H) + sc(10);
623660

624661
let _ = SetBkMode(hdc, TRANSPARENT);
625662
let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref()));
626663

627664
let font_name = native_interop::wide_str("Segoe UI");
628665
let font = CreateFontW(
629-
-12,
666+
sc(-12),
630667
0,
631668
0,
632669
0,
@@ -797,6 +834,7 @@ fn update_display() {
797834
}
798835

799836
fn position_at_taskbar() {
837+
refresh_dpi();
800838
let state = lock_state();
801839
let s = match state.as_ref() {
802840
Some(s) => s,
@@ -833,16 +871,17 @@ fn position_at_taskbar() {
833871

834872
let widget_width = total_widget_width();
835873

874+
let widget_height = sc(WIDGET_HEIGHT);
836875
if embedded {
837876
// Child window: coordinates relative to parent (taskbar)
838877
let x = tray_left - taskbar_rect.left - widget_width - tray_offset;
839-
let y = (taskbar_height - WIDGET_HEIGHT) / 2;
840-
native_interop::move_window(hwnd, x, y, widget_width, WIDGET_HEIGHT);
878+
let y = (taskbar_height - widget_height) / 2;
879+
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
841880
} else {
842881
// Topmost popup: screen coordinates
843882
let x = tray_left - widget_width - tray_offset;
844-
let y = taskbar_rect.top + (taskbar_height - WIDGET_HEIGHT) / 2;
845-
native_interop::move_window(hwnd, x, y, widget_width, WIDGET_HEIGHT);
883+
let y = taskbar_rect.top + (taskbar_height - widget_height) / 2;
884+
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
846885
}
847886
}
848887

@@ -883,6 +922,7 @@ unsafe extern "system" fn on_tray_location_changed(
883922
};
884923
if should_reposition {
885924
position_at_taskbar();
925+
render_layered();
886926
}
887927
}
888928
}
@@ -915,8 +955,14 @@ unsafe extern "system" fn wnd_proc(
915955
LRESULT(0)
916956
}
917957
WM_ERASEBKGND => LRESULT(1),
918-
WM_DISPLAYCHANGE => {
958+
WM_DISPLAYCHANGE | WM_DPICHANGED_MSG | WM_SETTINGCHANGE => {
959+
if msg == WM_DPICHANGED_MSG {
960+
let new_dpi = (wparam.0 & 0xFFFF) as u32;
961+
CURRENT_DPI.store(new_dpi, Ordering::Relaxed);
962+
}
963+
refresh_dpi();
919964
position_at_taskbar();
965+
render_layered();
920966
LRESULT(0)
921967
}
922968
WM_TIMER => {
@@ -966,7 +1012,7 @@ unsafe extern "system" fn wnd_proc(
9661012
let mut pt = POINT::default();
9671013
let _ = GetCursorPos(&mut pt);
9681014
let _ = ScreenToClient(hwnd, &mut pt);
969-
if pt.x < DIVIDER_HIT_ZONE {
1015+
if pt.x < sc(DIVIDER_HIT_ZONE) {
9701016
let cursor = LoadCursorW(HINSTANCE::default(), IDC_SIZEWE)
9711017
.unwrap_or_default();
9721018
SetCursor(cursor);
@@ -977,7 +1023,7 @@ unsafe extern "system" fn wnd_proc(
9771023
}
9781024
WM_LBUTTONDOWN => {
9791025
let client_x = (lparam.0 & 0xFFFF) as i16 as i32;
980-
if client_x < DIVIDER_HIT_ZONE {
1026+
if client_x < sc(DIVIDER_HIT_ZONE) {
9811027
let mut pt = POINT::default();
9821028
let _ = GetCursorPos(&mut pt);
9831029
let mut state = lock_state();
@@ -1049,14 +1095,15 @@ unsafe extern "system" fn wnd_proc(
10491095
}
10501096
}
10511097
let widget_width = total_widget_width();
1098+
let widget_height = sc(WIDGET_HEIGHT);
10521099
if s.embedded {
10531100
let x = tray_left - taskbar_rect.left - widget_width - new_offset;
1054-
let y = (taskbar_height - WIDGET_HEIGHT) / 2;
1055-
native_interop::move_window(hwnd_val, x, y, widget_width, WIDGET_HEIGHT);
1101+
let y = (taskbar_height - widget_height) / 2;
1102+
native_interop::move_window(hwnd_val, x, y, widget_width, widget_height);
10561103
} else {
10571104
let x = tray_left - widget_width - new_offset;
1058-
let y = taskbar_rect.top + (taskbar_height - WIDGET_HEIGHT) / 2;
1059-
native_interop::move_window(hwnd_val, x, y, widget_width, WIDGET_HEIGHT);
1105+
let y = taskbar_rect.top + (taskbar_height - widget_height) / 2;
1106+
native_interop::move_window(hwnd_val, x, y, widget_width, widget_height);
10601107
}
10611108
}
10621109
}
@@ -1342,13 +1389,18 @@ fn draw_row(
13421389
accent: &Color,
13431390
track: &Color,
13441391
) {
1392+
let seg_w = sc(SEGMENT_W);
1393+
let seg_h = sc(SEGMENT_H);
1394+
let seg_gap = sc(SEGMENT_GAP);
1395+
let corner_r = sc(CORNER_RADIUS);
1396+
13451397
unsafe {
13461398
let mut label_wide: Vec<u16> = label.encode_utf16().collect();
13471399
let mut label_rect = RECT {
13481400
left: x,
13491401
top: y,
1350-
right: x + LABEL_WIDTH,
1351-
bottom: y + SEGMENT_H,
1402+
right: x + sc(LABEL_WIDTH),
1403+
bottom: y + seg_h,
13521404
};
13531405
let _ = DrawTextW(
13541406
hdc,
@@ -1357,43 +1409,43 @@ fn draw_row(
13571409
DT_LEFT | DT_VCENTER | DT_SINGLELINE,
13581410
);
13591411

1360-
let bar_x = x + LABEL_WIDTH + LABEL_RIGHT_MARGIN;
1412+
let bar_x = x + sc(LABEL_WIDTH) + sc(LABEL_RIGHT_MARGIN);
13611413
let percent_clamped = percent.clamp(0.0, 100.0);
13621414

13631415
for i in 0..SEGMENT_COUNT {
1364-
let seg_x = bar_x + i * (SEGMENT_W + SEGMENT_GAP);
1416+
let seg_x = bar_x + i * (seg_w + seg_gap);
13651417
let seg_start = (i as f64) * 10.0;
13661418
let seg_end = seg_start + 10.0;
13671419

13681420
let seg_rect = RECT {
13691421
left: seg_x,
13701422
top: y,
1371-
right: seg_x + SEGMENT_W,
1372-
bottom: y + SEGMENT_H,
1423+
right: seg_x + seg_w,
1424+
bottom: y + seg_h,
13731425
};
13741426

13751427
if percent_clamped >= seg_end {
1376-
draw_rounded_rect(hdc, &seg_rect, accent, CORNER_RADIUS);
1428+
draw_rounded_rect(hdc, &seg_rect, accent, corner_r);
13771429
} else if percent_clamped <= seg_start {
1378-
draw_rounded_rect(hdc, &seg_rect, track, CORNER_RADIUS);
1430+
draw_rounded_rect(hdc, &seg_rect, track, corner_r);
13791431
} else {
1380-
draw_rounded_rect(hdc, &seg_rect, track, CORNER_RADIUS);
1432+
draw_rounded_rect(hdc, &seg_rect, track, corner_r);
13811433
let fraction = (percent_clamped - seg_start) / 10.0;
1382-
let fill_width = (SEGMENT_W as f64 * fraction) as i32;
1434+
let fill_width = (seg_w as f64 * fraction) as i32;
13831435
if fill_width > 0 {
13841436
let fill_rect = RECT {
13851437
left: seg_x,
13861438
top: y,
13871439
right: seg_x + fill_width,
1388-
bottom: y + SEGMENT_H,
1440+
bottom: y + seg_h,
13891441
};
13901442
let rgn = CreateRoundRectRgn(
13911443
seg_rect.left,
13921444
seg_rect.top,
13931445
seg_rect.right + 1,
13941446
seg_rect.bottom + 1,
1395-
CORNER_RADIUS * 2,
1396-
CORNER_RADIUS * 2,
1447+
corner_r * 2,
1448+
corner_r * 2,
13971449
);
13981450
let _ = SelectClipRgn(hdc, rgn);
13991451
let brush = CreateSolidBrush(COLORREF(accent.to_colorref()));
@@ -1406,13 +1458,13 @@ fn draw_row(
14061458
}
14071459

14081460
let text_x =
1409-
bar_x + SEGMENT_COUNT * (SEGMENT_W + SEGMENT_GAP) - SEGMENT_GAP + BAR_RIGHT_MARGIN;
1461+
bar_x + SEGMENT_COUNT * (seg_w + seg_gap) - seg_gap + sc(BAR_RIGHT_MARGIN);
14101462
let mut text_wide: Vec<u16> = text.encode_utf16().collect();
14111463
let mut text_rect = RECT {
14121464
left: text_x,
14131465
top: y,
1414-
right: text_x + TEXT_WIDTH,
1415-
bottom: y + SEGMENT_H,
1466+
right: text_x + sc(TEXT_WIDTH),
1467+
bottom: y + seg_h,
14161468
};
14171469
let _ = DrawTextW(
14181470
hdc,

0 commit comments

Comments
 (0)