├── .gitattributes ├── README.md ├── .gitignore ├── LICENSE └── main.c /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # win32-custom-titlebar 2 | 3 | C99 / C++98 code to render a custom title bar on Windows. 4 | 5 | [Read this article](https://kubyshkin.name/posts/win32-window-custom-title-bar-caption/) for 6 | more details. 7 | 8 | ## Building 9 | 10 | To build the project run `build.bat` in an MSVC command prompt. 11 | 12 | You can also build with Clang by running: 13 | 14 | ```bash 15 | clang-cl -D UNICODE main.c 16 | ``` 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PDB files 2 | *.pdb 3 | 4 | # Prerequisites 5 | *.d 6 | 7 | # Compiled Object files 8 | *.slo 9 | *.lo 10 | *.o 11 | *.obj 12 | 13 | # Precompiled Headers 14 | *.gch 15 | *.pch 16 | 17 | # Compiled Dynamic libraries 18 | *.so 19 | *.dylib 20 | *.dll 21 | 22 | # Fortran module files 23 | *.mod 24 | *.smod 25 | 26 | # Compiled Static libraries 27 | *.lai 28 | *.la 29 | *.a 30 | *.lib 31 | 32 | # Executables 33 | *.exe 34 | *.out 35 | *.app 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Domagoj "oberth" Pandža 4 | Copyright (c) 2021 Dmitriy Kubyshkin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include 5 | #pragma comment(lib, "user32.lib") 6 | #pragma comment(lib, "gdi32.lib") 7 | #pragma comment(lib, "uxtheme.lib") 8 | 9 | #include 10 | 11 | 12 | // These are defined in but we don't want to pull in whole header 13 | // And we need them because coordinates are signed when you have multi-monitor setup. 14 | #ifndef GET_X_PARAM 15 | #define GET_X_PARAM(lp) ((int)(short)LOWORD(lp)) 16 | #endif 17 | 18 | #ifndef GET_Y_PARAM 19 | #define GET_Y_PARAM(lp) ((int)(short)HIWORD(lp)) 20 | #endif 21 | 22 | static bool 23 | win32_window_is_maximized( 24 | HWND handle 25 | ); 26 | 27 | static LRESULT 28 | win32_custom_title_bar_example_window_callback( 29 | HWND handle, 30 | UINT message, 31 | WPARAM w_param, 32 | LPARAM l_param 33 | ); // Implementation is at the end of the file 34 | 35 | int WinMain( 36 | HINSTANCE hInstance, 37 | HINSTANCE hPrevInstance, 38 | char *pCmdLine, 39 | int nCmdShow 40 | ) { 41 | // Support high-dpi screens 42 | if (!SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) { 43 | OutputDebugStringA("WARNING: could not set DPI awareness"); 44 | } 45 | 46 | // Register a "Window Class" 47 | static const wchar_t *window_class_name = L"WIN32_CUSTOM_TITLEBAR_EXAMPLE"; 48 | WNDCLASSEXW window_class = {0}; 49 | { 50 | window_class.cbSize = sizeof(window_class); 51 | window_class.lpszClassName = window_class_name; 52 | // Set the procedure that will receive window messages (events) 53 | window_class.lpfnWndProc = win32_custom_title_bar_example_window_callback; 54 | // Ask to send WM_PAINT when resizing horizontally and vertically 55 | window_class.style = CS_HREDRAW | CS_VREDRAW; 56 | } 57 | RegisterClassExW(&window_class); 58 | 59 | int window_style 60 | = WS_THICKFRAME // required for a standard resizeable window 61 | | WS_SYSMENU // Explicitly ask for the titlebar to support snapping via Win + ← / Win + → 62 | | WS_MAXIMIZEBOX // Add maximize button to support maximizing via mouse dragging 63 | // to the top of the screen 64 | | WS_MINIMIZEBOX // Add minimize button to support minimizing by clicking on the taskbar icon 65 | | WS_VISIBLE; // Make window visible after it is created (not important) 66 | CreateWindowExW( 67 | WS_EX_APPWINDOW, 68 | window_class_name, 69 | L"Win32 Custom Title Bar Example", 70 | // The 71 | window_style, 72 | CW_USEDEFAULT, 73 | CW_USEDEFAULT, 74 | 800, 75 | 600, 76 | 0, 77 | 0, 78 | 0, 79 | 0 80 | ); 81 | 82 | // Run message loop 83 | for (MSG message = {0};;) { 84 | BOOL result = GetMessageW(&message, 0, 0, 0); 85 | if (result > 0) { 86 | TranslateMessage(&message); 87 | DispatchMessageW(&message); 88 | } else { 89 | break; 90 | } 91 | } 92 | 93 | return 0; 94 | } 95 | 96 | static int 97 | win32_dpi_scale( 98 | int value, 99 | UINT dpi 100 | ) { 101 | return (int)((float)value * dpi / 96); 102 | } 103 | 104 | static void 105 | set_menu_item_state( 106 | HMENU menu, 107 | MENUITEMINFO* menuItemInfo, 108 | UINT item, 109 | bool enabled 110 | ) { 111 | menuItemInfo->fState = enabled ? MF_ENABLED : MF_DISABLED; 112 | SetMenuItemInfo(menu, item, false, menuItemInfo); 113 | } 114 | 115 | // Adopted from: 116 | // https://github.com/oberth/custom-chrome/blob/master/source/gui/window_helper.hpp#L52-L64 117 | static RECT 118 | win32_titlebar_rect( 119 | HWND handle 120 | ) { 121 | SIZE title_bar_size = {0}; 122 | const int top_and_bottom_borders = 2; 123 | HTHEME theme = OpenThemeData(handle, L"WINDOW"); 124 | UINT dpi = GetDpiForWindow(handle); 125 | GetThemePartSize(theme, NULL, WP_CAPTION, CS_ACTIVE, NULL, TS_TRUE, &title_bar_size); 126 | CloseThemeData(theme); 127 | 128 | int height = win32_dpi_scale(title_bar_size.cy, dpi) + top_and_bottom_borders; 129 | 130 | RECT rect; 131 | GetClientRect(handle, &rect); 132 | rect.bottom = rect.top + height; 133 | 134 | if (win32_window_is_maximized(handle)) { 135 | int frame_y = GetSystemMetricsForDpi(SM_CYFRAME, dpi); 136 | rect.top -= frame_y; 137 | rect.bottom -= frame_y; 138 | } 139 | 140 | return rect; 141 | } 142 | 143 | // Set this to 0 to remove the fake shadow painting 144 | #define WIN32_FAKE_SHADOW_HEIGHT 1 145 | // The offset of the 2 rectangles of the maximized window button 146 | #define WIN32_MAXIMIZED_RECTANGLE_OFFSET 2 147 | 148 | static RECT 149 | win32_fake_shadow_rect( 150 | HWND handle 151 | ) { 152 | RECT rect; 153 | GetClientRect(handle, &rect); 154 | rect.bottom = rect.top + WIN32_FAKE_SHADOW_HEIGHT; 155 | return rect; 156 | } 157 | 158 | typedef struct { 159 | RECT close; 160 | RECT maximize; 161 | RECT minimize; 162 | } CustomTitleBarButtonRects; 163 | 164 | typedef enum { 165 | CustomTitleBarHoveredButton_None, 166 | CustomTitleBarHoveredButton_Minimize, 167 | CustomTitleBarHoveredButton_Maximize, 168 | CustomTitleBarHoveredButton_Close, 169 | } CustomTitleBarHoveredButton; 170 | 171 | static CustomTitleBarButtonRects 172 | win32_get_title_bar_button_rects( 173 | HWND handle, 174 | const RECT *title_bar_rect 175 | ){ 176 | UINT dpi = GetDpiForWindow(handle); 177 | CustomTitleBarButtonRects button_rects; 178 | // Sadly SM_CXSIZE does not result in the right size buttons for Win10 179 | int button_width = win32_dpi_scale(47, dpi); 180 | button_rects.close = *title_bar_rect; 181 | button_rects.close.top += WIN32_FAKE_SHADOW_HEIGHT; 182 | 183 | button_rects.close.left = button_rects.close.right - button_width; 184 | button_rects.maximize = button_rects.close; 185 | button_rects.maximize.left -= button_width; 186 | button_rects.maximize.right -= button_width; 187 | button_rects.minimize = button_rects.maximize; 188 | button_rects.minimize.left -= button_width; 189 | button_rects.minimize.right -= button_width; 190 | return button_rects; 191 | } 192 | 193 | static bool 194 | win32_window_is_maximized( 195 | HWND handle 196 | ) { 197 | WINDOWPLACEMENT placement = {0}; 198 | placement.length = sizeof(WINDOWPLACEMENT); 199 | if (GetWindowPlacement(handle, &placement)) { 200 | return placement.showCmd == SW_SHOWMAXIMIZED; 201 | } 202 | return false; 203 | } 204 | 205 | static void 206 | win32_center_rect_in_rect( 207 | RECT *to_center, 208 | const RECT *outer_rect 209 | ) { 210 | int to_width = to_center->right - to_center->left; 211 | int to_height = to_center->bottom - to_center->top; 212 | int outer_width = outer_rect->right - outer_rect->left; 213 | int outer_height = outer_rect->bottom - outer_rect->top; 214 | 215 | int padding_x = (outer_width - to_width) / 2; 216 | int padding_y = (outer_height - to_height) / 2; 217 | 218 | to_center->left = outer_rect->left + padding_x; 219 | to_center->top = outer_rect->top + padding_y; 220 | to_center->right = to_center->left + to_width; 221 | to_center->bottom = to_center->top + to_height; 222 | } 223 | 224 | static LRESULT 225 | win32_custom_title_bar_example_window_callback( 226 | HWND handle, 227 | UINT message, 228 | WPARAM w_param, 229 | LPARAM l_param 230 | ) { 231 | CustomTitleBarHoveredButton title_bar_hovered_button = 232 | (CustomTitleBarHoveredButton)GetWindowLongPtrW(handle, GWLP_USERDATA); 233 | 234 | switch (message) { 235 | // Handling this event allows us to extend client (paintable) area into the title bar region 236 | // The information is partially coming from: 237 | // https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#extending-the-client-frame 238 | // Most important paragraph is: 239 | // To remove the standard window frame, you must handle the WM_NCCALCSIZE message, 240 | // specifically when its wParam value is TRUE and the return value is 0. 241 | // By doing so, your application uses the entire window region as the client area, 242 | // removing the standard frame. 243 | case WM_NCCALCSIZE: { 244 | if (!w_param) return DefWindowProc(handle, message, w_param, l_param); 245 | UINT dpi = GetDpiForWindow(handle); 246 | 247 | int frame_x = GetSystemMetricsForDpi(SM_CXFRAME, dpi); 248 | int frame_y = GetSystemMetricsForDpi(SM_CYFRAME, dpi); 249 | int padding = GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi); 250 | 251 | NCCALCSIZE_PARAMS* params = (NCCALCSIZE_PARAMS *)l_param; 252 | RECT* requested_client_rect = params->rgrc; 253 | 254 | requested_client_rect->right -= frame_x + padding; 255 | requested_client_rect->left += frame_x + padding; 256 | requested_client_rect->bottom -= frame_y + padding; 257 | 258 | if (win32_window_is_maximized(handle)) { 259 | requested_client_rect->top += frame_y + padding; 260 | } 261 | 262 | return 0; 263 | } 264 | case WM_CREATE: { 265 | // Inform the application of the frame change to force redrawing with the new 266 | // client area that is extended into the title bar 267 | SetWindowPos( 268 | handle, NULL, 269 | 0, 0, 0, 0, 270 | SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER 271 | ); 272 | break; 273 | } 274 | case WM_ACTIVATE: { 275 | RECT title_bar_rect = win32_titlebar_rect(handle); 276 | InvalidateRect(handle, &title_bar_rect, FALSE); 277 | return DefWindowProc(handle, message, w_param, l_param); 278 | } 279 | case WM_NCHITTEST: { 280 | // Let the default procedure handle resizing areas 281 | LRESULT hit = DefWindowProc(handle, message, w_param, l_param); 282 | switch (hit) { 283 | case HTNOWHERE: 284 | case HTRIGHT: 285 | case HTLEFT: 286 | case HTTOPLEFT: 287 | case HTTOP: 288 | case HTTOPRIGHT: 289 | case HTBOTTOMRIGHT: 290 | case HTBOTTOM: 291 | case HTBOTTOMLEFT: { 292 | return hit; 293 | } 294 | } 295 | // Check if hover button is on maximize to support SnapLayout on Windows 11 296 | if (title_bar_hovered_button == CustomTitleBarHoveredButton_Maximize) { 297 | return HTMAXBUTTON; 298 | } 299 | 300 | // Looks like adjustment happening in NCCALCSIZE is messing with the detection 301 | // of the top hit area so manually fixing that. 302 | UINT dpi = GetDpiForWindow(handle); 303 | int frame_y = GetSystemMetricsForDpi(SM_CYFRAME, dpi); 304 | int padding = GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi); 305 | POINT cursor_point = {0}; 306 | cursor_point.x = GET_X_PARAM(l_param); 307 | cursor_point.y = GET_Y_PARAM(l_param); 308 | ScreenToClient(handle, &cursor_point); 309 | // We should not return HTTOP when hit-testing a maximized window 310 | if (!win32_window_is_maximized(handle) && cursor_point.y > 0 && cursor_point.y < frame_y + padding) { 311 | return HTTOP; 312 | } 313 | 314 | // Since we are drawing our own caption, this needs to be a custom test 315 | if (cursor_point.y < win32_titlebar_rect(handle).bottom) { 316 | return HTCAPTION; 317 | } 318 | 319 | return HTCLIENT; 320 | } 321 | case WM_PAINT: { 322 | bool has_focus = !!GetFocus(); 323 | 324 | PAINTSTRUCT ps; 325 | HDC hdc = BeginPaint(handle, &ps); 326 | 327 | // Paint Background 328 | COLORREF bg_color = RGB(200, 250, 230); 329 | HBRUSH bg_brush = CreateSolidBrush(bg_color); 330 | FillRect(hdc, &ps.rcPaint, bg_brush); 331 | DeleteObject(bg_brush); 332 | 333 | // Paint Title Bar 334 | HTHEME theme = OpenThemeData(handle, L"WINDOW"); 335 | 336 | COLORREF title_bar_color = RGB(150, 200, 180); 337 | HBRUSH title_bar_brush = CreateSolidBrush(title_bar_color); 338 | COLORREF title_bar_hover_color = RGB(130, 180, 160); 339 | HBRUSH title_bar_hover_brush = CreateSolidBrush(title_bar_hover_color); 340 | 341 | RECT title_bar_rect = win32_titlebar_rect(handle); 342 | 343 | // Title Bar Background 344 | FillRect(hdc, &title_bar_rect, title_bar_brush); 345 | 346 | COLORREF title_bar_item_color = has_focus ? RGB(33, 33, 33) : RGB(127, 127, 127); 347 | 348 | HBRUSH button_icon_brush = CreateSolidBrush(title_bar_item_color); 349 | HPEN button_icon_pen = CreatePen(PS_SOLID, 1, title_bar_item_color); 350 | 351 | CustomTitleBarButtonRects button_rects = 352 | win32_get_title_bar_button_rects(handle, &title_bar_rect); 353 | 354 | UINT dpi = GetDpiForWindow(handle); 355 | int icon_dimension = win32_dpi_scale(10, dpi); 356 | 357 | { // Minimize button 358 | if (title_bar_hovered_button == CustomTitleBarHoveredButton_Minimize) { 359 | FillRect(hdc, &button_rects.minimize, title_bar_hover_brush); 360 | } 361 | RECT icon_rect = {0}; 362 | icon_rect.right = icon_dimension; 363 | icon_rect.bottom = 1; 364 | win32_center_rect_in_rect(&icon_rect, &button_rects.minimize); 365 | FillRect(hdc, &icon_rect, button_icon_brush); 366 | } 367 | 368 | { // Maximize button 369 | bool const is_hovered = title_bar_hovered_button == CustomTitleBarHoveredButton_Maximize; 370 | if (is_hovered) { 371 | FillRect(hdc, &button_rects.maximize, title_bar_hover_brush); 372 | } 373 | RECT icon_rect = {0}; 374 | icon_rect.right = icon_dimension; 375 | icon_rect.bottom = icon_dimension; 376 | win32_center_rect_in_rect(&icon_rect, &button_rects.maximize); 377 | SelectObject(hdc, button_icon_pen); 378 | SelectObject(hdc, GetStockObject(HOLLOW_BRUSH)); 379 | if (win32_window_is_maximized(handle)) { 380 | // Draw the maximized icon 381 | Rectangle(hdc, 382 | icon_rect.left + WIN32_MAXIMIZED_RECTANGLE_OFFSET, 383 | icon_rect.top - WIN32_MAXIMIZED_RECTANGLE_OFFSET, 384 | icon_rect.right + WIN32_MAXIMIZED_RECTANGLE_OFFSET, 385 | icon_rect.bottom - WIN32_MAXIMIZED_RECTANGLE_OFFSET 386 | ); 387 | FillRect(hdc, &icon_rect, is_hovered ? title_bar_hover_brush : title_bar_brush); 388 | } 389 | Rectangle(hdc, icon_rect.left, icon_rect.top, icon_rect.right, icon_rect.bottom); 390 | } 391 | 392 | { // Close button 393 | HPEN custom_pen = 0; 394 | if (title_bar_hovered_button == CustomTitleBarHoveredButton_Close) { 395 | HBRUSH fill_brush = CreateSolidBrush(RGB(0xCC, 0, 0)); 396 | FillRect(hdc, &button_rects.close, fill_brush); 397 | DeleteObject(fill_brush); 398 | custom_pen = CreatePen(PS_SOLID, 1, RGB(0xFF, 0xFF, 0xFF)); 399 | SelectObject(hdc, custom_pen); 400 | } 401 | RECT icon_rect = {0}; 402 | icon_rect.right = icon_dimension; 403 | icon_rect.bottom = icon_dimension; 404 | win32_center_rect_in_rect(&icon_rect, &button_rects.close); 405 | MoveToEx(hdc, icon_rect.left, icon_rect.top, NULL); 406 | LineTo(hdc, icon_rect.right + 1, icon_rect.bottom + 1); 407 | MoveToEx(hdc, icon_rect.left, icon_rect.bottom, NULL); 408 | LineTo(hdc, icon_rect.right + 1, icon_rect.top - 1); 409 | if (custom_pen) DeleteObject(custom_pen); 410 | } 411 | DeleteObject(title_bar_hover_brush); 412 | DeleteObject(button_icon_brush); 413 | DeleteObject(button_icon_pen); 414 | DeleteObject(title_bar_brush); 415 | 416 | // Draw window title 417 | LOGFONT logical_font; 418 | HFONT old_font = NULL; 419 | if (SUCCEEDED(SystemParametersInfoForDpi(SPI_GETICONTITLELOGFONT, sizeof(logical_font), &logical_font, false, dpi))) { 420 | HFONT theme_font = CreateFontIndirect(&logical_font); 421 | old_font = (HFONT)SelectObject(hdc, theme_font); 422 | } 423 | 424 | wchar_t title_text_buffer[255] = {0}; 425 | int buffer_length = sizeof(title_text_buffer) / sizeof(title_text_buffer[0]); 426 | GetWindowTextW(handle, title_text_buffer, buffer_length); 427 | RECT title_bar_text_rect = title_bar_rect; 428 | // Add padding on the left 429 | int text_padding = 10; // There seems to be no good way to get this offset 430 | title_bar_text_rect.left += text_padding; 431 | // Add padding on the right for the buttons 432 | title_bar_text_rect.right = button_rects.minimize.left - text_padding; 433 | DTTOPTS draw_theme_options = {sizeof(draw_theme_options)}; 434 | draw_theme_options.dwFlags = DTT_TEXTCOLOR; 435 | draw_theme_options.crText = title_bar_item_color; 436 | DrawThemeTextEx( 437 | theme, 438 | hdc, 439 | 0, 0, 440 | title_text_buffer, 441 | -1, 442 | DT_VCENTER | DT_SINGLELINE | DT_WORD_ELLIPSIS, 443 | &title_bar_text_rect, 444 | &draw_theme_options 445 | ); 446 | if (old_font) SelectObject(hdc, old_font); 447 | CloseThemeData(theme); 448 | 449 | // Paint fake top shadow. Original is missing because of the client rect extension. 450 | // You might need to tweak the colors here based on the color scheme of your app 451 | // or just remove it if you decide it is not worth it. 452 | static const COLORREF shadow_color = RGB(100, 100, 100); 453 | COLORREF fake_top_shadow_color = has_focus ? shadow_color : RGB( 454 | (GetRValue(title_bar_color) + GetRValue(shadow_color)) / 2, 455 | (GetGValue(title_bar_color) + GetGValue(shadow_color)) / 2, 456 | (GetBValue(title_bar_color) + GetBValue(shadow_color)) / 2 457 | ); 458 | HBRUSH fake_top_shadow_brush = CreateSolidBrush(fake_top_shadow_color); 459 | RECT fake_top_shadow_rect = win32_fake_shadow_rect(handle); 460 | FillRect(hdc, &fake_top_shadow_rect, fake_top_shadow_brush); 461 | DeleteObject(fake_top_shadow_brush); 462 | 463 | EndPaint(handle, &ps); 464 | break; 465 | } 466 | // Track when mouse hovers each of the title bar buttons to draw the highlight correctly 467 | case WM_NCMOUSEMOVE: { 468 | POINT cursor_point; 469 | GetCursorPos(&cursor_point); 470 | ScreenToClient(handle, &cursor_point); 471 | 472 | RECT title_bar_rect = win32_titlebar_rect(handle); 473 | CustomTitleBarButtonRects button_rects = win32_get_title_bar_button_rects(handle, &title_bar_rect); 474 | 475 | CustomTitleBarHoveredButton new_hovered_button = CustomTitleBarHoveredButton_None; 476 | if (PtInRect(&button_rects.close, cursor_point)){ 477 | new_hovered_button = CustomTitleBarHoveredButton_Close; 478 | } else if (PtInRect(&button_rects.minimize, cursor_point)){ 479 | new_hovered_button = CustomTitleBarHoveredButton_Minimize; 480 | } else if (PtInRect(&button_rects.maximize, cursor_point)){ 481 | new_hovered_button = CustomTitleBarHoveredButton_Maximize; 482 | } 483 | if (new_hovered_button != title_bar_hovered_button) { 484 | // You could do tighter invalidation here but probably doesn't matter 485 | InvalidateRect(handle, &button_rects.close, FALSE); 486 | InvalidateRect(handle, &button_rects.minimize, FALSE); 487 | InvalidateRect(handle, &button_rects.maximize, FALSE); 488 | 489 | SetWindowLongPtrW(handle, GWLP_USERDATA, (LONG_PTR)new_hovered_button); 490 | } 491 | return DefWindowProc(handle, message, w_param, l_param); 492 | } 493 | // If the mouse gets into the client area then no title bar buttons are hovered 494 | // so need to reset the hover state 495 | case WM_MOUSEMOVE: { 496 | if (title_bar_hovered_button) { 497 | RECT title_bar_rect = win32_titlebar_rect(handle); 498 | // You could do tighter invalidation here but probably doesn't matter 499 | InvalidateRect(handle, &title_bar_rect, FALSE); 500 | SetWindowLongPtrW(handle, GWLP_USERDATA, (LONG_PTR)CustomTitleBarHoveredButton_None); 501 | } 502 | return DefWindowProc(handle, message, w_param, l_param); 503 | } 504 | // Handle mouse down and mouse up in the caption area to handle clicks on the buttons 505 | case WM_NCLBUTTONDOWN: { 506 | // Clicks on buttons will be handled in WM_NCLBUTTONUP, but we still need 507 | // to remove default handling of the click to avoid it counting as drag. 508 | // 509 | // Ideally you also want to check that the mouse hasn't moved out or too much 510 | // between DOWN and UP messages. 511 | if (title_bar_hovered_button) { 512 | return 0; 513 | } 514 | // Default handling allows for dragging and double click to maximize 515 | return DefWindowProc(handle, message, w_param, l_param); 516 | } 517 | // Map button clicks to the right messages for the window 518 | case WM_NCLBUTTONUP: { 519 | if (title_bar_hovered_button == CustomTitleBarHoveredButton_Close) { 520 | PostMessageW(handle, WM_CLOSE, 0, 0); 521 | return 0; 522 | } else if (title_bar_hovered_button == CustomTitleBarHoveredButton_Minimize) { 523 | ShowWindow(handle, SW_MINIMIZE); 524 | return 0; 525 | } else if (title_bar_hovered_button == CustomTitleBarHoveredButton_Maximize) { 526 | int mode = win32_window_is_maximized(handle) ? SW_NORMAL : SW_MAXIMIZE; 527 | ShowWindow(handle, mode); 528 | return 0; 529 | } 530 | return DefWindowProc(handle, message, w_param, l_param); 531 | } 532 | case WM_NCRBUTTONUP: { 533 | if (w_param == HTCAPTION) { 534 | BOOL const isMaximized = IsZoomed(handle); 535 | MENUITEMINFO menu_item_info = { 536 | .cbSize = sizeof(menu_item_info), 537 | .fMask = MIIM_STATE 538 | }; 539 | HMENU const sys_menu = GetSystemMenu(handle, false); 540 | set_menu_item_state(sys_menu, &menu_item_info, SC_RESTORE, isMaximized); 541 | set_menu_item_state(sys_menu, &menu_item_info, SC_MOVE, !isMaximized); 542 | set_menu_item_state(sys_menu, &menu_item_info, SC_SIZE, !isMaximized); 543 | set_menu_item_state(sys_menu, &menu_item_info, SC_MINIMIZE, true); 544 | set_menu_item_state(sys_menu, &menu_item_info, SC_MAXIMIZE, !isMaximized); 545 | set_menu_item_state(sys_menu, &menu_item_info, SC_CLOSE, true); 546 | BOOL const result = TrackPopupMenu(sys_menu, TPM_RETURNCMD, GET_X_PARAM(l_param), GET_Y_PARAM(l_param), 0, handle, NULL); 547 | if (result != 0) { 548 | PostMessage(handle, WM_SYSCOMMAND, result, 0); 549 | } 550 | } 551 | return DefWindowProc(handle, message, w_param, l_param); 552 | } 553 | case WM_SETCURSOR: { 554 | // Show an arrow instead of the busy cursor 555 | SetCursor(LoadCursor(NULL, IDC_ARROW)); 556 | break; 557 | } 558 | case WM_DESTROY: { 559 | PostQuitMessage(0); 560 | return 0; 561 | } 562 | } 563 | 564 | return DefWindowProc(handle, message, w_param, l_param); 565 | } 566 | --------------------------------------------------------------------------------