11use std:: path:: PathBuf ;
2+ use std:: sync:: atomic:: { AtomicU32 , Ordering } ;
23use std:: sync:: { Mutex , MutexGuard } ;
34use std:: time:: Duration ;
45
@@ -8,6 +9,7 @@ use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW
89use windows:: Win32 :: System :: Registry :: * ;
910use windows:: Win32 :: System :: Threading :: CreateMutexW ;
1011use windows:: Win32 :: UI :: Accessibility :: HWINEVENTHOOK ;
12+ use windows:: Win32 :: UI :: HiDpi :: * ;
1113use windows:: Win32 :: UI :: Input :: KeyboardAndMouse :: { ReleaseCapture , SetCapture } ;
1214use windows:: Win32 :: UI :: WindowsAndMessaging :: * ;
1315use windows:: core:: PCWSTR ;
@@ -75,6 +77,33 @@ const IDM_RESET_POSITION: u16 = 30;
7577
7678const 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+
78107unsafe impl Send for AppState { }
79108
80109static STATE : Mutex < Option < AppState > > = Mutex :: new ( None ) ;
@@ -269,18 +298,24 @@ const RIGHT_MARGIN: i32 = 1;
269298const WIDGET_HEIGHT : i32 = 46 ;
270299
271300fn 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
283312pub 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.
433468fn 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
799836fn 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