WoW Model Viewer
Your premiere tool for viewing, equipping and animating World of Warcraft models.
Loading...
Searching...
No Matches
CustomTitleBar.cpp
Go to the documentation of this file.
1// ---- Custom Title Bar (Windows implementation) ----------------------------
2// Subclasses the Win32 window to remove the native title bar and handles
3// WM_NCCALCSIZE / WM_NCHITTEST so the window can still be dragged, resized
4// and snapped. The visible title bar is drawn entirely with Dear ImGui.
5//
6// On non-Windows platforms every function is a thin wrapper around the
7// standard ImGui main-menu bar.
8
9#ifdef _WIN32
10#include <windows.h>
11#include <dwmapi.h>
12#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM
13#pragma comment(lib, "dwmapi.lib")
14#endif
15
16#include "CustomTitleBar.h"
17
18#include "imgui.h"
19#include "imgui_internal.h"
20#include <GLFW/glfw3.h>
21#ifdef _WIN32
22#define GLFW_EXPOSE_NATIVE_WIN32
23#include <GLFW/glfw3native.h>
24#endif
25
26#include <algorithm>
27#include <cmath>
28#include <filesystem>
29
30// ---------------------------------------------------------------------------
31// Module-level state
32// ---------------------------------------------------------------------------
33namespace
34{
35
36#ifdef _WIN32
37 WNDPROC g_origWndProc = nullptr;
38 HWND g_hwnd = nullptr;
39
40 // The pixel-rect of the window-control buttons (min/max/close) in
41 // client coordinates. Anything inside this rect returns HTCLIENT so
42 // that ImGui handles the click instead of Windows treating it as a
43 // caption drag.
44 RECT g_controlButtonsRect = {};
45
46 // The pixel-rect of the menu items. Also returns HTCLIENT.
47 RECT g_menuRect = {};
48#endif
49
50 // The height of the custom title bar in logical (unscaled) pixels.
51 // Updated every frame in begin().
52 float g_titleBarHeight = 0.0f;
53
54 // Separate ImFont for the Segoe MDL2 Assets caption-button icons,
55 // loaded at a larger pixel size than the UI text font.
56 ImFont* g_iconFont = nullptr;
57 bool g_hasIconFont = false;
58
59} // anonymous namespace
60
61// ---------------------------------------------------------------------------
62// Win32 custom frame
63// ---------------------------------------------------------------------------
64#ifdef _WIN32
65
66static LRESULT CALLBACK customWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
67{
68 switch (msg)
69 {
70 // ---- Remove the default non-client frame ----
71 case WM_NCCALCSIZE:
72 {
73 if (wParam == TRUE)
74 {
75 auto* params = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
76
77 // When maximised, the OS inflates the window by the frame thickness
78 // so edges extend beyond the monitor. Compensate so the content
79 // stays within the work area.
80 if (IsZoomed(hwnd))
81 {
82 HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
83 MONITORINFO mi{};
84 mi.cbSize = sizeof(mi);
85 GetMonitorInfo(mon, &mi);
86 params->rgrc[0] = mi.rcWork;
87 }
88 return 0; // returning 0 removes the default title bar
89 }
90 break;
91 }
92
93 // ---- Hit-testing: resize borders + caption drag ----
94 case WM_NCHITTEST:
95 {
96 const int x = GET_X_LPARAM(lParam);
97 const int y = GET_Y_LPARAM(lParam);
98
99 RECT rc;
100 GetWindowRect(hwnd, &rc);
101
102 // Resize borders are only active when the window is not maximised.
103 if (!IsZoomed(hwnd))
104 {
105 // Resize-border thickness (use the real system metric)
106 const int border = GetSystemMetrics(SM_CXSIZEFRAME) +
107 GetSystemMetrics(SM_CXPADDEDBORDER);
108
109 // --- Resize borders (corners first, then edges) ---
110 const bool onLeft = x < rc.left + border;
111 const bool onRight = x > rc.right - border;
112 const bool onTop = y < rc.top + border;
113 const bool onBottom = y > rc.bottom - border;
114
115 if (onTop && onLeft) return HTTOPLEFT;
116 if (onTop && onRight) return HTTOPRIGHT;
117 if (onBottom && onLeft) return HTBOTTOMLEFT;
118 if (onBottom && onRight) return HTBOTTOMRIGHT;
119 if (onLeft) return HTLEFT;
120 if (onRight) return HTRIGHT;
121 if (onTop) return HTTOP;
122 if (onBottom) return HTBOTTOM;
123 }
124
125 // --- Title bar area ---
126 // Convert to client coordinates for comparison with ImGui rects.
127 // GLFW reports physical pixels via GetClientRect/ClientToScreen,
128 // and ImGui uses those directly, so no DPI scaling is needed.
129 POINT pt = { x, y };
130 ScreenToClient(hwnd, &pt);
131
132 const int titleH = static_cast<int>(g_titleBarHeight);
133
134 if (pt.y < titleH)
135 {
136 // If the cursor is over the window-control buttons or the menu,
137 // let ImGui handle the interaction.
138 if (PtInRect(&g_controlButtonsRect, pt) || PtInRect(&g_menuRect, pt))
139 return HTCLIENT;
140
141 // Everything else in the title-bar strip is draggable caption.
142 return HTCAPTION;
143 }
144
145 return HTCLIENT;
146 }
147
148 // Preserve the thin shadow / border around the window even without a
149 // visible non-client frame.
150 case WM_ACTIVATE:
151 {
152 MARGINS m = { 0, 0, 0, 1 };
153 DwmExtendFrameIntoClientArea(hwnd, &m);
154 break;
155 }
156
157 default:
158 break;
159 }
160
161 return CallWindowProc(g_origWndProc, hwnd, msg, wParam, lParam);
162}
163
164#endif // _WIN32
165
166// ---------------------------------------------------------------------------
167// Public API
168// ---------------------------------------------------------------------------
169void CustomTitleBar::init(GLFWwindow* window)
170{
171#ifdef _WIN32
172 if (!window)
173 return;
174
175 g_hwnd = glfwGetWin32Window(window);
176 if (!g_hwnd)
177 return;
178
179 // Subclass the window
180 g_origWndProc = reinterpret_cast<WNDPROC>(
181 SetWindowLongPtr(g_hwnd, GWLP_WNDPROC,
182 reinterpret_cast<LONG_PTR>(customWndProc)));
183
184 // Extend the client area so we get the DWM drop-shadow
185 MARGINS m = { 0, 0, 0, 1 };
186 DwmExtendFrameIntoClientArea(g_hwnd, &m);
187
188 // Disable Windows 11 rounded corners so the window matches UE5's
189 // sharp-cornered look. DWMWA_WINDOW_CORNER_PREFERENCE (33) with
190 // DWMWCP_DONOTROUND (1).
191 constexpr DWORD DWMWA_WINDOW_CORNER_PREFERENCE_VAL = 33;
192 constexpr DWORD DWMWCP_DONOTROUND_VAL = 1;
193 DwmSetWindowAttribute(g_hwnd, DWMWA_WINDOW_CORNER_PREFERENCE_VAL,
194 &DWMWCP_DONOTROUND_VAL, sizeof(DWMWCP_DONOTROUND_VAL));
195
196 // Force the frame to be recalculated so the title bar disappears
197 SetWindowPos(g_hwnd, nullptr, 0, 0, 0, 0,
198 SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
199 SWP_NOZORDER | SWP_NOOWNERZORDER);
200#else
201 (void)window;
202#endif
203}
204
205bool CustomTitleBar::begin([[maybe_unused]] GLFWwindow* window)
206{
207#ifdef _WIN32
208 // We draw a full-width window at the top that acts as the title bar.
209 // Use ImGuiWindowFlags_MenuBar so we can embed menus inside it.
210 const ImGuiViewport* vp = ImGui::GetMainViewport();
211
212 // Height: menu-bar frame padding * 2 + font size + a little extra for
213 // the window-control buttons so they feel comfortable.
214 const float frameH = ImGui::GetFrameHeight();
215 g_titleBarHeight = frameH + ImGui::GetStyle().FramePadding.y * 2.0f;
216
217 ImGui::SetNextWindowPos(vp->WorkPos);
218 ImGui::SetNextWindowSize(ImVec2(vp->WorkSize.x, g_titleBarHeight));
219
220 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
221 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
222 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
223 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_MenuBarBg));
224
225 ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
226 ImGuiWindowFlags_NoResize |
227 ImGuiWindowFlags_NoMove |
228 ImGuiWindowFlags_NoScrollbar |
229 ImGuiWindowFlags_NoSavedSettings |
230 ImGuiWindowFlags_NoDocking |
231 ImGuiWindowFlags_MenuBar |
232 ImGuiWindowFlags_NoBringToFrontOnFocus;
233
234 ImGui::Begin("##CustomTitleBar", nullptr, flags);
235 ImGui::PopStyleColor(1);
236 ImGui::PopStyleVar(3);
237
238 // Begin the embedded menu bar
239 if (ImGui::BeginMenuBar())
240 {
241 // Record the menu starting position so we can track its extent.
242 // GLFW/ImGui coordinates are already in the same physical-pixel
243 // space as ScreenToClient, so no DPI scaling is needed.
244 const float menuStartX = ImGui::GetCursorScreenPos().x - vp->Pos.x;
245 g_menuRect.left = static_cast<LONG>(menuStartX);
246 g_menuRect.top = 0;
247 g_menuRect.bottom = static_cast<LONG>(g_titleBarHeight);
248 return true;
249 }
250
251 ImGui::End();
252 return false;
253#else
254 return ImGui::BeginMainMenuBar();
255#endif
256}
257
258void CustomTitleBar::end([[maybe_unused]] GLFWwindow* window, const char* statusText)
259{
260#ifdef _WIN32
261 // Finish recording how wide the menus are (in client coords)
262 {
263 const float menuEndX = ImGui::GetCursorScreenPos().x - ImGui::GetMainViewport()->Pos.x;
264 g_menuRect.right = static_cast<LONG>(menuEndX);
265 }
266
267 // ---- Right-aligned section: status text + window controls ----
268 const ImGuiViewport* vp = ImGui::GetMainViewport();
269
270 // Button dimensions
271 const float btnW = ImGui::GetFrameHeight() * 1.6f;
272 const float btnH = g_titleBarHeight;
273 const float controlsWidth = btnW * 3.0f; // min + max + close
274
275 // Status text
276 if (statusText && statusText[0])
277 {
278 const float textW = ImGui::CalcTextSize(statusText).x;
279 const float availX = vp->WorkSize.x - controlsWidth - 20.0f;
280 if (availX > textW + 20.0f)
281 {
282 ImGui::SameLine(availX - textW);
283 ImGui::TextDisabled("%s", statusText);
284 }
285 }
286
287 // Position the control buttons at the far right
288 const float startX = vp->WorkSize.x - controlsWidth;
289 ImGui::SameLine(startX);
290
291 // Record the control-button rect for hit-testing (in client coords).
292 // Use the actual ImGui cursor position so the rect matches exactly.
293 {
294 const float btnLeft = ImGui::GetCursorScreenPos().x - vp->Pos.x;
295 g_controlButtonsRect.left = static_cast<LONG>(btnLeft);
296 g_controlButtonsRect.top = 0;
297 g_controlButtonsRect.right = static_cast<LONG>(btnLeft + controlsWidth);
298 g_controlButtonsRect.bottom = static_cast<LONG>(g_titleBarHeight);
299 }
300
301 // Minimise / Maximise-Restore / Close — flat, frameless buttons
302 // Override FrameBorderSize so the active theme's border doesn't leak in.
303 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
304 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
305 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
306 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
307 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1, 1, 1, 0.15f));
308 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1, 1, 1, 0.25f));
309
310 // Switch to the dedicated icon font (larger than the UI text font)
311 // so the caption-button glyphs match native Windows / UE5 proportions.
312 if (g_iconFont)
313 ImGui::PushFont(g_iconFont);
314
315 // Segoe MDL2 Assets glyph labels (UTF-8 encoded):
316 // U+E921 = ChromeMinimize U+E922 = ChromeMaximize
317 // U+E923 = ChromeRestore U+E8BB = ChromeClose
318 // Fallback to simple ASCII if the icon font wasn't loaded.
319 const char* lblMin = g_hasIconFont ? "\xEE\xA4\xA1##min" : " - ##min";
320 const char* lblMax = g_hasIconFont ? "\xEE\xA4\xA2##max" : " [] ##max";
321 const char* lblRest = g_hasIconFont ? "\xEE\xA4\xA3##max" : " = ##max";
322 const char* lblClose = g_hasIconFont ? "\xEE\xA2\xBB##close" : " X ##close";
323
324 // Minimise
325 if (ImGui::Button(lblMin, ImVec2(btnW, btnH)))
326 {
327 if (g_hwnd) ShowWindow(g_hwnd, SW_MINIMIZE);
328 }
329
330 // Maximise / Restore
331 ImGui::SameLine();
332 const bool isMaximised = g_hwnd && IsZoomed(g_hwnd);
333 if (ImGui::Button(isMaximised ? lblRest : lblMax, ImVec2(btnW, btnH)))
334 {
335 if (g_hwnd) ShowWindow(g_hwnd, isMaximised ? SW_RESTORE : SW_MAXIMIZE);
336 }
337
338 // Close — red hover
339 ImGui::SameLine();
340 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.90f, 0.18f, 0.18f, 1.0f));
341 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.75f, 0.10f, 0.10f, 1.0f));
342 if (ImGui::Button(lblClose, ImVec2(btnW, btnH)))
343 {
344 if (window) glfwSetWindowShouldClose(window, GLFW_TRUE);
345 }
346 ImGui::PopStyleColor(2); // close-specific colours
347
348 if (g_iconFont)
349 ImGui::PopFont();
350
351 ImGui::PopStyleColor(3); // button base colours
352 ImGui::PopStyleVar(3); // FrameRounding, FrameBorderSize, ItemSpacing
353
354 ImGui::EndMenuBar();
355 ImGui::End();
356
357 // ---- 1px window border (UE5 style) ----
358 // Draw a thin border around the entire viewport using the foreground
359 // draw list so it renders on top of all ImGui content. Skip when
360 // maximised because the window edges are off-screen.
361 if (!IsZoomed(g_hwnd))
362 {
363 ImDrawList* fg = ImGui::GetForegroundDrawList();
364 const ImVec2 p0 = vp->Pos;
365 const ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
366 const ImU32 borderCol = ImGui::GetColorU32(ImGuiCol_Border);
367 fg->AddRect(p0, p1, borderCol, 0.0f, 0, 1.0f);
368 }
369
370#else
371 // Non-Windows fallback: standard main menu bar
372 if (statusText && statusText[0])
373 {
374 float textWidth = ImGui::CalcTextSize(statusText).x;
375 ImGui::SameLine(ImGui::GetWindowWidth() - textWidth - 10.0f);
376 ImGui::TextDisabled("%s", statusText);
377 }
378 ImGui::EndMainMenuBar();
379#endif
380}
381
383{
384 return g_titleBarHeight;
385}
386
387void CustomTitleBar::mergeIconFont(float pixelSize)
388{
389#ifdef _WIN32
390 // Segoe MDL2 Assets ships with Windows 10/11 and contains the standard
391 // Chrome caption-button icons used by Explorer, VS, etc.
392 const char* fontPath = "C:\\Windows\\Fonts\\segmdl2.ttf";
393 if (!std::filesystem::exists(fontPath))
394 {
395 g_iconFont = nullptr;
396 g_hasIconFont = false;
397 return;
398 }
399
400 // Only rasterise the four glyphs we actually use.
401 static const ImWchar ranges[] = { 0xE8BB, 0xE8BB, 0xE921, 0xE923, 0 };
402
403 ImFontConfig cfg;
404 cfg.PixelSnapH = true;
405 // Loaded as a separate font so PushFont/PopFont controls the size
406 // independently of the UI text. 0.55x gives proportions matching
407 // the native Windows / UE5 caption buttons.
408 const float iconSize = pixelSize * 0.55f;
409 // Segoe MDL2's ascender is tall relative to the glyph's visual centre,
410 // so nudge the icons up to sit in the middle of the button.
411 cfg.GlyphOffset.y = -std::round(iconSize * 0.4f);
412
413 g_iconFont = ImGui::GetIO().Fonts->AddFontFromFileTTF(
414 fontPath, iconSize, &cfg, ranges);
415 g_hasIconFont = (g_iconFont != nullptr);
416#else
417 (void)pixelSize;
418 g_iconFont = nullptr;
419 g_hasIconFont = false;
420#endif
421}
void end(GLFWwindow *window, const char *statusText)
bool begin(GLFWwindow *window)
float height() noexcept
Title bar height in logical pixels (call after begin()).
void init(GLFWwindow *window)
void mergeIconFont(float pixelSize)