first pass at custom window border

This commit is contained in:
Ryan Fleury
2024-04-17 08:46:21 -07:00
parent c31847f785
commit 572fad1f03
18 changed files with 9241 additions and 551 deletions
+5 -15
View File
@@ -7,22 +7,12 @@
////////////////////////////////
//~ NOTE(allen): Negotiate the windows header include order
#if OS_FEATURE_SOCKET
#include <WinSock2.h>
#endif
#include <Windows.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <timeapi.h>
#include <tlhelp32.h>
#include <Shlobj.h>
#if OS_FEATURE_GRAPHICAL
#include <shellscalingapi.h>
#endif
#if OS_FEATURE_SOCKET
#include <WS2tcpip.h>
#include <Mswsock.h>
#endif
#include <processthreadsapi.h>
////////////////////////////////
+12 -1
View File
@@ -7,6 +7,12 @@
////////////////////////////////
//~ rjf: Window Types
typedef U32 OS_WindowFlags;
enum
{
OS_WindowFlag_CustomBorder = (1<<0),
};
typedef void OS_WindowRepaintFunctionType(OS_Handle window, void *user_data);
////////////////////////////////
@@ -112,7 +118,7 @@ internal String8 os_get_clipboard_text(Arena *arena);
////////////////////////////////
//~ rjf: @os_hooks Windows (Implemented Per-OS)
internal OS_Handle os_window_open(Vec2F32 resolution, String8 title);
internal OS_Handle os_window_open(Vec2F32 resolution, OS_WindowFlags flags, String8 title);
internal void os_window_close(OS_Handle window);
internal void os_window_first_paint(OS_Handle window);
internal void os_window_equip_repaint(OS_Handle window, OS_WindowRepaintFunctionType *repaint, void *user_data);
@@ -122,8 +128,13 @@ internal B32 os_window_is_fullscreen(OS_Handle window);
internal void os_window_set_fullscreen(OS_Handle window, B32 fullscreen);
internal B32 os_window_is_maximized(OS_Handle window);
internal void os_window_set_maximized(OS_Handle window, B32 maximized);
internal void os_window_minimize(OS_Handle window);
internal void os_window_bring_to_front(OS_Handle window);
internal void os_window_set_monitor(OS_Handle window, OS_Handle monitor);
internal void os_window_clear_custom_border_data(OS_Handle handle);
internal void os_window_push_custom_title_bar(OS_Handle handle, F32 thickness);
internal void os_window_push_custom_edges(OS_Handle handle, F32 thickness);
internal void os_window_push_custom_title_bar_client_area(OS_Handle handle, Rng2F32 rect);
internal Rng2F32 os_rect_from_window(OS_Handle window);
internal Rng2F32 os_client_rect_from_window(OS_Handle window);
internal F32 os_dpi_from_window(OS_Handle window);
+26 -1
View File
@@ -24,7 +24,7 @@ os_get_clipboard_text(Arena *arena)
//~ rjf: @os_hooks Windows (Implemented Per-OS)
internal OS_Handle
os_window_open(Vec2F32 resolution, String8 title)
os_window_open(Vec2F32 resolution, OS_WindowFlags flags, String8 title)
{
OS_Handle handle = {1};
return handle;
@@ -78,6 +78,11 @@ os_window_set_maximized(OS_Handle window, B32 maximized)
{
}
internal void
os_window_minimize(OS_Handle window)
{
}
internal void
os_window_bring_to_front(OS_Handle window)
{
@@ -88,6 +93,26 @@ os_window_set_monitor(OS_Handle window, OS_Handle monitor)
{
}
internal void
os_window_clear_custom_border_data(OS_Handle handle)
{
}
internal void
os_window_push_custom_title_bar(OS_Handle handle, F32 thickness)
{
}
internal void
os_window_push_custom_edges(OS_Handle handle, F32 thickness)
{
}
internal void
os_window_push_custom_title_bar_client_area(OS_Handle handle, Rng2F32 rect)
{
}
internal Rng2F32
os_rect_from_window(OS_Handle window)
{
+322 -8
View File
@@ -1,6 +1,17 @@
// Copyright (c) 2024 Epic Games Tools
// Licensed under the MIT license (https://opensource.org/license/mit/)
////////////////////////////////
//~ rjf: Includes
#include <uxtheme.h>
#include <dwmapi.h>
#include <shellscalingapi.h>
#pragma comment(lib, "gdi32")
#pragma comment(lib, "dwmapi")
#pragma comment(lib, "UxTheme")
#pragma comment(lib, "ole32")
////////////////////////////////
//~ rjf: Globals
@@ -100,6 +111,10 @@ w32_allocate_window(void)
internal void
w32_free_window(W32_Window *window)
{
if(window->paint_arena != 0)
{
arena_release(window->paint_arena);
}
DestroyWindow(window->hwnd);
DLLRemove(w32_first_window, w32_last_window, window);
window->next = w32_first_free_window;
@@ -530,8 +545,19 @@ w32_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
case WM_SETCURSOR:
{
if(!w32_resizing &&
contains_2f32(os_client_rect_from_window(window_handle), os_mouse_from_window(window_handle)))
Rng2F32 window_rect = os_client_rect_from_window(window_handle);
Vec2F32 mouse = os_mouse_from_window(window_handle);
B32 on_border = 0;
DWORD window_style = window ? GetWindowLong(window->hwnd, GWL_STYLE) : 0;
B32 is_fullscreen = !(window_style & WS_OVERLAPPEDWINDOW);
if(window != 0 && window->custom_border && !is_fullscreen)
{
B32 on_border_x = (mouse.x <= window->custom_border_edge_thickness || window_rect.x1-window->custom_border_edge_thickness <= mouse.x);
B32 on_border_y = (mouse.y <= window->custom_border_edge_thickness || window_rect.y1-window->custom_border_edge_thickness <= mouse.y);
on_border = on_border_x || on_border_y;
}
if(!w32_resizing && !on_border &&
contains_2f32(window_rect, mouse))
{
SetCursor(w32_hcursor);
}
@@ -546,6 +572,226 @@ w32_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
F32 new_dpi = (F32)(wParam & 0xffff);
window->dpi = new_dpi;
}break;
//- rjf: [custom border]
case WM_NCPAINT:
{
if(window != 0 && window->custom_border && !window->custom_border_composition_enabled)
{
result = 0;
}
else
{
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
}break;
case WM_DWMCOMPOSITIONCHANGED:
{
if(window != 0 && window->custom_border)
{
BOOL enabled = 0;
DwmIsCompositionEnabled(&enabled);
window->custom_border_composition_enabled = enabled;
if(enabled)
{
MARGINS m = { 0, 0, 1, 0 };
DwmExtendFrameIntoClientArea(hwnd, &m);
DWORD dwmncrp_enabled = DWMNCRP_ENABLED;
DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &enabled, sizeof(dwmncrp_enabled));
}
}
else
{
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
}break;
case WM_WINDOWPOSCHANGED:
{
result = 0;
}break;
case WM_NCUAHDRAWCAPTION:
case WM_NCUAHDRAWFRAME:
{
// NOTE(rjf): undocumented messages for drawing themed window borders.
if(window != 0 && window->custom_border)
{
result = 0;
}
else
{
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
}break;
case WM_SETICON:
case WM_SETTEXT:
{
if(window && window->custom_border && !window->custom_border_composition_enabled)
{
// NOTE(rjf):
// https://blogs.msdn.microsoft.com/wpfsdk/2008/09/08/custom-window-chrome-in-wpf/
LONG_PTR old_style = GetWindowLongPtrW(hwnd, GWL_STYLE);
SetWindowLongPtrW(hwnd, GWL_STYLE, old_style & ~WS_VISIBLE);
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
SetWindowLongPtrW(hwnd, GWL_STYLE, old_style);
}
else
{
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
}break;
//- rjf: [custom border] activation - without this `result`, stuff flickers.
case WM_NCACTIVATE:
{
if(window == 0 || window->custom_border == 0)
{
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
else
{
result = DefWindowProcW(hwnd, uMsg, wParam, -1);
result = 1;
}
}break;
//- rjf: [custom border] client/window size calculation
case WM_NCCALCSIZE:
if(window != 0)
{
if(window->custom_border == 0)
{
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
else
{
MARGINS m = {0, 0, 0, 0};
RECT *r = (RECT *)lParam;
DWORD window_style = window ? GetWindowLong(window->hwnd, GWL_STYLE) : 0;
B32 is_fullscreen = !(window_style & WS_OVERLAPPEDWINDOW);
if(IsZoomed(hwnd) && !is_fullscreen)
{
int x_push_in = GetSystemMetrics(SM_CXFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER);
int y_push_in = GetSystemMetrics(SM_CYFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER);
r->left += x_push_in;
r->top += y_push_in;
r->bottom -= x_push_in;
r->right -= y_push_in;
m.cxLeftWidth = m.cxRightWidth = x_push_in;
m.cyTopHeight = m.cyBottomHeight = y_push_in;
}
DwmExtendFrameIntoClientArea(hwnd, &m);
}
}break;
//- rjf: [custom border] client/window hit testing (mapping mouse -> action)
case WM_NCHITTEST:
{
DWORD window_style = window ? GetWindowLong(window->hwnd, GWL_STYLE) : 0;
B32 is_fullscreen = !(window_style & WS_OVERLAPPEDWINDOW);
if(window == 0 || window->custom_border == 0 || is_fullscreen)
{
result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
else
{
POINT pos_monitor;
pos_monitor.x = GET_X_LPARAM(lParam);
pos_monitor.y = GET_Y_LPARAM(lParam);
POINT pos_client = pos_monitor;
ScreenToClient(hwnd, &pos_client);
//- rjf: check against window boundaries
RECT frame_rect;
GetWindowRect(hwnd, &frame_rect);
B32 is_over_window = (frame_rect.left <= pos_monitor.x && pos_monitor.x < frame_rect.right &&
frame_rect.top <= pos_monitor.y && pos_monitor.y < frame_rect.bottom);
//- rjf: check against borders
B32 is_over_left = 0;
B32 is_over_right = 0;
B32 is_over_top = 0;
B32 is_over_bottom = 0;
{
RECT rect;
GetClientRect(hwnd, &rect);
if(!IsZoomed(hwnd))
{
if(rect.left <= pos_client.x && pos_client.x < rect.left + window->custom_border_edge_thickness)
{
is_over_left = 1;
}
if(rect.right - window->custom_border_edge_thickness <= pos_client.x && pos_client.x < rect.right)
{
is_over_right = 1;
}
if(rect.bottom - window->custom_border_edge_thickness <= pos_client.y && pos_client.y < rect.bottom)
{
is_over_bottom = 1;
}
if(rect.top <= pos_client.y && pos_client.y < rect.top + window->custom_border_edge_thickness)
{
is_over_top = 1;
}
}
}
//- rjf: check against title bar
B32 is_over_title_bar = 0;
{
RECT rect;
GetClientRect(hwnd, &rect);
is_over_title_bar = (rect.left <= pos_client.x && pos_client.x < rect.right &&
rect.top <= pos_client.y && pos_client.y < rect.top + window->custom_border_title_thickness);
}
//- rjf: check against title bar client areas
B32 is_over_title_bar_client_area = 0;
for(W32_TitleBarClientArea *area = window->first_title_bar_client_area;
area != 0;
area = area->next)
{
Rng2F32 rect = area->rect;
if(rect.x0 <= pos_client.x && pos_client.x < rect.x1 &&
rect.y0 <= pos_client.y && pos_client.y < rect.y1)
{
is_over_title_bar_client_area = 1;
break;
}
}
//- rjf: resolve hovering to result
result = HTNOWHERE;
if(is_over_window)
{
// rjf: default to client area
result = HTCLIENT;
// rjf: title bar
if(is_over_title_bar)
{
result = HTCAPTION;
}
// rjf: title bar client area
if(is_over_title_bar_client_area)
{
result = HTCLIENT;
}
// rjf: normal edges
if(is_over_left) { result = HTLEFT; }
if(is_over_right) { result = HTRIGHT; }
if(is_over_top) { result = HTTOP; }
if(is_over_bottom) { result = HTBOTTOM; }
// rjf: corners
if(is_over_left && is_over_top) { result = HTTOPLEFT; }
if(is_over_left && is_over_bottom) { result = HTBOTTOMLEFT; }
if(is_over_right && is_over_top) { result = HTTOPRIGHT; }
if(is_over_right && is_over_bottom) { result = HTBOTTOMRIGHT; }
}
}
}break;
}
}
@@ -580,7 +826,7 @@ os_graphical_init(void)
//- rjf: set dpi awareness
w32_SetProcessDpiAwarenessContext_Type *SetProcessDpiAwarenessContext_func = 0;
HMODULE module = LoadLibraryA("user32.dll");
if (module != 0)
if(module != 0)
{
SetProcessDpiAwarenessContext_func =
(w32_SetProcessDpiAwarenessContext_Type*)GetProcAddress(module, "SetProcessDpiAwarenessContext");
@@ -588,7 +834,7 @@ os_graphical_init(void)
(w32_GetDpiForWindow_Type*)GetProcAddress(module, "GetDpiForWindow");
FreeLibrary(module);
}
if (SetProcessDpiAwarenessContext_func != 0)
if(SetProcessDpiAwarenessContext_func != 0)
{
SetProcessDpiAwarenessContext_func(w32_DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
@@ -601,6 +847,7 @@ os_graphical_init(void)
wndclass.lpszClassName = L"graphical-window";
wndclass.hCursor = LoadCursorA(0, IDC_ARROW);
wndclass.hIcon = LoadIcon(w32_h_instance, MAKEINTRESOURCE(1));
wndclass.style = CS_HREDRAW|CS_VREDRAW|CS_OWNDC|CS_DBLCLKS;
ATOM wndatom = RegisterClassExW(&wndclass);
(void)wndatom;
}
@@ -667,17 +914,17 @@ os_get_clipboard_text(Arena *arena)
//~ rjf: @os_hooks Windows (Implemented Per-OS)
internal OS_Handle
os_window_open(Vec2F32 resolution, String8 title)
os_window_open(Vec2F32 resolution, OS_WindowFlags flags, String8 title)
{
//- rjf: make hwnd
HWND hwnd = 0;
{
Temp scratch = scratch_begin(0, 0);
String16 title16 = str16_from_8(scratch.arena, title);
hwnd = CreateWindowExW(0,
hwnd = CreateWindowExW(WS_EX_APPWINDOW,
L"graphical-window",
(WCHAR*)title16.str,
WS_OVERLAPPEDWINDOW,
WS_OVERLAPPEDWINDOW | WS_SIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT,
(int)resolution.x,
(int)resolution.y,
@@ -699,8 +946,23 @@ os_window_open(Vec2F32 resolution, String8 title)
}
}
//- rjf: early detection of composition
{
BOOL enabled = 0;
DwmIsCompositionEnabled(&enabled);
window->custom_border_composition_enabled = enabled;
}
//- rjf: custom border
if(flags & OS_WindowFlag_CustomBorder)
{
window->custom_border = 1;
window->paint_arena = arena_alloc();
}
//- rjf: convert to handle + return
return os_window_from_w32_window(window);
OS_Handle result = os_window_from_w32_window(window);
return result;
}
internal void
@@ -825,6 +1087,16 @@ os_window_set_maximized(OS_Handle handle, B32 maximized)
}
}
internal void
os_window_minimize(OS_Handle handle)
{
W32_Window *window = w32_window_from_os_window(handle);
if(window != 0)
{
ShowWindow(window->hwnd, SW_MINIMIZE);
}
}
internal void
os_window_bring_to_front(OS_Handle handle)
{
@@ -856,6 +1128,48 @@ os_window_set_monitor(OS_Handle window_handle, OS_Handle monitor)
}
}
internal void
os_window_clear_custom_border_data(OS_Handle handle)
{
W32_Window *window = w32_window_from_os_window(handle);
if(window->custom_border)
{
arena_clear(window->paint_arena);
window->first_title_bar_client_area = window->last_title_bar_client_area = 0;
window->custom_border_title_thickness = 0;
window->custom_border_edge_thickness = 0;
}
}
internal void
os_window_push_custom_title_bar(OS_Handle handle, F32 thickness)
{
W32_Window *window = w32_window_from_os_window(handle);
window->custom_border_title_thickness = thickness;
}
internal void
os_window_push_custom_edges(OS_Handle handle, F32 thickness)
{
W32_Window *window = w32_window_from_os_window(handle);
window->custom_border_edge_thickness = thickness;
}
internal void
os_window_push_custom_title_bar_client_area(OS_Handle handle, Rng2F32 rect)
{
W32_Window *window = w32_window_from_os_window(handle);
if(window->custom_border)
{
W32_TitleBarClientArea *area = push_array(window->paint_arena, W32_TitleBarClientArea, 1);
if(area != 0)
{
area->rect = rect;
SLLQueuePush(window->first_title_bar_client_area, window->last_title_bar_client_area, area);
}
}
}
internal Rng2F32
os_rect_from_window(OS_Handle handle)
{
+21
View File
@@ -7,9 +7,23 @@
#pragma comment(lib, "user32")
#pragma comment(lib, "gdi32")
#ifndef WM_NCUAHDRAWCAPTION
#define WM_NCUAHDRAWCAPTION (0x00AE)
#endif
#ifndef WM_NCUAHDRAWFRAME
#define WM_NCUAHDRAWFRAME (0x00AF)
#endif
////////////////////////////////
//~ rjf: Windows
typedef struct W32_TitleBarClientArea W32_TitleBarClientArea;
struct W32_TitleBarClientArea
{
W32_TitleBarClientArea *next;
Rng2F32 rect;
};
typedef struct W32_Window W32_Window;
struct W32_Window
{
@@ -22,6 +36,13 @@ struct W32_Window
F32 dpi;
B32 first_paint_done;
B32 maximized;
B32 custom_border;
F32 custom_border_title_thickness;
F32 custom_border_edge_thickness;
B32 custom_border_composition_enabled;
Arena *paint_arena;
W32_TitleBarClientArea *first_title_bar_client_area;
W32_TitleBarClientArea *last_title_bar_client_area;
};
////////////////////////////////