basalt-win32 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,562 @@
1
+ #include "Win32TitleBar.h"
2
+
3
+ #include "RnWin32View.h"
4
+ #include "Win32Strings.h"
5
+
6
+ #include <d2d1.h>
7
+ #include <dwmapi.h>
8
+ #include <windowsx.h>
9
+ #include <wrl/client.h>
10
+
11
+ #include <algorithm>
12
+ #include <cmath>
13
+ #include <utility>
14
+
15
+ namespace basalt {
16
+
17
+ namespace {
18
+
19
+ using win32::CaptionButton;
20
+
21
+ // The DWM attributes, by value: older SDK headers do not name the colour ones,
22
+ // and the numbers are Windows' rather than the header's to change.
23
+ constexpr DWORD kUseImmersiveDarkMode = 20; // DWMWA_USE_IMMERSIVE_DARK_MODE
24
+ constexpr DWORD kBorderColor = 34; // DWMWA_BORDER_COLOR
25
+ constexpr DWORD kCaptionColor = 35; // DWMWA_CAPTION_COLOR
26
+ constexpr DWORD kTextColor = 36; // DWMWA_TEXT_COLOR
27
+ constexpr COLORREF kColorDefault = 0xFFFFFFFF; // DWMWA_COLOR_DEFAULT
28
+
29
+ UINT dpiOf(HWND window) {
30
+ const UINT dpi = window != nullptr ? GetDpiForWindow(window) : 96;
31
+ return dpi == 0 ? 96 : dpi;
32
+ }
33
+
34
+ // The resize band along an edge: the frame and the padding Windows adds to it.
35
+ int resizeBorder(UINT dpi) {
36
+ return GetSystemMetricsForDpi(SM_CYFRAME, dpi) + GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
37
+ }
38
+
39
+ LRESULT hitCodeFor(CaptionButton button) {
40
+ switch (button) {
41
+ case CaptionButton::Minimize:
42
+ return HTMINBUTTON;
43
+ case CaptionButton::Maximize:
44
+ return HTMAXBUTTON;
45
+ case CaptionButton::Close:
46
+ return HTCLOSE;
47
+ case CaptionButton::None:
48
+ break;
49
+ }
50
+ return HTNOWHERE;
51
+ }
52
+
53
+ CaptionButton buttonForHitCode(WPARAM code) {
54
+ switch (code) {
55
+ case HTMINBUTTON:
56
+ return CaptionButton::Minimize;
57
+ case HTMAXBUTTON:
58
+ return CaptionButton::Maximize;
59
+ case HTCLOSE:
60
+ return CaptionButton::Close;
61
+ default:
62
+ return CaptionButton::None;
63
+ }
64
+ }
65
+
66
+ D2D1_COLOR_F toColor(COLORREF color, float alpha = 1.0f) {
67
+ return D2D1::ColorF(static_cast<float>(GetRValue(color)) / 255.0f,
68
+ static_cast<float>(GetGValue(color)) / 255.0f,
69
+ static_cast<float>(GetBValue(color)) / 255.0f,
70
+ alpha);
71
+ }
72
+
73
+ void setDwmColor(HWND window, DWORD attribute, std::optional<COLORREF> color) {
74
+ const COLORREF value = color.value_or(kColorDefault);
75
+ // Fails on Windows 10, which has no such attributes. That is the documented
76
+ // way to find out, and a caption that stays the system's colour is the right
77
+ // outcome there.
78
+ DwmSetWindowAttribute(window, attribute, &value, sizeof(value));
79
+ }
80
+
81
+ } // namespace
82
+
83
+ Win32TitleBar &titleBar() {
84
+ static Win32TitleBar instance;
85
+ return instance;
86
+ }
87
+
88
+ void Win32TitleBar::attach(HWND window) {
89
+ window_ = window;
90
+ applyTitle();
91
+ const BOOL dark = dark_ ? TRUE : FALSE;
92
+ DwmSetWindowAttribute(window_, kUseImmersiveDarkMode, &dark, sizeof(dark));
93
+ applyColors();
94
+ publishMetrics();
95
+ }
96
+
97
+ void Win32TitleBar::setDefaultTitle(const std::string &title) {
98
+ defaultTitle_ = title;
99
+ applyTitle();
100
+ }
101
+
102
+ void Win32TitleBar::setTitle(std::optional<std::string> title) {
103
+ title_ = std::move(title);
104
+ applyTitle();
105
+ }
106
+
107
+ void Win32TitleBar::applyTitle() {
108
+ if (window_ == nullptr) {
109
+ return;
110
+ }
111
+ const std::string &title = title_.has_value() && !title_->empty() ? *title_ : defaultTitle_;
112
+ SetWindowTextW(window_, win32::widen(title).c_str());
113
+ }
114
+
115
+ void Win32TitleBar::setDarkMode(bool dark) {
116
+ if (dark == dark_) {
117
+ return;
118
+ }
119
+ dark_ = dark;
120
+ if (window_ == nullptr) {
121
+ return;
122
+ }
123
+ const BOOL value = dark ? TRUE : FALSE;
124
+ DwmSetWindowAttribute(window_, kUseImmersiveDarkMode, &value, sizeof(value));
125
+ redrawFrame();
126
+ invalidateButtons();
127
+ }
128
+
129
+ void Win32TitleBar::setColors(std::optional<COLORREF> caption,
130
+ std::optional<COLORREF> text,
131
+ std::optional<COLORREF> border) {
132
+ captionColor_ = caption;
133
+ textColor_ = text;
134
+ borderColor_ = border;
135
+ applyColors();
136
+ invalidateButtons();
137
+ }
138
+
139
+ void Win32TitleBar::applyColors() {
140
+ if (window_ == nullptr) {
141
+ return;
142
+ }
143
+ setDwmColor(window_, kCaptionColor, captionColor_);
144
+ setDwmColor(window_, kTextColor, textColor_);
145
+ setDwmColor(window_, kBorderColor, borderColor_);
146
+ }
147
+
148
+ void Win32TitleBar::setStyle(TitleBarStyle style) {
149
+ if (style == style_) {
150
+ return;
151
+ }
152
+ style_ = style;
153
+ hovered_ = CaptionButton::None;
154
+ pressed_ = CaptionButton::None;
155
+ // WM_NCCALCSIZE is only asked again when the frame is said to have changed,
156
+ // which is what gives the caption back to the client area or takes it away.
157
+ redrawFrame();
158
+ publishMetrics();
159
+ invalidateButtons();
160
+ }
161
+
162
+ void Win32TitleBar::redrawFrame() {
163
+ if (window_ != nullptr) {
164
+ SetWindowPos(window_,
165
+ nullptr,
166
+ 0,
167
+ 0,
168
+ 0,
169
+ 0,
170
+ SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
171
+ }
172
+ }
173
+
174
+ win32::CaptionMetrics Win32TitleBar::captionMetrics() const {
175
+ return win32::captionMetricsForDpi(dpiOf(window_));
176
+ }
177
+
178
+ TitleBarMetrics Win32TitleBar::metrics() const {
179
+ std::lock_guard<std::mutex> lock(mutex_);
180
+ return metrics_;
181
+ }
182
+
183
+ void Win32TitleBar::setMetricsListener(MetricsListener listener) {
184
+ std::lock_guard<std::mutex> lock(mutex_);
185
+ listener_ = std::move(listener);
186
+ }
187
+
188
+ void Win32TitleBar::refreshMetrics() {
189
+ publishMetrics();
190
+ }
191
+
192
+ void Win32TitleBar::publishMetrics() {
193
+ TitleBarMetrics next;
194
+ next.style = style_;
195
+ if (style_ == TitleBarStyle::Hidden) {
196
+ const auto caption = captionMetrics();
197
+ next.height = caption.height;
198
+ next.buttonsWidth = 3 * caption.buttonWidth;
199
+ }
200
+
201
+ MetricsListener listener;
202
+ {
203
+ std::lock_guard<std::mutex> lock(mutex_);
204
+ const bool changed = next.height != metrics_.height ||
205
+ next.buttonsWidth != metrics_.buttonsWidth || next.style != metrics_.style;
206
+ metrics_ = next;
207
+ if (!changed) {
208
+ return;
209
+ }
210
+ listener = listener_;
211
+ }
212
+ if (listener) {
213
+ listener(next);
214
+ }
215
+ }
216
+
217
+ bool Win32TitleBar::handleNcCalcSize(WPARAM wparam, LPARAM lparam, LRESULT &result) {
218
+ if (style_ != TitleBarStyle::Hidden || window_ == nullptr || wparam != TRUE) {
219
+ return false;
220
+ }
221
+ auto *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(lparam);
222
+ const LONG top = params->rgrc[0].top;
223
+
224
+ // Windows takes its usual frame off all four sides, and then the top is given
225
+ // back: the side and bottom borders stay -- and with them resizing from those
226
+ // edges, and the window's shadow -- while the caption becomes client area.
227
+ result = DefWindowProc(window_, WM_NCCALCSIZE, wparam, lparam);
228
+ params->rgrc[0].top = top;
229
+
230
+ // A maximised window is placed with its frame hanging past the edges of the
231
+ // monitor. The sides and bottom are still taken off above, but the top would
232
+ // now start that far above the screen, and the app's header with it.
233
+ if (IsZoomed(window_)) {
234
+ params->rgrc[0].top += resizeBorder(dpiOf(window_));
235
+ }
236
+ return true;
237
+ }
238
+
239
+ bool Win32TitleBar::handleNcHitTest(LPARAM lparam, win32::RnWin32View *root, LRESULT &result) {
240
+ if (style_ != TitleBarStyle::Hidden || window_ == nullptr) {
241
+ return false;
242
+ }
243
+
244
+ // The side and bottom edges are still Windows' own frame, and so is its
245
+ // answer about them.
246
+ const LRESULT byWindows = DefWindowProc(window_, WM_NCHITTEST, 0, lparam);
247
+ if (byWindows != HTCLIENT) {
248
+ result = byWindows;
249
+ return true;
250
+ }
251
+
252
+ POINT point{GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
253
+ ScreenToClient(window_, &point);
254
+ RECT client{};
255
+ GetClientRect(window_, &client);
256
+ const float width = static_cast<float>(client.right - client.left);
257
+ const UINT dpi = dpiOf(window_);
258
+
259
+ // The top edge became client area when the caption was given back, so
260
+ // resizing from it has to be offered here. Not while maximised, when there
261
+ // is no edge to drag.
262
+ if (!IsZoomed(window_)) {
263
+ const int border = resizeBorder(dpi);
264
+ if (point.y < border) {
265
+ if (point.x < border) {
266
+ result = HTTOPLEFT;
267
+ } else if (point.x >= static_cast<LONG>(width) - border) {
268
+ result = HTTOPRIGHT;
269
+ } else {
270
+ result = HTTOP;
271
+ }
272
+ return true;
273
+ }
274
+ }
275
+
276
+ const CaptionButton button = win32::captionButtonAt(
277
+ static_cast<float>(point.x), static_cast<float>(point.y), width, captionMetrics());
278
+ if (button != CaptionButton::None) {
279
+ result = hitCodeFor(button);
280
+ return true;
281
+ }
282
+
283
+ if (root != nullptr &&
284
+ win32::isTitleBarDragRegionAt(root, static_cast<float>(point.x), static_cast<float>(point.y))) {
285
+ result = HTCAPTION;
286
+ return true;
287
+ }
288
+
289
+ result = HTCLIENT;
290
+ return true;
291
+ }
292
+
293
+ bool Win32TitleBar::handleNcMouse(UINT message, WPARAM wparam, LRESULT &result) {
294
+ if (style_ != TitleBarStyle::Hidden || window_ == nullptr) {
295
+ return false;
296
+ }
297
+
298
+ switch (message) {
299
+ case WM_NCMOUSEMOVE: {
300
+ const CaptionButton button = buttonForHitCode(wparam);
301
+ if (button != hovered_) {
302
+ hovered_ = button;
303
+ invalidateButtons();
304
+ }
305
+ // Leaving the non-client area for the client area is otherwise silent,
306
+ // and a button would stay lit after the pointer had gone.
307
+ if (!trackingLeave_) {
308
+ TRACKMOUSEEVENT track{sizeof(track), TME_LEAVE | TME_NONCLIENT, window_, 0};
309
+ trackingLeave_ = TrackMouseEvent(&track) != FALSE;
310
+ }
311
+ if (button == CaptionButton::None) {
312
+ return false;
313
+ }
314
+ result = 0;
315
+ return true;
316
+ }
317
+
318
+ case WM_NCMOUSELEAVE:
319
+ trackingLeave_ = false;
320
+ if (hovered_ != CaptionButton::None || pressed_ != CaptionButton::None) {
321
+ hovered_ = CaptionButton::None;
322
+ pressed_ = CaptionButton::None;
323
+ invalidateButtons();
324
+ }
325
+ result = 0;
326
+ return true;
327
+
328
+ case WM_NCLBUTTONDOWN:
329
+ case WM_NCLBUTTONDBLCLK: {
330
+ const CaptionButton button = buttonForHitCode(wparam);
331
+ // Anything else -- the caption's drag regions above all -- is Windows'
332
+ // to handle: that is the move, and a double click's maximise.
333
+ if (button == CaptionButton::None) {
334
+ return false;
335
+ }
336
+ // Not passed on: DefWindowProc would run its own tracking loop for a
337
+ // caption button and paint the classic one on top of ours.
338
+ pressed_ = button;
339
+ invalidateButtons();
340
+ result = 0;
341
+ return true;
342
+ }
343
+
344
+ case WM_NCLBUTTONUP: {
345
+ const CaptionButton pressed = pressed_;
346
+ if (pressed == CaptionButton::None) {
347
+ return false;
348
+ }
349
+ pressed_ = CaptionButton::None;
350
+ invalidateButtons();
351
+ // A press only acts if it is released over the same button, as Windows'
352
+ // own buttons behave.
353
+ if (buttonForHitCode(wparam) == pressed) {
354
+ WPARAM command = 0;
355
+ switch (pressed) {
356
+ case CaptionButton::Minimize:
357
+ command = SC_MINIMIZE;
358
+ break;
359
+ case CaptionButton::Maximize:
360
+ command = IsZoomed(window_) ? SC_RESTORE : SC_MAXIMIZE;
361
+ break;
362
+ case CaptionButton::Close:
363
+ command = SC_CLOSE;
364
+ break;
365
+ case CaptionButton::None:
366
+ break;
367
+ }
368
+ if (command != 0) {
369
+ PostMessage(window_, WM_SYSCOMMAND, command, 0);
370
+ }
371
+ }
372
+ result = 0;
373
+ return true;
374
+ }
375
+
376
+ default:
377
+ return false;
378
+ }
379
+ }
380
+
381
+ // The four caption actions, reached from JavaScript rather than from a click.
382
+ //
383
+ // Each posts rather than sends. A caption action can destroy the window, and
384
+ // the caller here is a trampoline from the JavaScript thread: sending would
385
+ // run the close synchronously underneath a stack that still expects a window.
386
+ // Posting is also what the button path above does, so both arrive the same way
387
+ // and in the same order as everything else in the queue.
388
+ //
389
+ // Each tolerates no window. An app can ask before the host has one, and the
390
+ // honest answer to "minimise what?" is to do nothing rather than to crash.
391
+ void Win32TitleBar::minimize() {
392
+ if (window_ == nullptr) {
393
+ return;
394
+ }
395
+ PostMessage(window_, WM_SYSCOMMAND, SC_MINIMIZE, 0);
396
+ }
397
+
398
+ void Win32TitleBar::toggleMaximize() {
399
+ if (window_ == nullptr) {
400
+ return;
401
+ }
402
+ // Asked now rather than when the request was made: a window can be maximised
403
+ // by a drag to the top edge in between, and then "toggle" means the opposite
404
+ // of what it meant a moment ago.
405
+ PostMessage(window_, WM_SYSCOMMAND, IsZoomed(window_) ? SC_RESTORE : SC_MAXIMIZE, 0);
406
+ }
407
+
408
+ void Win32TitleBar::close() {
409
+ if (window_ == nullptr) {
410
+ return;
411
+ }
412
+ // SC_CLOSE, not DestroyWindow: this way it arrives as WM_CLOSE, which is
413
+ // where core's interceptClose gets its say. Closing any other way would let
414
+ // an app refuse a close and then be unable to honour agreeing to one -- the
415
+ // exact bug that not registering these left on this platform.
416
+ PostMessage(window_, WM_SYSCOMMAND, SC_CLOSE, 0);
417
+ }
418
+
419
+ void Win32TitleBar::startDrag() {
420
+ if (window_ == nullptr) {
421
+ return;
422
+ }
423
+ // There is no "begin moving this window" call. Releasing the capture and
424
+ // then posting a caption click hands Windows into its own move loop, which
425
+ // is the same message its title bar sends itself. GetMessagePos is already
426
+ // in the screen coordinates WM_NCLBUTTONDOWN wants.
427
+ ReleaseCapture();
428
+ PostMessage(window_, WM_NCLBUTTONDOWN, HTCAPTION, static_cast<LPARAM>(GetMessagePos()));
429
+ }
430
+
431
+ void Win32TitleBar::handleActivate(bool active) {
432
+ if (active != active_) {
433
+ active_ = active;
434
+ invalidateButtons();
435
+ }
436
+ }
437
+
438
+ void Win32TitleBar::clearHover() {
439
+ if (hovered_ != CaptionButton::None && pressed_ == CaptionButton::None) {
440
+ hovered_ = CaptionButton::None;
441
+ invalidateButtons();
442
+ }
443
+ }
444
+
445
+ void Win32TitleBar::invalidateButtons() {
446
+ if (style_ != TitleBarStyle::Hidden || window_ == nullptr) {
447
+ return;
448
+ }
449
+ RECT client{};
450
+ GetClientRect(window_, &client);
451
+ const auto caption = captionMetrics();
452
+ RECT buttons{client.right - static_cast<LONG>(std::ceil(3 * caption.buttonWidth)),
453
+ 0,
454
+ client.right,
455
+ static_cast<LONG>(std::ceil(caption.height))};
456
+ InvalidateRect(window_, &buttons, FALSE);
457
+ }
458
+
459
+ void Win32TitleBar::paintButtons(ID2D1RenderTarget *target) const {
460
+ if (style_ != TitleBarStyle::Hidden || window_ == nullptr || target == nullptr) {
461
+ return;
462
+ }
463
+
464
+ RECT client{};
465
+ GetClientRect(window_, &client);
466
+ const float width = static_cast<float>(client.right - client.left);
467
+ const auto caption = captionMetrics();
468
+ const float scale = static_cast<float>(dpiOf(window_)) / 96.0f;
469
+
470
+ // Windows 11's own caption button colours: a faint wash of the glyph colour
471
+ // on hover and press, and the close button's red.
472
+ const D2D1_COLOR_F themeGlyph =
473
+ dark_ ? D2D1::ColorF(1.0f, 1.0f, 1.0f) : D2D1::ColorF(0.0f, 0.0f, 0.0f);
474
+ D2D1_COLOR_F glyph = textColor_.has_value() ? toColor(*textColor_) : themeGlyph;
475
+ if (!active_) {
476
+ glyph.a *= 0.45f;
477
+ }
478
+ const D2D1_COLOR_F closeRed = D2D1::ColorF(196.0f / 255.0f, 43.0f / 255.0f, 28.0f / 255.0f);
479
+
480
+ target->SetTransform(D2D1::Matrix3x2F::Identity());
481
+
482
+ Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
483
+ if (FAILED(target->CreateSolidColorBrush(glyph, &brush))) {
484
+ return;
485
+ }
486
+
487
+ // Crisp one-device-pixel strokes at any scale: an odd width sits on a half
488
+ // pixel, an even one on a whole one.
489
+ const float stroke = std::max(1.0f, std::round(scale));
490
+ const float offset = static_cast<int>(stroke) % 2 == 1 ? 0.5f : 0.0f;
491
+ const float half = std::round(5.0f * scale);
492
+
493
+ const CaptionButton order[3] = {
494
+ CaptionButton::Minimize, CaptionButton::Maximize, CaptionButton::Close};
495
+ for (int index = 0; index < 3; ++index) {
496
+ const CaptionButton button = order[index];
497
+ const float left = width - static_cast<float>(3 - index) * caption.buttonWidth;
498
+ const D2D1_RECT_F cell{left, 0.0f, left + caption.buttonWidth, caption.height};
499
+
500
+ const bool hovered = hovered_ == button;
501
+ const bool pressed = pressed_ == button;
502
+ D2D1_COLOR_F color = glyph;
503
+ if (hovered || pressed) {
504
+ D2D1_COLOR_F wash = themeGlyph;
505
+ if (button == CaptionButton::Close) {
506
+ wash = closeRed;
507
+ wash.a = pressed ? 0.9f : 1.0f;
508
+ color = D2D1::ColorF(1.0f, 1.0f, 1.0f);
509
+ } else {
510
+ wash.a = pressed ? 0.04f : 0.06f;
511
+ }
512
+ brush->SetColor(wash);
513
+ target->FillRectangle(cell, brush.Get());
514
+ }
515
+ brush->SetColor(color);
516
+
517
+ const float cx = std::floor(left + caption.buttonWidth / 2.0f) + offset;
518
+ const float cy = std::floor(caption.height / 2.0f) + offset;
519
+
520
+ switch (button) {
521
+ case CaptionButton::Minimize:
522
+ target->DrawLine(
523
+ D2D1::Point2F(cx - half, cy), D2D1::Point2F(cx + half, cy), brush.Get(), stroke);
524
+ break;
525
+ case CaptionButton::Maximize:
526
+ if (IsZoomed(window_)) {
527
+ // Restore: a window in front, and the edge of one behind it.
528
+ const float inset = std::round(2.0f * scale);
529
+ target->DrawRectangle(
530
+ D2D1::RectF(cx - half, cy - half + inset, cx + half - inset, cy + half),
531
+ brush.Get(),
532
+ stroke);
533
+ target->DrawLine(D2D1::Point2F(cx - half + inset, cy - half),
534
+ D2D1::Point2F(cx + half, cy - half),
535
+ brush.Get(),
536
+ stroke);
537
+ target->DrawLine(D2D1::Point2F(cx + half, cy - half),
538
+ D2D1::Point2F(cx + half, cy + half - inset),
539
+ brush.Get(),
540
+ stroke);
541
+ } else {
542
+ target->DrawRectangle(
543
+ D2D1::RectF(cx - half, cy - half, cx + half, cy + half), brush.Get(), stroke);
544
+ }
545
+ break;
546
+ case CaptionButton::Close:
547
+ target->DrawLine(D2D1::Point2F(cx - half, cy - half),
548
+ D2D1::Point2F(cx + half, cy + half),
549
+ brush.Get(),
550
+ stroke);
551
+ target->DrawLine(D2D1::Point2F(cx + half, cy - half),
552
+ D2D1::Point2F(cx - half, cy + half),
553
+ brush.Get(),
554
+ stroke);
555
+ break;
556
+ case CaptionButton::None:
557
+ break;
558
+ }
559
+ }
560
+ }
561
+
562
+ } // namespace basalt
@@ -0,0 +1,146 @@
1
+ // The window's title bar on Windows: its title, its colours, dark or light,
2
+ // and -- when an app asks for it hidden -- the caption the host draws instead.
3
+ //
4
+ // Two different things share this class, because they share one window and
5
+ // one set of requests from JavaScript:
6
+ //
7
+ // - Native style. Windows draws the caption, and this only talks to DWM. Dark
8
+ // mode is DWMWA_USE_IMMERSIVE_DARK_MODE, following the app's effective colour
9
+ // scheme. Colours are DWMWA_CAPTION_COLOR, DWMWA_TEXT_COLOR and
10
+ // DWMWA_BORDER_COLOR, which Windows 11 honours and Windows 10 ignores.
11
+ //
12
+ // - Hidden style. WM_NCCALCSIZE gives the caption's height back to the client
13
+ // area, so the app's content reaches the top of the window. The resize edge
14
+ // along the top, the three caption buttons and the app's drag regions are then
15
+ // this class's to answer WM_NCHITTEST for, and the buttons are painted by the
16
+ // host over the app's content. Answering HTMAXBUTTON over the maximise button
17
+ // is also what makes Windows 11 offer its snap layouts there.
18
+ //
19
+ // UI thread only, like the window, except where a method says otherwise. The
20
+ // BasaltWindow module posts its requests here.
21
+
22
+ #pragma once
23
+
24
+ #include "Win32TitleBarLayout.h"
25
+
26
+ #include <windows.h>
27
+
28
+ #include <functional>
29
+ #include <mutex>
30
+ #include <optional>
31
+ #include <string>
32
+
33
+ struct ID2D1RenderTarget;
34
+
35
+ namespace basalt {
36
+
37
+ namespace win32 {
38
+ class RnWin32View;
39
+ } // namespace win32
40
+
41
+ enum class TitleBarStyle { Native, Hidden };
42
+
43
+ // What JavaScript is told about the caption, in client pixels -- which are the
44
+ // surface root's units on this host. All zero while the style is native: the
45
+ // system's caption is outside the client area and takes nothing from the app.
46
+ struct TitleBarMetrics {
47
+ float height{0};
48
+ float buttonsWidth{0};
49
+ TitleBarStyle style{TitleBarStyle::Native};
50
+ };
51
+
52
+ class Win32TitleBar {
53
+ public:
54
+ using MetricsListener = std::function<void(const TitleBarMetrics &)>;
55
+
56
+ void attach(HWND window);
57
+
58
+ // The title shown when the app has not set one: its name, which the host
59
+ // decides.
60
+ void setDefaultTitle(const std::string &title);
61
+ // The app's title, or nullopt to go back to the default.
62
+ void setTitle(std::optional<std::string> title);
63
+
64
+ void setDarkMode(bool dark);
65
+
66
+ // Each a COLORREF, or nullopt for the system's own.
67
+ void setColors(std::optional<COLORREF> caption,
68
+ std::optional<COLORREF> text,
69
+ std::optional<COLORREF> border);
70
+
71
+ void setStyle(TitleBarStyle style);
72
+ TitleBarStyle style() const { return style_; }
73
+
74
+ // Safe from any thread: the module reads it on the JavaScript thread.
75
+ TitleBarMetrics metrics() const;
76
+ // The one listener, or null to clear it. Safe from any thread; called on the
77
+ // UI thread whenever the metrics change.
78
+ void setMetricsListener(MetricsListener listener);
79
+ // After a resize or a DPI change, either of which can move the caption
80
+ // without anything having been asked.
81
+ void refreshMetrics();
82
+
83
+ // The caption's four actions, which an app-drawn header asks for by name.
84
+ // GtkWindowModule and AppKitWindowModule register the same four, and this
85
+ // host registered none of them: an app could draw its own header on Windows
86
+ // and then find its close button did nothing.
87
+ //
88
+ // They are what the host-drawn buttons already do on a click -- see the
89
+ // WM_NCLBUTTONUP case in handleNcMouse, which posts the same WM_SYSCOMMAND.
90
+ // Naming them here is what lets JavaScript ask for the same thing, and is
91
+ // what stops the two paths drifting apart.
92
+ //
93
+ // Each is safe with no window attached, which is what an app asking before
94
+ // the host has a window would do.
95
+ void minimize();
96
+ void toggleMaximize();
97
+ void close();
98
+ // Hands the drag to Windows, which runs the move loop itself. There is no
99
+ // "start moving this window" call: the idiom is to release the capture and
100
+ // then post a caption click, which is what the system's own title bar does.
101
+ void startDrag();
102
+
103
+ // Window procedure hooks. Each returns true when it answered the message,
104
+ // with the answer in `result`; false means the host should carry on as it
105
+ // would have.
106
+ bool handleNcCalcSize(WPARAM wparam, LPARAM lparam, LRESULT &result);
107
+ bool handleNcHitTest(LPARAM lparam, win32::RnWin32View *root, LRESULT &result);
108
+ bool handleNcMouse(UINT message, WPARAM wparam, LRESULT &result);
109
+ void handleActivate(bool active);
110
+ // The pointer is over the client area, so no caption button is hovered.
111
+ void clearHover();
112
+
113
+ // Paints the three caption buttons over whatever has already been drawn, in
114
+ // client coordinates. Does nothing unless the style is hidden.
115
+ void paintButtons(ID2D1RenderTarget *target) const;
116
+
117
+ private:
118
+ win32::CaptionMetrics captionMetrics() const;
119
+ void applyTitle();
120
+ void applyColors();
121
+ void redrawFrame();
122
+ void invalidateButtons();
123
+ void publishMetrics();
124
+
125
+ HWND window_{nullptr};
126
+ TitleBarStyle style_{TitleBarStyle::Native};
127
+ bool dark_{false};
128
+ bool active_{true};
129
+ std::string defaultTitle_;
130
+ std::optional<std::string> title_;
131
+ std::optional<COLORREF> captionColor_;
132
+ std::optional<COLORREF> textColor_;
133
+ std::optional<COLORREF> borderColor_;
134
+ win32::CaptionButton hovered_{win32::CaptionButton::None};
135
+ win32::CaptionButton pressed_{win32::CaptionButton::None};
136
+ bool trackingLeave_{false};
137
+
138
+ mutable std::mutex mutex_;
139
+ TitleBarMetrics metrics_;
140
+ MetricsListener listener_;
141
+ };
142
+
143
+ // The host's one title bar.
144
+ Win32TitleBar &titleBar();
145
+
146
+ } // namespace basalt