From 08dc9c445d36963e2502301026e0e58a959b5aa6 Mon Sep 17 00:00:00 2001 From: Allen Webster Date: Fri, 29 Sep 2023 17:28:10 -0700 Subject: [PATCH] init --- .gitignore | 1 + build.bat | 11 + project.4coder | 31 ++ win32_custom_window.c | 869 +++++++++++++++++++++++++++++++++++++++ win32_custom_window_mc.c | 584 ++++++++++++++++++++++++++ 5 files changed, 1496 insertions(+) create mode 100644 .gitignore create mode 100644 build.bat create mode 100644 project.4coder create mode 100644 win32_custom_window.c create mode 100644 win32_custom_window_mc.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d163863 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..fc2f537 --- /dev/null +++ b/build.bat @@ -0,0 +1,11 @@ +@echo off + +set libs=User32.lib Dwmapi.lib Gdi32.lib UxTheme.lib + +set opts=-FC -GR- -EHa- -nologo -Zi +set code=%cd% + +if not exist build ( mkdir build ) +pushd build +cl %opts% %code%\win32_custom_window.c %libs% -Fecustom_window +popd diff --git a/project.4coder b/project.4coder new file mode 100644 index 0000000..ee56298 --- /dev/null +++ b/project.4coder @@ -0,0 +1,31 @@ +version(1); +project_name = "custom_window"; +patterns = { +"*.c", +"*.cpp", +"*.h", +"*.m", +"*.bat", +"*.sh", +"*.4coder", +}; +blacklist_patterns = { +".*", +}; +load_paths_base = { + { ".", .relative = true, .recursive = true, }, +}; +load_paths = { + { load_paths_base, .os = "win", }, +}; + +command_list = { + { .name = "build", + .out = "*compilation*", .footer_panel = true, .save_dirty_files = true, + .cmd = { { "build.bat" , .os = "win" }, }, }, + { .name = "run", + .out = "*run*", .footer_panel = false, .save_dirty_files = false, + .cmd = { { "build\\custom_window", .os = "win" }, }, }, +}; +fkey_command[1] = "build"; +fkey_command[2] = "run"; diff --git a/win32_custom_window.c b/win32_custom_window.c new file mode 100644 index 0000000..3c34f41 --- /dev/null +++ b/win32_custom_window.c @@ -0,0 +1,869 @@ +/* +** Win32 Custom Window Example Program +** v1.3.0 - May 8th 2020 +** by Allen Webster allenwebster@4coder.net +** +** public domain example program +** NO WARRANTY IMPLIED; USE AT YOUR OWN RISK +** +** +** Tested on: Windows 7 aero, Windows 7 classic, Windows 10 +** +** +** This is an example only, it is designed for: +** Easy copy-pasting the main components +** Learn how and why it works +** Build, step through, modify, experiment +** +** This example is not designed for use as a library/engine/framework etc. +** +** +** This example *does show* how to create a win32 window where the client area +** covers the whole area of the window and is an axis aligned rectangle. It shows +** how to maintain normal window resizing, moving, and positioning shortcuts like +** WinKey + Arrows for such a window. And it shows how to handle widgets that are +** placed inside the caption area of the window. +** +** This example *does not show* non-rectangular windows or transparency. It does +** not show a fully abstracted win32 main loop for an application or game. It does +** not show graphics context setup. +** +** +** Most important points: +** CustomBorderWindowProc +** CreateWindowW +** +** +** Required windows libraries: +** User32.lib UxTheme.lib Dwmapi.lib +** Pragmas versions: +** #pragma comment(lib, "User32.lib") +** #pragma comment(lib, "UxTheme.lib") +** #pragma comment(lib, "Dwmapi.lib") +** +** Libraries only for rendering, not required: +** Gdi32.lib +** +** +** Additional contributions from: +** Martins Mozeiko +** +*/ + +#include +#include + +// only for vsync, not necessary in copies: +#include + +// only for logging, not necessary in copies: +#include + +//////////////////////////////// + +// For simplicity this example statically defines the border and caption width. +// These determine the size of the area where resizing and moving the window +// is possible. There is no reason in principle why these have to be statically +// determined or why these would be the only options in setting up the border +// shape. + +int caption_width = 30; +int border_width = 10; + +//////////////////////////////// + +// These fascilitate communication between the message handling loop and the +// update function. + +// The application has a way to report widgets that overlap the caption of the +// window so that clicking them will not move the window. A detailed discussion +// of this problem can be found at WM_NCHITTEST. + +// With 100% control of how the border looks it is nice to be able to determine +// if the window is active so that it's border can respond with some kind of +// visual que to being inactive. + +// Besides those points the details of this aren't specific to a custom window +// they are just mimicking a simple system layer <-> application layer interface. + +int WindowIsActive(void); +void StopRunning(void); +void Minimize(HWND hwnd); +void ToggleMaximize(HWND hwnd); +void EmbeddedWidgetRect(RECT rect); + +typedef enum{ + InputEventKind_MouseLeftPress, + InputEventKind_MouseLeftRelease, +} Input_Event_Kind; + +typedef struct Input_Event Input_Event; +struct Input_Event{ + Input_Event *next; + Input_Event_Kind kind; +}; + +typedef struct Input Input; +struct Input{ + Input_Event *first_event; + Input_Event *last_event; + int mouse_x; + int mouse_y; + int left; +}; + +void UpdateAndRender(HWND hwnd, Input *input); + +//////////////////////////////// + +// A handy helper for this example + +int +HitTest(int x, int y, RECT rect){ + return((rect.left <= x && x < rect.right) && (rect.top <= y && y < rect.bottom)); +} + +//////////////////////////////// + +// Implementation for the pseudo system layer. + +int keep_running = 0; +int window_is_active = 1; + +int minimize_at_end_of_update = 0; +int toggle_maximize_at_end_of_update = 0; + +int +WindowIsActive(void){ + return(window_is_active); +} +void +StopRunning(void){ + keep_running = 0; +} +void +Minimize(HWND hwnd){ + minimize_at_end_of_update = 1; +} +void +ToggleMaximize(HWND hwnd){ + toggle_maximize_at_end_of_update = 1; +} + +#define MAX_EMBEDDED_WIDGETS 128 +int embedded_widget_count = 0; +RECT embedded_widget_rect[MAX_EMBEDDED_WIDGETS]; +void +EmbeddedWidgetRect(RECT rect){ + if (embedded_widget_count < MAX_EMBEDDED_WIDGETS){ + embedded_widget_rect[embedded_widget_count] = rect; + embedded_widget_count += 1; + } +} + +//////////////////////////////// + +BOOL composition_enabled; + +#define MAX_INPUT_EVENT_COUNT 128 +int input_event_cursor = 0; +Input_Event input_event_memory[MAX_INPUT_EVENT_COUNT]; +Input input; + +Input_Event* +PushEvent(void){ + Input_Event *result = 0; + if (input_event_cursor < MAX_INPUT_EVENT_COUNT){ + result = &input_event_memory[input_event_cursor]; + input_event_cursor += 1; + if (input.first_event == 0){ + input.first_event = result; + } + if (input.last_event != 0){ + input.last_event->next = result; + } + input.last_event = result; + } + return(result); +} + +// The WindowProc callback does most of the work for a custom boredred window. +// The messages that start with "WM_NC" deal with processing the non-client +// area of the window. The main goal is to leave window moving and resizing +// up to the OS while taking control of the rendering and border widgets. +LRESULT +CustomBorderWindowProc(HWND hwnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam){ + LRESULT result = 0; + switch (uMsg){ + // The WM_NCCALCSIZE is used to establish the mapping from the window's + // non-client area to the client area. The non-client area is passed to + // this code through a pointer in lParam that points to the RECT type. + // Then this code indicates the mapping to a client area by modifying + // that RECT. + + // The WM_NCCALCSIZE documentation mentions a circumstance where wParam + // is true and another where wParam is false, and the possibility that + // lParam points to the type NCCALCSIZE_PARAMS. It turns out that the + // first member of NCCALCSIZE_PARAMS is a RECT, and that if the rest + // of the NCCALCSIZE_PARAMS structure is ignored then this message + // behaves the same way in eitehr case. NCCALCSIZE_PARAMS offers + // additional features that are not not used here. + + // The main strategy is to leave the client area the same as the + // non-client area by not modifying the RECT at all. This way the whole + // window will be considered client area and the rendering of the border + // is just the same as rendering anything else inside the window. + + // However there is a circumstance when this strategy doesn't work. + // When a window is maximized the OS will automatically allow the + // window's area hang a little outside of the monitor. When a window + // uses the default border this makes it so that the maximized window + // doesn't show it's border. This behavior can't be disabled but + // it is possible nullify the effect by querying how wide the overhang + // area will be and pushing the non-client area in by that amount. + + // Pushing in the client area gives the render target for the application + // the correct area, but it still leaves an unrendered artifact hanging + // outside of the window, which is visible for users with multiple + // monitors. Calling DwmExtendFrameIntoClientArea addresses this. Normally + // this call widens the area thw window paints as part of border by + // pushing in the interior of the window frame. In this case where border + // rendering is in application code it just pushes in the area where + // the overhang artifact is visible to the point where it is removed. + case WM_NCCALCSIZE: + { + MARGINS m = { 0, 0, 0, 0 }; + + RECT* r = (RECT*)lParam; + + // A convenient function for checking if a window is maximized. + if (IsZoomed(hwnd)){ + + 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; + } + + if (composition_enabled){ + DwmExtendFrameIntoClientArea(hwnd, &m); + } + + }break; + + // TODO(allen): This message relates to Windows 7 and Vista where the DWM mode + // can change. I need to experiment with it before leaving commentary. + case WM_DWMCOMPOSITIONCHANGED: + { + DwmIsCompositionEnabled(&composition_enabled); + if (composition_enabled){ + MARGINS m = { 0, 0, 0, 0 }; + if (IsZoomed(hwnd)){ + int x_push_in = GetSystemMetrics(SM_CXFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER); + int y_push_in = GetSystemMetrics(SM_CYFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER); + m.cxLeftWidth = m.cxRightWidth = x_push_in; + m.cyTopHeight = m.cyBottomHeight = y_push_in; + } + DwmExtendFrameIntoClientArea(hwnd, &m); + } + }break; + + + // The WM_NCACTIVATE message is sent to a window before the WM_ACTIVATE + // message. When a window uses the default border this message is used + // to repaint the border to indicate the change of status to active or + // inactive. This would paint over the custom window border. This code + // simply overrides that behavior with tracking the window state. It is + // still necessary to return true from this message so that normal window + // activation handling continues and so that the WM_ACTIVATE message is + // still sent. + + // The documentation for WM_NCACTIVATE says without explanation: + // "If the window is minimized when this message is received, the + // application should pass the message to the DefWindowProc function" + // This code follows this instruction, but for extra caution passes + // -1 instead of lParam as the docs suggest this will prevent it from + // rendering. + case WM_NCACTIVATE: + { + result = 1; + window_is_active = wParam; + // A convenient function for checking if a window is minimized. + if (IsIconic(hwnd)){ + result = DefWindowProcW(hwnd, uMsg, wParam, -1); + } + }break; + + // The WM_NCHITTEST message is used to map points on the window to + // a symbol indicating the "behavior" of that point. The possible + // "behaviors" include resizing, moving, clicking close, minimize, + // and maximize buttons, being outside of the window, and being + // inside the client area. + + // In some versions of Windows using the behaviors for the various + // buttons causes unwanted rendering for the buttons, so those + // are not used in this custom window border. Luckily there is not + // much difficulty in recreating the buttons. + + // There is one tricky problem with handling this message. The problem + // occurs if your custom border embeds any widgets along the area that + // would be considered the "caption" of the window which really means + // the area that can be grabbed to move the window around. For example + // the close, minimize, and maximize buttons are widgets embedded in + // the caption. Generally any widget that overlaps any part of the + // area that would be for moving the window. + // + // When a point is inside an embedded widget this message should return + // HTCLIENT so that normal mouse events are sent to the application layer. + // The problem is that the application is going to be in charge of placing + // those buttons, so there must be a strategy for getting that information + // back to this message which expects an immediate response, not a response + // later on in the frame. + + // Some ideas of strategies to consider: + + // 1. Just don't have any embedded widgets. + // + // > This works but it does mean you're officially giving up on replacing + // the normal close, minimize, maximize buttons. + + // 2. Only allow embedded widgets that immediately move the window when + // clicked so that there is no chance of accidentally moving the + // window when trying to use the widgets. + // + // > This works but is very restricting, and requires buttons that + // respond on mouse down immediately instead of mouse up. + + // 3. Pre-define the embedded widget layout and have both the application + // layer and platform layer depend on this pre-defined layout rule. + // + // > This works but it does couple the platform layer to things that could + // have been purely application controlled concepts. + + // 4. Have the application layer set a "widget rectangle list" when it + // updates and always use the most recent one + // + // > This works but it is also the most work to set up in the platform + // layer and requires the update function to do new work too. + + // Certainly there are more possible strategies, this list is just mean + // to offer a few ideas, mostly to clarify the nature of the problem. + + // This example is using strategy 4. + + case WM_NCHITTEST: + { + POINT pos; + pos.x = GET_X_LPARAM(lParam); + pos.y = GET_Y_LPARAM(lParam); + + // Make sure the point is inside of the window + RECT frame_rect; + GetWindowRect(hwnd, &frame_rect); + if (!HitTest(pos.x, pos.y, frame_rect)){ + result = HTNOWHERE; + } + + else{ + + RECT rect; + GetClientRect(hwnd, &rect); + ScreenToClient(hwnd, &pos); + + // Check each border + int l = 0; + int r = 0; + int b = 0; + int t = 0; + if (!IsZoomed(hwnd)){ + if (rect.left <= pos.x && pos.x < rect.left + border_width){ + l = 1; + } + if (rect.right - border_width <= pos.x && pos.x < rect.right){ + r = 1; + } + if (rect.bottom - border_width <= pos.y && pos.y < rect.bottom){ + b = 1; + } + if (rect.top <= pos.y && pos.y < rect.top + border_width){ + t = 1; + } + } + + // If the point is in two borders, use the corresponding corner resize. + // If the point is in just one border, use the corresponding side resize. + if (l){ + if (t){ + result = HTTOPLEFT; + } + else if (b){ + result = HTBOTTOMLEFT; + } + else{ + result = HTLEFT; + } + } + else if (r){ + if (t){ + result = HTTOPRIGHT; + } + else if (b){ + result = HTBOTTOMRIGHT; + } + else{ + result = HTRIGHT; + } + } + else if (t){ + result = HTTOP; + } + else if (b){ + result = HTBOTTOM; + } + + // Here the point must be further inside the window than the resize + // borders, so the final options are the window moving area (caption) + // and the client area. + else{ + if (rect.top <= pos.y && pos.y < rect.top + caption_width){ + result = HTCAPTION; + // Check the application defined widget areas + for (int i = 0; i < embedded_widget_count; i += 1){ + if (HitTest(pos.x, pos.y, embedded_widget_rect[i])){ + result = HTCLIENT; + break; + } + } + } + else{ + result = HTCLIENT; + } + } + } + + }break; + + // This is only included to emphasize that there is no reason to handle + // any of these WM_NC* messages. They give the window the opportunity + // to handle mouse events in the non-client area before generating the + // equivalent client area versions. If WM_NCHITTEST returns HTCLIENT + // then the default handling for these is to generate the client area + // version of the message. If WM_NCHITTEST generates one of the symbols, + // HTLEFT, HTTOP, ... HTTOPLEFT ... HTCAPTION, then it determined that + // the OS should provide default handling for a resize or move operation. + // In either case, these should just do their default behavior. +#if 0 + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONUP: + case WM_NCLBUTTONDBLCLK: + case WM_NCMBUTTONDBLCLK: + case WM_NCMBUTTONDOWN: + case WM_NCMBUTTONUP: + case WM_NCMOUSEHOVER: + case WM_NCMOUSELEAVE: + case WM_NCMOUSEMOVE: + case WM_NCRBUTTONDBLCLK: + case WM_NCRBUTTONDOWN: + case WM_NCRBUTTONUP: + case WM_NCXBUTTONDBLCLK: + case WM_NCXBUTTONDOWN: + case WM_NCXBUTTONUP: + { + result = DefWindowProcW(hwnd, uMsg, wParam, lParam); + }break; +#endif + + case WM_CLOSE: + { + keep_running = 0; + }break; + + case WM_SIZING: + { + InvalidateRect(hwnd, 0, 0); + }break; + + // It's always a good idea to do *something* with the WM_PAINT message + // so that your window looks better when resizing, but in this case it's + // especially important. Remember your code creates the border graphics + // now, so if you don't render anything when the window is reszing then + // there will be nothing at all to indicate that the window is changing + // size until the resize finishes. + case WM_PAINT: + { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + embedded_widget_count = 0; + UpdateAndRender(hwnd, 0); + EndPaint(hwnd, &ps); + }break; + + case WM_LBUTTONDOWN: + { + Input_Event *event = PushEvent(); + if (event != 0){ + event->kind = InputEventKind_MouseLeftPress; + } + }break; + + case WM_LBUTTONUP: + { + Input_Event *event = PushEvent(); + if (event != 0){ + event->kind = InputEventKind_MouseLeftRelease; + } + }break; + + default: + { + result = DefWindowProcW(hwnd, uMsg, wParam, lParam); + }break; + } + return(result); +} + +int +WinMain(HINSTANCE hInstance, + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nShowCmd){ + + // With CustomBorderWindowProc done, creating the window is not + // that much more work, and is almost identical to creating a + // default window. + +#define WINDOW_CLASS L"MainWindow" + + // Nothing about the window class differs from a default window. + WNDCLASSW window_class = {0}; + window_class.style = 0; + window_class.lpfnWndProc = CustomBorderWindowProc; + window_class.hInstance = hInstance; + window_class.hCursor = LoadCursorA(0, IDC_ARROW); + window_class.lpszClassName = WINDOW_CLASS; + + ATOM atom = RegisterClassW(&window_class); + if (atom == 0){ + fprintf(stderr, "RegisterClassW failed\n"); + exit(1); + } + + // One key aspect is making sure these styles are present: + // WS_SIZEBOX, WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_CAPTION, WM_SYSMENU + // Because these styles are needed for making shortcuts and + // default resize and moving behavior work, even when the border + // itself is overriden. These are exactly equivalent to the combined style: + // WS_OVERLAPPEDWINDOW + DWORD style = WS_OVERLAPPEDWINDOW; + HWND hwnd = CreateWindowW(WINDOW_CLASS, + L"Window Name", + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + 0, + 0, + hInstance, + 0); + if (hwnd == 0){ + fprintf(stderr, "CreateWindowExW failed\n"); + exit(1); + } + + // The other key aspect is SetWindowTheme. Using SetWindowTheme with single + // spaces effectively disables an theme on the window. Doing this is critical + // for removing special shapes, shading, etc. from the default window on + // any given version of Windows. + SetWindowTheme(hwnd, L" ", L" "); + + + // A convenient way to get vsync since this example doesn't have a more + // sophistcated graphics context. + if (DwmIsCompositionEnabled(&composition_enabled) != S_OK){ + fprintf(stderr, "DwmIsCompositionEnabled failed\n"); + composition_enabled = 0; + } + fprintf(stdout, "Has compositor? %s\n", composition_enabled?"Yes":"No"); + + ShowWindow(hwnd, SW_SHOW); + + keep_running = 1; + for (;keep_running;){ + input.first_event = 0; + input.last_event = 0; + input_event_cursor = 0; + + // Personal preference - I like to put anything that resizes the window + // from my code after the main update so that I can assume that the + // size never changes during the update. This also happens to be a + // convenient way to make sure I immediately redo my layout and render + // after a size change. + + MSG msg; + + if (minimize_at_end_of_update){ + ShowWindow(hwnd, SW_MINIMIZE); + } + else if (toggle_maximize_at_end_of_update){ + if (IsZoomed(hwnd)){ + ShowWindow(hwnd, SW_RESTORE); + } + else{ + ShowWindow(hwnd, SW_MAXIMIZE); + } + } + else{ + GetMessage(&msg, 0, 0, 0); + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + for (;PeekMessageW(&msg, 0, 0, 0, PM_REMOVE);){ + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + POINT mpos; + GetCursorPos(&mpos); + ScreenToClient(hwnd, &mpos); + input.mouse_x = mpos.x; + input.mouse_y = mpos.y; + input.left = (GetKeyState(VK_LBUTTON)&(1 << 15)); + + minimize_at_end_of_update = 0; + toggle_maximize_at_end_of_update = 0; + embedded_widget_count = 0; + UpdateAndRender(hwnd, &input); + + // This can be whatever vsync or frame rate limiting method you'd + // like or none at all. + if (composition_enabled){ + DwmFlush(); + } + else{ + Sleep(33); + } + } + + return(0); +} + +//////////////////////////////// + +// The implementation of the application tick function which renders the border, +// inside of the window, and embedded widgets, and processes input for both the +// border and the interior through a single input system. + +int +HasEvent(Input *input, Input_Event_Kind kind){ + int result = 0; + if (input != 0){ + Input_Event **ptr_to = &input->first_event; + Input_Event *last = 0; + for (Input_Event *event = input->first_event, *next = 0; + event != 0; + event = next){ + next = event->next; + if (event->kind == kind){ + result = 1; + *ptr_to = next; + if (event == input->last_event){ + input->last_event = last; + } + break; + } + else{ + ptr_to = &event->next; + last = event; + } + } + } + return(result); +} + +typedef enum{ + Widget_None, + Widget_Close, + Widget_Minimize, + Widget_Maximize, + Widget_Slider, +} Widget; + +Widget click_started = Widget_None; +int slider_x = 0; +int delta_from_mouse_x = 0; + +void +UpdateAndRender(HWND hwnd, Input *input){ + RECT window_rect; + GetClientRect(hwnd, &window_rect); + + RECT inside_rect; + inside_rect.top = window_rect.top + caption_width; + if (!IsZoomed(hwnd)){ + inside_rect.left = window_rect.left + border_width; + inside_rect.right = window_rect.right - border_width; + inside_rect.bottom = window_rect.bottom - border_width; + } + else{ + inside_rect.left = window_rect.left; + inside_rect.right = window_rect.right; + inside_rect.bottom = window_rect.bottom; + } + + HDC dc = GetDC(hwnd); + { + SelectObject(dc, GetStockObject(DC_PEN)); + SelectObject(dc, GetStockObject(DC_BRUSH)); + + // Window border + { + if (WindowIsActive()){ + SetDCPenColor(dc, RGB(255, 200, 0)); + SetDCBrushColor(dc, RGB(255, 200, 0)); + } + else{ + SetDCPenColor(dc, RGB(128, 100, 0)); + SetDCBrushColor(dc, RGB(128, 100, 0)); + } + + Rectangle(dc, window_rect.left, window_rect.top, window_rect.right, inside_rect.top); + Rectangle(dc, window_rect.left, inside_rect.bottom, window_rect.right, window_rect.bottom); + Rectangle(dc, window_rect.left, inside_rect.top, inside_rect.left, inside_rect.bottom); + Rectangle(dc, inside_rect.right, inside_rect.top, window_rect.right, inside_rect.bottom); + } + + // Close button + if (window_rect.right > 20){ + RECT button; + button.left = window_rect.right - 20; + button.top = window_rect.top + 10; + button.right = window_rect.right - 10; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(180, 0, 0)); + SetDCBrushColor(dc, RGB(180, 0, 0)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0 && HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Close; + } + if (click_started == Widget_Close && HasEvent(input, InputEventKind_MouseLeftRelease)){ + StopRunning(); + } + } + } + + // Minimize button + if (window_rect.right > 40){ + RECT button; + button.left = window_rect.right - 40; + button.top = window_rect.top + 10; + button.right = window_rect.right - 30; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(0, 0, 180)); + SetDCBrushColor(dc, RGB(0, 0, 180)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0 && HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Minimize; + } + if (click_started == Widget_Minimize && HasEvent(input, InputEventKind_MouseLeftRelease)){ + Minimize(hwnd); + } + } + } + + // Maximize button + if (window_rect.right > 60){ + RECT button; + button.left = window_rect.right - 60; + button.top = window_rect.top + 10; + button.right = window_rect.right - 50; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(0, 180, 0)); + SetDCBrushColor(dc, RGB(0, 180, 0)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0 && HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Maximize; + } + if (click_started == Widget_Maximize && HasEvent(input, InputEventKind_MouseLeftRelease)){ + ToggleMaximize(hwnd); + } + } + } + + // Slider + if (window_rect.right > 200){ + RECT track; + track.left = window_rect.right - 195; + track.top = window_rect.top + 14; + track.right = window_rect.right - 95; + track.bottom = window_rect.top + 16; + + SetDCPenColor(dc, RGB(0, 0, 0)); + SetDCBrushColor(dc, RGB(0, 0, 0)); + Rectangle(dc, track.left, track.top, track.right, track.bottom); + + RECT button; + button.left = window_rect.right - 200 + slider_x; + button.top = window_rect.top + 10; + button.right = window_rect.right - 190 + slider_x; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(100, 200, 200)); + SetDCBrushColor(dc, RGB(100, 200, 200)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0){ + if (HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Slider; + delta_from_mouse_x = slider_x - input->mouse_x; + } + } + if (click_started == Widget_Slider){ + slider_x = delta_from_mouse_x + input->mouse_x; + if (slider_x < 0){ + slider_x = 0; + } + if (slider_x > 100){ + slider_x = 100; + } + } + } + } + + if (input != 0 && !input->left){ + click_started = Widget_None; + } + + // Inside area + { + SetDCPenColor(dc, RGB(127, 127, 127)); + SetDCBrushColor(dc, RGB(127, 127, 127)); + Rectangle(dc, inside_rect.left, inside_rect.top, inside_rect.right, inside_rect.bottom); + } + } + ReleaseDC(hwnd, dc); +} + diff --git a/win32_custom_window_mc.c b/win32_custom_window_mc.c new file mode 100644 index 0000000..e877aeb --- /dev/null +++ b/win32_custom_window_mc.c @@ -0,0 +1,584 @@ +/* +** Win32 Custom Window Example Program +** v1.2 - April 30th 2020 +** by Allen Webster allenwebster@4coder.net +** +** public domain example program +** NO WARRANTY IMPLIED; USE AT YOUR OWN RISK +** +** Minimal comments version +** +*/ + +#include +#include +#include +#include + +//////////////////////////////// + +int caption_width = 30; +int border_width = 10; + +//////////////////////////////// + +int WindowIsActive(void); +void StopRunning(void); +void Minimize(HWND hwnd); +void ToggleMaximize(HWND hwnd); +void EmbeddedWidgetRect(RECT rect); + +typedef enum{ + InputEventKind_MouseLeftPress, + InputEventKind_MouseLeftRelease, +} Input_Event_Kind; + +typedef struct Input_Event Input_Event; +struct Input_Event{ + Input_Event *next; + Input_Event_Kind kind; +}; + +typedef struct Input Input; +struct Input{ + Input_Event *first_event; + Input_Event *last_event; + int mouse_x; + int mouse_y; + int left; +}; + +void UpdateAndRender(HWND hwnd, Input *input); + +//////////////////////////////// + +int +HitTest(int x, int y, RECT rect){ + return((rect.left <= x && x < rect.right) && (rect.top <= y && y < rect.bottom)); +} + +//////////////////////////////// + +int keep_running = 0; +int window_is_active = 1; + +int minimize_at_end_of_update = 0; +int toggle_maximize_at_end_of_update = 0; + +int +WindowIsActive(void){ + return(window_is_active); +} +void +StopRunning(void){ + keep_running = 0; +} +void +Minimize(HWND hwnd){ + minimize_at_end_of_update = 1; +} +void +ToggleMaximize(HWND hwnd){ + toggle_maximize_at_end_of_update = 1; +} + +#define MAX_EMBEDDED_WIDGETS 128 +int embedded_widget_count = 0; +RECT embedded_widget_rect[MAX_EMBEDDED_WIDGETS]; +void +EmbeddedWidgetRect(RECT rect){ + if (embedded_widget_count < MAX_EMBEDDED_WIDGETS){ + embedded_widget_rect[embedded_widget_count] = rect; + embedded_widget_count += 1; + } +} + +//////////////////////////////// + +#define MAX_INPUT_EVENT_COUNT 128 +int input_event_cursor = 0; +Input_Event input_event_memory[MAX_INPUT_EVENT_COUNT]; +Input input; + +Input_Event* +PushEvent(void){ + Input_Event *result = 0; + if (input_event_cursor < MAX_INPUT_EVENT_COUNT){ + result = &input_event_memory[input_event_cursor]; + input_event_cursor += 1; + if (input.first_event == 0){ + input.first_event = result; + } + if (input.last_event != 0){ + input.last_event->next = result; + } + input.last_event = result; + } + return(result); +} + +LRESULT +CustomBorderWindowProc(HWND hwnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam){ + LRESULT result = 0; + switch (uMsg){ + case WM_NCCALCSIZE: + { + RECT* r = (RECT*)lParam; + if (IsZoomed(hwnd)){ + int x_push_in = GetSystemMetrics(SM_CYFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER); + int y_push_in = GetSystemMetrics(SM_CXFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER); + r->top += x_push_in; + r->left += y_push_in; + r->right -= x_push_in; + r->bottom -= y_push_in; + } + }break; + + case WM_NCACTIVATE: + { + result = 1; + window_is_active = wParam; + // A convenient function for checking if a window is minimized. + if (IsIconic(hwnd)){ + result = DefWindowProcW(hwnd, uMsg, wParam, -1); + } + }break; + + case WM_NCHITTEST: + { + POINT pos; + pos.x = GET_X_LPARAM(lParam); + pos.y = GET_Y_LPARAM(lParam); + + RECT frame_rect; + GetWindowRect(hwnd, &frame_rect); + if (!HitTest(pos.x, pos.y, frame_rect)){ + result = HTNOWHERE; + } + + else{ + + RECT rect; + GetClientRect(hwnd, &rect); + ScreenToClient(hwnd, &pos); + + // Borders + int l = 0; + int r = 0; + int b = 0; + int t = 0; + if (!IsZoomed(hwnd)){ + if (rect.left <= pos.x && pos.x < rect.left + border_width){ + l = 1; + } + if (rect.right - border_width <= pos.x && pos.x < rect.right){ + r = 1; + } + if (rect.bottom - border_width <= pos.y && pos.y < rect.bottom){ + b = 1; + } + if (rect.top <= pos.y && pos.y < rect.top + border_width){ + t = 1; + } + } + if (l){ + if (t){ + result = HTTOPLEFT; + } + else if (b){ + result = HTBOTTOMLEFT; + } + else{ + result = HTLEFT; + } + } + else if (r){ + if (t){ + result = HTTOPRIGHT; + } + else if (b){ + result = HTBOTTOMRIGHT; + } + else{ + result = HTRIGHT; + } + } + else if (t){ + result = HTTOP; + } + else if (b){ + result = HTBOTTOM; + } + + // Inside + else{ + if (rect.top <= pos.y && pos.y < rect.top + caption_width){ + result = HTCAPTION; + for (int i = 0; i < embedded_widget_count; i += 1){ + if (HitTest(pos.x, pos.y, embedded_widget_rect[i])){ + result = HTCLIENT; + break; + } + } + } + else{ + result = HTCLIENT; + } + } + } + + }break; + + case WM_CLOSE: + { + keep_running = 0; + }break; + + case WM_SIZING: + { + InvalidateRect(hwnd, 0, 0); + }break; + + case WM_PAINT: + { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + embedded_widget_count = 0; + UpdateAndRender(hwnd, 0); + EndPaint(hwnd, &ps); + }break; + + case WM_LBUTTONDOWN: + { + Input_Event *event = PushEvent(); + if (event != 0){ + event->kind = InputEventKind_MouseLeftPress; + } + }break; + + case WM_LBUTTONUP: + { + Input_Event *event = PushEvent(); + if (event != 0){ + event->kind = InputEventKind_MouseLeftRelease; + } + }break; + + default: + { + result = DefWindowProcW(hwnd, uMsg, wParam, lParam); + }break; + } + return(result); +} + +int +WinMain(HINSTANCE hInstance, + HINSTANCE hPrevInstance, + LPSTR lpCmdLine, + int nShowCmd){ + +#define WINDOW_CLASS L"MainWindow" + + WNDCLASSW window_class = {0}; + window_class.style = 0; + window_class.lpfnWndProc = CustomBorderWindowProc; + window_class.hInstance = hInstance; + window_class.hCursor = LoadCursorA(0, IDC_ARROW); + window_class.lpszClassName = WINDOW_CLASS; + + ATOM atom = RegisterClassW(&window_class); + if (atom == 0){ + fprintf(stderr, "RegisterClassW failed\n"); + exit(1); + } + + DWORD style = WS_OVERLAPPEDWINDOW; + HWND hwnd = CreateWindowW(WINDOW_CLASS, + L"Window Name", + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + 0, + 0, + hInstance, + 0); + if (hwnd == 0){ + fprintf(stderr, "CreateWindowExW failed\n"); + exit(1); + } + + SetWindowTheme(hwnd, L" ", L" "); + + + BOOL composition_enabled; + if (DwmIsCompositionEnabled(&composition_enabled) != S_OK){ + fprintf(stderr, "DwmIsCompositionEnabled failed\n"); + composition_enabled = 0; + } + fprintf(stdout, "Has compositor? %s\n", composition_enabled?"Yes":"No"); + + ShowWindow(hwnd, SW_SHOW); + + keep_running = 1; + for (;keep_running;){ + input.first_event = 0; + input.last_event = 0; + input_event_cursor = 0; + + MSG msg; + + if (minimize_at_end_of_update){ + ShowWindow(hwnd, SW_MINIMIZE); + } + else if (toggle_maximize_at_end_of_update){ + if (IsZoomed(hwnd)){ + ShowWindow(hwnd, SW_RESTORE); + } + else{ + ShowWindow(hwnd, SW_MAXIMIZE); + } + } + else{ + GetMessage(&msg, 0, 0, 0); + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + for (;PeekMessageW(&msg, 0, 0, 0, PM_REMOVE);){ + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + POINT mpos; + GetCursorPos(&mpos); + ScreenToClient(hwnd, &mpos); + input.mouse_x = mpos.x; + input.mouse_y = mpos.y; + input.left = (GetKeyState(VK_LBUTTON)&(1 << 15)); + + minimize_at_end_of_update = 0; + toggle_maximize_at_end_of_update = 0; + embedded_widget_count = 0; + UpdateAndRender(hwnd, &input); + + if (composition_enabled){ + DwmFlush(); + } + else{ + Sleep(33); + } + } + + return(0); +} + +//////////////////////////////// + +int +HasEvent(Input *input, Input_Event_Kind kind){ + int result = 0; + if (input != 0){ + Input_Event **ptr_to = &input->first_event; + Input_Event *last = 0; + for (Input_Event *event = input->first_event, *next = 0; + event != 0; + event = next){ + next = event->next; + if (event->kind == kind){ + result = 1; + *ptr_to = next; + break; + } + else{ + ptr_to = &event->next; + last = event; + } + } + input->last_event = last; + } + return(result); +} + +typedef enum{ + Widget_None, + Widget_Close, + Widget_Minimize, + Widget_Maximize, + Widget_Slider, +} Widget; + +Widget click_started = Widget_None; +int slider_x = 0; +int delta_from_mouse_x = 0; + +void +UpdateAndRender(HWND hwnd, Input *input){ + RECT window_rect; + GetClientRect(hwnd, &window_rect); + + RECT inside_rect; + inside_rect.top = window_rect.top + caption_width; + if (!IsZoomed(hwnd)){ + inside_rect.left = window_rect.left + border_width; + inside_rect.right = window_rect.right - border_width; + inside_rect.bottom = window_rect.bottom - border_width; + } + else{ + inside_rect.left = window_rect.left; + inside_rect.right = window_rect.right; + inside_rect.bottom = window_rect.bottom; + } + + HDC dc = GetDC(hwnd); + { + SelectObject(dc, GetStockObject(DC_PEN)); + SelectObject(dc, GetStockObject(DC_BRUSH)); + + // Window border + { + if (WindowIsActive()){ + SetDCPenColor(dc, RGB(255, 200, 0)); + SetDCBrushColor(dc, RGB(255, 200, 0)); + } + else{ + SetDCPenColor(dc, RGB(128, 100, 0)); + SetDCBrushColor(dc, RGB(128, 100, 0)); + } + + Rectangle(dc, window_rect.left, window_rect.top, window_rect.right, inside_rect.top); + Rectangle(dc, window_rect.left, inside_rect.bottom, window_rect.right, window_rect.bottom); + Rectangle(dc, window_rect.left, inside_rect.top, inside_rect.left, inside_rect.bottom); + Rectangle(dc, inside_rect.right, inside_rect.top, window_rect.right, inside_rect.bottom); + } + + // Close button + if (window_rect.right > 20){ + RECT button; + button.left = window_rect.right - 20; + button.top = window_rect.top + 10; + button.right = window_rect.right - 10; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(180, 0, 0)); + SetDCBrushColor(dc, RGB(180, 0, 0)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0 && HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Close; + } + if (click_started == Widget_Close && HasEvent(input, InputEventKind_MouseLeftRelease)){ + StopRunning(); + } + } + } + + // Minimize button + if (window_rect.right > 40){ + RECT button; + button.left = window_rect.right - 40; + button.top = window_rect.top + 10; + button.right = window_rect.right - 30; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(0, 0, 180)); + SetDCBrushColor(dc, RGB(0, 0, 180)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0 && HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Minimize; + } + if (click_started == Widget_Minimize && HasEvent(input, InputEventKind_MouseLeftRelease)){ + Minimize(hwnd); + } + } + } + + // Maximize button + if (window_rect.right > 60){ + RECT button; + button.left = window_rect.right - 60; + button.top = window_rect.top + 10; + button.right = window_rect.right - 50; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(0, 180, 0)); + SetDCBrushColor(dc, RGB(0, 180, 0)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0 && HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Maximize; + } + if (click_started == Widget_Maximize && HasEvent(input, InputEventKind_MouseLeftRelease)){ + ToggleMaximize(hwnd); + } + } + } + + // Slider + if (window_rect.right > 200){ + RECT track; + track.left = window_rect.right - 195; + track.top = window_rect.top + 14; + track.right = window_rect.right - 95; + track.bottom = window_rect.top + 16; + + SetDCPenColor(dc, RGB(0, 0, 0)); + SetDCBrushColor(dc, RGB(0, 0, 0)); + Rectangle(dc, track.left, track.top, track.right, track.bottom); + + RECT button; + button.left = window_rect.right - 200 + slider_x; + button.top = window_rect.top + 10; + button.right = window_rect.right - 190 + slider_x; + button.bottom = window_rect.top + 20; + + SetDCPenColor(dc, RGB(100, 200, 200)); + SetDCBrushColor(dc, RGB(100, 200, 200)); + + EmbeddedWidgetRect(button); + Rectangle(dc, button.left, button.top, button.right, button.bottom); + if (input != 0){ + if (HitTest(input->mouse_x, input->mouse_y, button)){ + if (HasEvent(input, InputEventKind_MouseLeftPress)){ + click_started = Widget_Slider; + delta_from_mouse_x = slider_x - input->mouse_x; + } + } + if (click_started == Widget_Slider){ + slider_x = delta_from_mouse_x + input->mouse_x; + if (slider_x < 0){ + slider_x = 0; + } + if (slider_x > 100){ + slider_x = 100; + } + } + } + } + + if (input != 0 && !input->left){ + click_started = Widget_None; + } + + // Inside area + { + SetDCPenColor(dc, RGB(127, 127, 127)); + SetDCBrushColor(dc, RGB(127, 127, 127)); + Rectangle(dc, inside_rect.left, inside_rect.top, inside_rect.right, inside_rect.bottom); + } + } + ReleaseDC(hwnd, dc); +} +