@zogien/native-sdk 0.11.0-fork.2 → 0.11.0-fork.4
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.
- package/README.md +1 -1
- package/build/app.zig +47 -23
- package/build/cli.zig +22 -0
- package/native-sdk.d.ts +2 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -0
- package/src/platform/macos/appkit_host.h +9 -0
- package/src/platform/macos/appkit_host.m +366 -0
- package/src/platform/macos/root.zig +29 -6
- package/src/platform/null_platform.zig +66 -0
- package/src/platform/root.zig +1 -0
- package/src/platform/types.zig +46 -0
- package/src/platform/windows/root.zig +174 -0
- package/src/platform/windows/scriptc_mingw_compat.c +248 -0
- package/src/platform/windows/webview2_host.cpp +500 -26
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +1 -0
- package/src/runtime/bridge_responses.zig +2 -0
- package/src/runtime/channel_sources.zig +49 -1
- package/src/runtime/core.zig +5 -0
- package/src/runtime/effects.zig +12 -1
- package/src/runtime/flow.zig +6 -0
- package/src/runtime/flow_reference.zig +6 -0
- package/src/runtime/root.zig +4 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/runtime/tests.zig +1 -0
- package/src/runtime/window_controls.zig +73 -0
- package/src/runtime/window_controls_tests.zig +192 -0
- package/src/runtime/window_storage.zig +3 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +53 -3
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- package/tools/native-sdk/main.zig +10 -9
|
@@ -312,6 +312,9 @@ struct WindowsEvent {
|
|
|
312
312
|
* the selected item's id (0 = dismissed without a selection). */
|
|
313
313
|
uint64_t widget_id;
|
|
314
314
|
uint32_t menu_item_id;
|
|
315
|
+
/* Window events: nonzero while the window is maximized (IsZoomed).
|
|
316
|
+
* Every emit carries the live value, so any frame event reports it. */
|
|
317
|
+
int maximized;
|
|
315
318
|
};
|
|
316
319
|
|
|
317
320
|
/* One context-menu entry crossing the C ABI (the same shape the macOS
|
|
@@ -415,6 +418,12 @@ struct Window {
|
|
|
415
418
|
/* True while the .hide policy has this window off the glass; the
|
|
416
419
|
* hwnd stays live and the map entry stays owned. */
|
|
417
420
|
bool policy_hidden = false;
|
|
421
|
+
/* The maximized state last reported to the runtime, so WM_SIZE emits
|
|
422
|
+
* a frame event only when it changes. */
|
|
423
|
+
bool maximized = false;
|
|
424
|
+
/* Same-thread child kept above content children across the top
|
|
425
|
+
* resize band (see syncTopResizeStrip); created on first need. */
|
|
426
|
+
HWND top_resize_strip = nullptr;
|
|
418
427
|
};
|
|
419
428
|
|
|
420
429
|
/* One rectangle of a canvas view's window-drag mirror (runtime push,
|
|
@@ -737,6 +746,11 @@ struct Host {
|
|
|
737
746
|
std::string window_title;
|
|
738
747
|
std::string bundle_id;
|
|
739
748
|
std::string icon_path;
|
|
749
|
+
/* The WebView2 user data folder every environment in this host
|
|
750
|
+
* shares, resolved once on first use (webViewUserDataFolder). Empty
|
|
751
|
+
* means WebView2's own default. */
|
|
752
|
+
bool webview_user_data_folder_resolved = false;
|
|
753
|
+
std::wstring webview_user_data_folder;
|
|
740
754
|
EventCallback callback = nullptr;
|
|
741
755
|
void *callback_context = nullptr;
|
|
742
756
|
BridgeCallback bridge_callback = nullptr;
|
|
@@ -1165,8 +1179,57 @@ static std::string bridgeOriginForWebViewUrl(const ChildWebView &webview, const
|
|
|
1165
1179
|
return isInternalAssetUrl(webview, url) ? assetOrigin(webview) : originForUrl(url);
|
|
1166
1180
|
}
|
|
1167
1181
|
|
|
1182
|
+
/* The resources\ directory of a packaged Windows artifact: `native
|
|
1183
|
+
* package` stages the executable in bin\ and the app-relative asset tree
|
|
1184
|
+
* in resources\ beside it. Empty when the executable is not in a bin\
|
|
1185
|
+
* directory. */
|
|
1186
|
+
static const std::wstring &packagedResourcesDirectory() {
|
|
1187
|
+
static const std::wstring directory = [] {
|
|
1188
|
+
std::wstring path(MAX_PATH, L'\0');
|
|
1189
|
+
for (;;) {
|
|
1190
|
+
const DWORD length = GetModuleFileNameW(nullptr, path.data(), (DWORD)path.size());
|
|
1191
|
+
if (length == 0) return std::wstring();
|
|
1192
|
+
if (length < path.size()) {
|
|
1193
|
+
path.resize(length);
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
if (path.size() >= 32768) return std::wstring();
|
|
1197
|
+
path.resize(path.size() * 2);
|
|
1198
|
+
}
|
|
1199
|
+
const size_t exe_slash = path.find_last_of(L"\\/");
|
|
1200
|
+
if (exe_slash == std::wstring::npos) return std::wstring();
|
|
1201
|
+
const std::wstring bin = path.substr(0, exe_slash);
|
|
1202
|
+
const size_t bin_slash = bin.find_last_of(L"\\/");
|
|
1203
|
+
if (bin_slash == std::wstring::npos || CompareStringOrdinal(bin.c_str() + bin_slash + 1, -1, L"bin", -1, TRUE) != CSTR_EQUAL) return std::wstring();
|
|
1204
|
+
return bin.substr(0, bin_slash) + L"\\resources";
|
|
1205
|
+
}();
|
|
1206
|
+
return directory;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/* A relative WebView asset root names a directory of the packaged asset
|
|
1210
|
+
* tree when the app runs from a package, the tree the macOS host finds
|
|
1211
|
+
* in its bundle's Resources. In development it stays relative to the
|
|
1212
|
+
* working directory. */
|
|
1213
|
+
static std::string assetRootPath(const std::string &root) {
|
|
1214
|
+
const bool absolute = !root.empty() && (root[0] == '\\' || root[0] == '/' || (root.size() > 1 && root[1] == ':'));
|
|
1215
|
+
if (!absolute) {
|
|
1216
|
+
const std::wstring &resources = packagedResourcesDirectory();
|
|
1217
|
+
if (!resources.empty()) {
|
|
1218
|
+
std::wstring candidate = resources;
|
|
1219
|
+
if (!root.empty() && root != ".") {
|
|
1220
|
+
std::wstring relative = widen(root);
|
|
1221
|
+
std::replace(relative.begin(), relative.end(), L'/', L'\\');
|
|
1222
|
+
candidate += L"\\" + relative;
|
|
1223
|
+
}
|
|
1224
|
+
const DWORD attributes = GetFileAttributesW(candidate.c_str());
|
|
1225
|
+
if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) return narrow(candidate);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return root.empty() ? std::string(".") : root;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1168
1231
|
static std::wstring assetFilePath(const ChildWebView &webview, const std::string &relative) {
|
|
1169
|
-
std::string path = webview.asset_root
|
|
1232
|
+
std::string path = assetRootPath(webview.asset_root);
|
|
1170
1233
|
if (!path.empty() && path.back() != '/' && path.back() != '\\') path.push_back('\\');
|
|
1171
1234
|
std::string native_relative = relative;
|
|
1172
1235
|
std::replace(native_relative.begin(), native_relative.end(), '/', '\\');
|
|
@@ -1382,6 +1445,7 @@ static void emit(Host *host, const Window &window, EventKind kind) {
|
|
|
1382
1445
|
event.open = window.hwnd != nullptr;
|
|
1383
1446
|
event.focused = windowOwnsKeyboardFocus(window);
|
|
1384
1447
|
event.hidden = window.policy_hidden ? 1 : 0;
|
|
1448
|
+
event.maximized = window.hwnd && IsZoomed(window.hwnd) ? 1 : 0;
|
|
1385
1449
|
event.label = window.label.c_str();
|
|
1386
1450
|
event.label_len = window.label.size();
|
|
1387
1451
|
event.title = window.title.c_str();
|
|
@@ -1389,6 +1453,23 @@ static void emit(Host *host, const Window &window, EventKind kind) {
|
|
|
1389
1453
|
host->callback(host->callback_context, &event);
|
|
1390
1454
|
}
|
|
1391
1455
|
|
|
1456
|
+
/* WM_SIZE runs for every maximize and restore, whoever started it: the
|
|
1457
|
+
* caption double-click, the system menu, Win+Up, a drag to the top edge,
|
|
1458
|
+
* or the app's own maximize call. A frame event on each transition keeps the runtime's
|
|
1459
|
+
* maximized state current; ordinary resizes emit nothing new. */
|
|
1460
|
+
static void emitMaximizedTransition(Host *host, HWND hwnd) {
|
|
1461
|
+
if (!host || !hwnd) return;
|
|
1462
|
+
for (auto &entry : host->windows) {
|
|
1463
|
+
Window &window = entry.second;
|
|
1464
|
+
if (window.hwnd != hwnd) continue;
|
|
1465
|
+
const bool maximized = IsZoomed(hwnd) != FALSE;
|
|
1466
|
+
if (maximized == window.maximized) return;
|
|
1467
|
+
window.maximized = maximized;
|
|
1468
|
+
emit(host, window, kWindowFrame);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1392
1473
|
static void emitFileDrop(Host *host, const Window &window, const std::string &paths) {
|
|
1393
1474
|
if (!host || !host->callback || paths.empty()) return;
|
|
1394
1475
|
WindowsEvent event = {};
|
|
@@ -1990,6 +2071,8 @@ static void applyNativeViewState(NativeView &view, bool update_text, const std::
|
|
|
1990
2071
|
applyNativeViewAccessibility(view);
|
|
1991
2072
|
}
|
|
1992
2073
|
|
|
2074
|
+
static void syncTopResizeStrip(Host *host, uint64_t window_id);
|
|
2075
|
+
|
|
1993
2076
|
static void reorderWindowChildren(Host *host, uint64_t window_id) {
|
|
1994
2077
|
if (!host) return;
|
|
1995
2078
|
struct LayerItem {
|
|
@@ -2017,6 +2100,8 @@ static void reorderWindowChildren(Host *host, uint64_t window_id) {
|
|
|
2017
2100
|
for (const LayerItem &item : items) {
|
|
2018
2101
|
SetWindowPos(item.hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
|
2019
2102
|
}
|
|
2103
|
+
/* Content layers never rise above the window's own resize band. */
|
|
2104
|
+
syncTopResizeStrip(host, window_id);
|
|
2020
2105
|
}
|
|
2021
2106
|
|
|
2022
2107
|
static bool emitNativeCommandForHwnd(Host *host, HWND hwnd, UINT notification_code) {
|
|
@@ -2304,6 +2389,25 @@ static bool windowIsChromeless(const Window &window) {
|
|
|
2304
2389
|
return window.titlebar_style == 3;
|
|
2305
2390
|
}
|
|
2306
2391
|
|
|
2392
|
+
/* Windows whose top resize frame lives INSIDE the client, answered by
|
|
2393
|
+
* the window's own WM_NCHITTEST and the top resize strip: the hidden
|
|
2394
|
+
* styles, and chromeless windows. On Windows 10 a caption-less frame
|
|
2395
|
+
* with any non-client top draws it as a blank white bar, so chromeless
|
|
2396
|
+
* keeps real left/right/bottom borders (resize and shadow) and hands
|
|
2397
|
+
* the client the top edge, the frame Tauri gives its undecorated
|
|
2398
|
+
* windows. Two chromeless shapes keep the plain popup frame: transparent
|
|
2399
|
+
* windows, whose per-pixel present seeds hit-testable resize bands
|
|
2400
|
+
* around the client, and windows carrying a menu bar, which needs the
|
|
2401
|
+
* non-client top. */
|
|
2402
|
+
static bool windowReclaimsTopFrame(const Window &window, bool has_menu) {
|
|
2403
|
+
if (windowUsesHiddenTitlebar(window)) return true;
|
|
2404
|
+
return windowIsChromeless(window) && !window.transparent && !has_menu;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
static bool windowReclaimsTopFrame(const Window &window) {
|
|
2408
|
+
return windowReclaimsTopFrame(window, window.hwnd && GetMenu(window.hwnd) != nullptr);
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2307
2411
|
static Window *hiddenTitlebarWindowForHwnd(Host *host, HWND hwnd) {
|
|
2308
2412
|
if (!host || !hwnd) return nullptr;
|
|
2309
2413
|
for (auto &entry : host->windows) {
|
|
@@ -2414,6 +2518,168 @@ static int hiddenFrameTopThickness(HWND hwnd) {
|
|
|
2414
2518
|
return systemMetricForDpi(SM_CYSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
2415
2519
|
}
|
|
2416
2520
|
|
|
2521
|
+
/* The top resize band's hit code for a client point: `thickness` rows
|
|
2522
|
+
* tall, with corner slivers as wide as the side borders so the diagonal
|
|
2523
|
+
* grips survive at the very top; HTNOWHERE outside the band. */
|
|
2524
|
+
static constexpr LRESULT topResizeBandHitCode(int x, int y, int client_width, int thickness, int corner) {
|
|
2525
|
+
if (y < 0 || y >= thickness || x < 0 || x >= client_width) return HTNOWHERE;
|
|
2526
|
+
if (x < corner) return HTTOPLEFT;
|
|
2527
|
+
if (x >= client_width - corner) return HTTOPRIGHT;
|
|
2528
|
+
return HTTOP;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
static_assert(
|
|
2532
|
+
topResizeBandHitCode(3, 2, 1040, 8, 8) == HTTOPLEFT &&
|
|
2533
|
+
topResizeBandHitCode(8, 2, 1040, 8, 8) == HTTOP &&
|
|
2534
|
+
topResizeBandHitCode(1031, 7, 1040, 8, 8) == HTTOP &&
|
|
2535
|
+
topResizeBandHitCode(1032, 7, 1040, 8, 8) == HTTOPRIGHT &&
|
|
2536
|
+
topResizeBandHitCode(520, 8, 1040, 8, 8) == HTNOWHERE &&
|
|
2537
|
+
topResizeBandHitCode(520, -1, 1040, 8, 8) == HTNOWHERE,
|
|
2538
|
+
"the top band is exactly `thickness` rows tall with `corner`-wide diagonal grips");
|
|
2539
|
+
|
|
2540
|
+
/* The top resize band of a window that reclaims its top frame, in the
|
|
2541
|
+
* window's client coordinates. Only a restored resizable window has one:
|
|
2542
|
+
* a maximized window does not resize and a fixed one never does. Every
|
|
2543
|
+
* surface that answers for the band — the window itself, canvas children
|
|
2544
|
+
* handing it back, and the top resize strip — asks here. */
|
|
2545
|
+
static LRESULT topResizeBandHit(const Window &window, POINT client) {
|
|
2546
|
+
if (!window.hwnd || !windowReclaimsTopFrame(window)) return HTNOWHERE;
|
|
2547
|
+
if (!window.resizable || IsZoomed(window.hwnd) || IsIconic(window.hwnd)) return HTNOWHERE;
|
|
2548
|
+
RECT rect = {};
|
|
2549
|
+
GetClientRect(window.hwnd, &rect);
|
|
2550
|
+
const UINT dpi = dpiForWindow(window.hwnd);
|
|
2551
|
+
const int corner = systemMetricForDpi(SM_CXSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
2552
|
+
return topResizeBandHitCode(client.x, client.y, rect.right - rect.left, hiddenFrameTopThickness(window.hwnd), corner);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/* ------------------------------------------------------------------------
|
|
2556
|
+
* Top resize strip.
|
|
2557
|
+
*
|
|
2558
|
+
* A child covering the top band must hand it back to the window. Canvas
|
|
2559
|
+
* surfaces in hidden-titlebar windows do (they answer HTTRANSPARENT
|
|
2560
|
+
* there), but a WebView2 child belongs to the browser process and never
|
|
2561
|
+
* consults its parent, so under WebView content the band was only
|
|
2562
|
+
* resizable where WebView2's own drag-region windows happened to answer
|
|
2563
|
+
* it. The strip is a same-thread, unpainted child kept above every
|
|
2564
|
+
* content child across the band: it answers the window's band hit codes
|
|
2565
|
+
* itself (the cursor follows from them) and forwards the press to the
|
|
2566
|
+
* window, whose DefWindowProc runs the system size loop. Tauri recovers
|
|
2567
|
+
* the top edge of its undecorated windows the same way. WS_EX_TRANSPARENT
|
|
2568
|
+
* keeps the strip out of its siblings' visible regions: the WebView
|
|
2569
|
+
* containers clip siblings, and without it the band shows the window
|
|
2570
|
+
* background instead of the content under it. */
|
|
2571
|
+
|
|
2572
|
+
static Window *windowForTopResizeStrip(Host *host, HWND strip) {
|
|
2573
|
+
if (!host || !strip) return nullptr;
|
|
2574
|
+
for (auto &entry : host->windows) {
|
|
2575
|
+
if (entry.second.top_resize_strip == strip) return &entry.second;
|
|
2576
|
+
}
|
|
2577
|
+
return nullptr;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
static LRESULT CALLBACK topResizeStripProc(HWND strip, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
2581
|
+
Host *host = reinterpret_cast<Host *>(GetWindowLongPtrW(strip, GWLP_USERDATA));
|
|
2582
|
+
switch (message) {
|
|
2583
|
+
case WM_NCHITTEST: {
|
|
2584
|
+
Window *window = windowForTopResizeStrip(host, strip);
|
|
2585
|
+
if (!window || !window->hwnd) return HTTRANSPARENT;
|
|
2586
|
+
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
2587
|
+
ScreenToClient(window->hwnd, &point);
|
|
2588
|
+
const LRESULT hit = topResizeBandHit(*window, point);
|
|
2589
|
+
return hit != HTNOWHERE ? hit : HTTRANSPARENT;
|
|
2590
|
+
}
|
|
2591
|
+
case WM_NCLBUTTONDOWN:
|
|
2592
|
+
case WM_NCLBUTTONDBLCLK:
|
|
2593
|
+
/* Same thread, so the window's own non-client handling runs
|
|
2594
|
+
* synchronously with the original screen point: the size loop
|
|
2595
|
+
* for a press, the vertical maximize for a double-click. */
|
|
2596
|
+
if (wparam == HTTOP || wparam == HTTOPLEFT || wparam == HTTOPRIGHT) {
|
|
2597
|
+
Window *window = windowForTopResizeStrip(host, strip);
|
|
2598
|
+
if (window && window->hwnd) return SendMessageW(window->hwnd, message, wparam, lparam);
|
|
2599
|
+
}
|
|
2600
|
+
break;
|
|
2601
|
+
case WM_ERASEBKGND:
|
|
2602
|
+
return 1;
|
|
2603
|
+
case WM_PAINT: {
|
|
2604
|
+
PAINTSTRUCT paint = {};
|
|
2605
|
+
BeginPaint(strip, &paint);
|
|
2606
|
+
EndPaint(strip, &paint);
|
|
2607
|
+
return 0;
|
|
2608
|
+
}
|
|
2609
|
+
default:
|
|
2610
|
+
break;
|
|
2611
|
+
}
|
|
2612
|
+
return DefWindowProcW(strip, message, wparam, lparam);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
static const wchar_t *topResizeStripClassName(Host *host) {
|
|
2616
|
+
static bool registered = false;
|
|
2617
|
+
if (!registered) {
|
|
2618
|
+
WNDCLASSEXW wc = {};
|
|
2619
|
+
wc.cbSize = sizeof(wc);
|
|
2620
|
+
wc.lpfnWndProc = topResizeStripProc;
|
|
2621
|
+
wc.hInstance = host->instance;
|
|
2622
|
+
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
|
2623
|
+
wc.lpszClassName = L"NativeSdkTopResizeStrip";
|
|
2624
|
+
registered = RegisterClassExW(&wc) != 0;
|
|
2625
|
+
}
|
|
2626
|
+
return L"NativeSdkTopResizeStrip";
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/* Whether any WebView container reaches into the window's top band. */
|
|
2630
|
+
static bool webViewCoversTopBand(Host *host, const Window &window, int band) {
|
|
2631
|
+
RECT client = {};
|
|
2632
|
+
GetClientRect(window.hwnd, &client);
|
|
2633
|
+
const RECT band_rect = { 0, 0, client.right - client.left, band };
|
|
2634
|
+
for (auto &entry : host->webviews) {
|
|
2635
|
+
const ChildWebView &webview = entry.second;
|
|
2636
|
+
if (webview.window_id != window.id || !webview.hwnd) continue;
|
|
2637
|
+
if (!(GetWindowLongPtrW(webview.hwnd, GWL_STYLE) & WS_VISIBLE)) continue;
|
|
2638
|
+
RECT bounds = {};
|
|
2639
|
+
GetWindowRect(webview.hwnd, &bounds);
|
|
2640
|
+
POINT top_left = { bounds.left, bounds.top };
|
|
2641
|
+
POINT bottom_right = { bounds.right, bounds.bottom };
|
|
2642
|
+
ScreenToClient(window.hwnd, &top_left);
|
|
2643
|
+
ScreenToClient(window.hwnd, &bottom_right);
|
|
2644
|
+
const RECT local = { top_left.x, top_left.y, bottom_right.x, bottom_right.y };
|
|
2645
|
+
RECT overlap = {};
|
|
2646
|
+
if (IntersectRect(&overlap, &local, &band_rect)) return true;
|
|
2647
|
+
}
|
|
2648
|
+
return false;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
/* Show, size and raise the strip over the band of a restored resizable
|
|
2652
|
+
* window that reclaims its top frame; hide it otherwise. Chromeless
|
|
2653
|
+
* windows always take it: their content children answer the band as
|
|
2654
|
+
* client. Hidden-titlebar windows take it only while a WebView covers the
|
|
2655
|
+
* band, so canvas windows keep their own band and the DWM keeps first
|
|
2656
|
+
* claim on its caption buttons. Runs after every geometry, state, or
|
|
2657
|
+
* child z-order change. */
|
|
2658
|
+
static void syncTopResizeStrip(Host *host, uint64_t window_id) {
|
|
2659
|
+
if (!host) return;
|
|
2660
|
+
auto found = host->windows.find(window_id);
|
|
2661
|
+
if (found == host->windows.end() || !found->second.hwnd) return;
|
|
2662
|
+
Window &window = found->second;
|
|
2663
|
+
const int band = hiddenFrameTopThickness(window.hwnd);
|
|
2664
|
+
const bool wanted = windowReclaimsTopFrame(window) && window.resizable && !window.click_through &&
|
|
2665
|
+
!IsZoomed(window.hwnd) && !IsIconic(window.hwnd) &&
|
|
2666
|
+
(windowIsChromeless(window) || webViewCoversTopBand(host, window, band));
|
|
2667
|
+
if (!wanted) {
|
|
2668
|
+
if (window.top_resize_strip) ShowWindow(window.top_resize_strip, SW_HIDE);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
if (!window.top_resize_strip) {
|
|
2672
|
+
HWND strip = CreateWindowExW(WS_EX_TRANSPARENT, topResizeStripClassName(host), L"", WS_CHILD | WS_CLIPSIBLINGS,
|
|
2673
|
+
0, 0, 0, 0, window.hwnd, nullptr, host->instance, nullptr);
|
|
2674
|
+
if (!strip) return;
|
|
2675
|
+
SetWindowLongPtrW(strip, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(host));
|
|
2676
|
+
window.top_resize_strip = strip;
|
|
2677
|
+
}
|
|
2678
|
+
RECT client = {};
|
|
2679
|
+
GetClientRect(window.hwnd, &client);
|
|
2680
|
+
SetWindowPos(window.top_resize_strip, HWND_TOP, 0, 0, client.right - client.left, band, SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2417
2683
|
/* Height of the standard caption band (frame + caption) at the window's
|
|
2418
2684
|
* dpi — the DWM frame extension depth, and the chrome-channel top inset
|
|
2419
2685
|
* fallback when the live button rects are unavailable. */
|
|
@@ -3424,7 +3690,7 @@ static LRESULT CALLBACK gpuSurfaceProc(HWND hwnd, UINT message, WPARAM wparam, L
|
|
|
3424
3690
|
if (!chrome_window) break;
|
|
3425
3691
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
3426
3692
|
ScreenToClient(chrome_window->hwnd, &point);
|
|
3427
|
-
if (
|
|
3693
|
+
if (topResizeBandHit(*chrome_window, point) != HTNOWHERE) return HTTRANSPARENT;
|
|
3428
3694
|
RECT cluster = {};
|
|
3429
3695
|
if (captionButtonsClientRect(chrome_window->hwnd, &cluster) && PtInRect(&cluster, point)) return HTTRANSPARENT;
|
|
3430
3696
|
if (windowDragRegionHit(host, *chrome_window, point)) return HTTRANSPARENT;
|
|
@@ -4986,6 +5252,7 @@ static void destroyAllWindows(Host *host) {
|
|
|
4986
5252
|
if (entry.second.hwnd) {
|
|
4987
5253
|
DestroyWindow(entry.second.hwnd);
|
|
4988
5254
|
entry.second.hwnd = nullptr;
|
|
5255
|
+
entry.second.top_resize_strip = nullptr;
|
|
4989
5256
|
}
|
|
4990
5257
|
}
|
|
4991
5258
|
}
|
|
@@ -5311,13 +5578,90 @@ static CreateEnvironmentFn webView2Factory() {
|
|
|
5311
5578
|
return reinterpret_cast<CreateEnvironmentFn>(GetProcAddress(loader, "CreateCoreWebView2EnvironmentWithOptions"));
|
|
5312
5579
|
}
|
|
5313
5580
|
|
|
5581
|
+
static bool webViewDirectoryExists(const std::wstring &path) {
|
|
5582
|
+
const DWORD attributes = GetFileAttributesW(path.c_str());
|
|
5583
|
+
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
/* The folder WebView2 picks when none is given: "<exe>.WebView2" beside
|
|
5587
|
+
* the executable. Earlier hosts left their data there. */
|
|
5588
|
+
static std::wstring webViewLegacyUserDataFolder() {
|
|
5589
|
+
std::wstring path(MAX_PATH, L'\0');
|
|
5590
|
+
for (;;) {
|
|
5591
|
+
const DWORD length = GetModuleFileNameW(nullptr, path.data(), (DWORD)path.size());
|
|
5592
|
+
if (length == 0) return std::wstring();
|
|
5593
|
+
if (length < path.size()) {
|
|
5594
|
+
path.resize(length);
|
|
5595
|
+
return path + L".WebView2";
|
|
5596
|
+
}
|
|
5597
|
+
if (path.size() >= 32768) return std::wstring();
|
|
5598
|
+
path.resize(path.size() * 2);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5602
|
+
/* WebView2 keeps cookies, storage and caches in its user data folder.
|
|
5603
|
+
* Its default sits beside the executable, which is read-only under
|
|
5604
|
+
* Program Files and is replaced together with the program files when
|
|
5605
|
+
* an installed app updates. Keep it with the app's other per-user data
|
|
5606
|
+
* instead: %LOCALAPPDATA%\<bundle id>\WebView2, next to the Data, Cache,
|
|
5607
|
+
* State and Logs folders app_dirs resolves from the same variable and
|
|
5608
|
+
* bundle id. A folder left beside the executable by an earlier host
|
|
5609
|
+
* moves there once. A folder on another volume cannot be renamed there,
|
|
5610
|
+
* so that install keeps its old folder; when the move fails for another
|
|
5611
|
+
* reason (another process still has the folder open) this launch keeps
|
|
5612
|
+
* the old folder and the next launch tries again. Either way no data is
|
|
5613
|
+
* lost. Without LOCALAPPDATA or a usable bundle id, WebView2 keeps its
|
|
5614
|
+
* default. */
|
|
5615
|
+
static const wchar_t *webViewUserDataFolder(Host *host) {
|
|
5616
|
+
if (host->webview_user_data_folder_resolved) {
|
|
5617
|
+
return host->webview_user_data_folder.empty() ? nullptr : host->webview_user_data_folder.c_str();
|
|
5618
|
+
}
|
|
5619
|
+
host->webview_user_data_folder_resolved = true;
|
|
5620
|
+
const std::string &bundle_id = host->bundle_id;
|
|
5621
|
+
if (bundle_id.empty() || bundle_id == "." || bundle_id == ".." ||
|
|
5622
|
+
bundle_id.find_first_of("\\/:*?\"<>|") != std::string::npos) return nullptr;
|
|
5623
|
+
for (unsigned char ch : bundle_id) {
|
|
5624
|
+
if (ch < 0x20) return nullptr;
|
|
5625
|
+
}
|
|
5626
|
+
const DWORD needed = GetEnvironmentVariableW(L"LOCALAPPDATA", nullptr, 0);
|
|
5627
|
+
if (needed <= 1) return nullptr;
|
|
5628
|
+
std::wstring local_app_data((size_t)needed, L'\0');
|
|
5629
|
+
const DWORD written = GetEnvironmentVariableW(L"LOCALAPPDATA", local_app_data.data(), needed);
|
|
5630
|
+
if (written == 0 || written >= needed) return nullptr;
|
|
5631
|
+
local_app_data.resize(written);
|
|
5632
|
+
while (!local_app_data.empty() && (local_app_data.back() == L'\\' || local_app_data.back() == L'/')) local_app_data.pop_back();
|
|
5633
|
+
if (local_app_data.empty()) return nullptr;
|
|
5634
|
+
|
|
5635
|
+
const std::wstring app_root = local_app_data + L"\\" + widen(bundle_id);
|
|
5636
|
+
const std::wstring folder = app_root + L"\\WebView2";
|
|
5637
|
+
SHCreateDirectoryExW(nullptr, app_root.c_str(), nullptr);
|
|
5638
|
+
if (!webViewDirectoryExists(app_root)) return nullptr;
|
|
5639
|
+
|
|
5640
|
+
if (!webViewDirectoryExists(folder)) {
|
|
5641
|
+
const std::wstring legacy = webViewLegacyUserDataFolder();
|
|
5642
|
+
if (!legacy.empty() && webViewDirectoryExists(legacy) && !MoveFileExW(legacy.c_str(), folder.c_str(), 0)) {
|
|
5643
|
+
const DWORD error = GetLastError();
|
|
5644
|
+
if (error != ERROR_NOT_SAME_DEVICE) {
|
|
5645
|
+
fprintf(stderr, "native-sdk: keeping WebView2 data in %s for this launch; moving it to %s failed (error %lu)\n",
|
|
5646
|
+
narrow(legacy).c_str(), narrow(folder).c_str(), static_cast<unsigned long>(error));
|
|
5647
|
+
}
|
|
5648
|
+
host->webview_user_data_folder = legacy;
|
|
5649
|
+
return host->webview_user_data_folder.c_str();
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
host->webview_user_data_folder = folder;
|
|
5653
|
+
return host->webview_user_data_folder.c_str();
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5314
5656
|
static void cleanupPendingChildWebView(Host *host, const std::string &key) {
|
|
5315
5657
|
if (!host) return;
|
|
5316
5658
|
auto found = host->webviews.find(key);
|
|
5317
5659
|
if (found == host->webviews.end()) return;
|
|
5660
|
+
const uint64_t window_id = found->second.window_id;
|
|
5318
5661
|
if (found->second.controller) found->second.controller->Close();
|
|
5319
5662
|
if (found->second.hwnd) DestroyWindow(found->second.hwnd);
|
|
5320
5663
|
host->webviews.erase(found);
|
|
5664
|
+
syncTopResizeStrip(host, window_id);
|
|
5321
5665
|
}
|
|
5322
5666
|
|
|
5323
5667
|
static void failWindowDragInitialization(Host *host, const std::string &key, HRESULT result) {
|
|
@@ -5337,7 +5681,7 @@ static bool createChildWebView(Host *host, const std::string &key) {
|
|
|
5337
5681
|
HWND parent = found->second.hwnd;
|
|
5338
5682
|
const uint64_t generation = found->second.window_drag_generation;
|
|
5339
5683
|
std::weak_ptr<HostLifetime> lifetime = host->lifetime;
|
|
5340
|
-
HRESULT hr = factory(nullptr,
|
|
5684
|
+
HRESULT hr = factory(nullptr, webViewUserDataFolder(host), nullptr, Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
|
|
5341
5685
|
[host, key, parent, lifetime, generation](HRESULT result, ICoreWebView2Environment *environment) -> HRESULT {
|
|
5342
5686
|
auto token = lifetime.lock();
|
|
5343
5687
|
if (!token) return S_OK;
|
|
@@ -5673,6 +6017,67 @@ static bool handleAppTimerMessage(Host *host, WPARAM wparam) {
|
|
|
5673
6017
|
return true;
|
|
5674
6018
|
}
|
|
5675
6019
|
|
|
6020
|
+
/* Windows sizes only captioned windows to the work area when they
|
|
6021
|
+
* maximize; a caption-less popup covers the whole monitor, taskbar
|
|
6022
|
+
* included. Chromeless windows get the captioned behavior back: the
|
|
6023
|
+
* maximized frame is the work area of the window's monitor, grown by the
|
|
6024
|
+
* resize frame so the borders park offscreen and the client fills the
|
|
6025
|
+
* work area exactly, whether an app control, a drag-region double-click,
|
|
6026
|
+
* or the system menu maximized it. Writes that frame in screen
|
|
6027
|
+
* coordinates, plus the monitor rect it was taken from. */
|
|
6028
|
+
static bool chromelessMaximizedFrame(HWND hwnd, HMONITOR monitor_handle, RECT *maximized, RECT *monitor_rect) {
|
|
6029
|
+
MONITORINFO monitor = {};
|
|
6030
|
+
monitor.cbSize = sizeof(monitor);
|
|
6031
|
+
if (!GetMonitorInfoW(monitor_handle, &monitor)) return false;
|
|
6032
|
+
RECT frame = {};
|
|
6033
|
+
const DWORD style = (DWORD)GetWindowLongPtrW(hwnd, GWL_STYLE);
|
|
6034
|
+
const DWORD ex_style = (DWORD)GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
|
|
6035
|
+
if (!adjustWindowRectForDpi(&frame, style, GetMenu(hwnd) != nullptr, ex_style, dpiForWindow(hwnd))) return false;
|
|
6036
|
+
maximized->left = monitor.rcWork.left + frame.left;
|
|
6037
|
+
maximized->top = monitor.rcWork.top + frame.top;
|
|
6038
|
+
maximized->right = monitor.rcWork.right + frame.right;
|
|
6039
|
+
maximized->bottom = monitor.rcWork.bottom + frame.bottom;
|
|
6040
|
+
if (monitor_rect) *monitor_rect = monitor.rcMonitor;
|
|
6041
|
+
return true;
|
|
6042
|
+
}
|
|
6043
|
+
|
|
6044
|
+
static void applyChromelessMaximizedBounds(Host *host, HWND hwnd, MINMAXINFO *info) {
|
|
6045
|
+
if (!info || !chromelessWindowForHwnd(host, hwnd)) return;
|
|
6046
|
+
RECT maximized = {};
|
|
6047
|
+
RECT monitor = {};
|
|
6048
|
+
if (!chromelessMaximizedFrame(hwnd, MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &maximized, &monitor)) return;
|
|
6049
|
+
info->ptMaxPosition.x = maximized.left - monitor.left;
|
|
6050
|
+
info->ptMaxPosition.y = maximized.top - monitor.top;
|
|
6051
|
+
info->ptMaxSize.x = maximized.right - maximized.left;
|
|
6052
|
+
info->ptMaxSize.y = maximized.bottom - maximized.top;
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
/* WM_GETMINMAXINFO speaks in primary-monitor terms: when a window
|
|
6056
|
+
* maximizes onto another monitor, the system rescales those values by
|
|
6057
|
+
* the difference between the two monitors, which can overshoot a
|
|
6058
|
+
* monitor larger than the primary one. So the final frame of a maximized
|
|
6059
|
+
* chromeless window is set here, from the monitor it actually lands on,
|
|
6060
|
+
* whatever moved it: a maximize, or a maximized window moving to another
|
|
6061
|
+
* monitor. Pure z-order changes pass through. */
|
|
6062
|
+
static void clampChromelessMaximizedFrame(Host *host, HWND hwnd, WINDOWPOS *pos) {
|
|
6063
|
+
if (!pos || ((pos->flags & SWP_NOMOVE) && (pos->flags & SWP_NOSIZE))) return;
|
|
6064
|
+
if (!IsZoomed(hwnd) || !chromelessWindowForHwnd(host, hwnd)) return;
|
|
6065
|
+
RECT proposed = {};
|
|
6066
|
+
GetWindowRect(hwnd, &proposed);
|
|
6067
|
+
if (!(pos->flags & SWP_NOMOVE)) OffsetRect(&proposed, pos->x - proposed.left, pos->y - proposed.top);
|
|
6068
|
+
if (!(pos->flags & SWP_NOSIZE)) {
|
|
6069
|
+
proposed.right = proposed.left + pos->cx;
|
|
6070
|
+
proposed.bottom = proposed.top + pos->cy;
|
|
6071
|
+
}
|
|
6072
|
+
RECT maximized = {};
|
|
6073
|
+
if (!chromelessMaximizedFrame(hwnd, MonitorFromRect(&proposed, MONITOR_DEFAULTTONEAREST), &maximized, nullptr)) return;
|
|
6074
|
+
pos->x = maximized.left;
|
|
6075
|
+
pos->y = maximized.top;
|
|
6076
|
+
pos->cx = maximized.right - maximized.left;
|
|
6077
|
+
pos->cy = maximized.bottom - maximized.top;
|
|
6078
|
+
pos->flags &= ~(SWP_NOMOVE | SWP_NOSIZE);
|
|
6079
|
+
}
|
|
6080
|
+
|
|
5676
6081
|
static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
|
5677
6082
|
if (message == WM_NCCREATE) {
|
|
5678
6083
|
auto *create = reinterpret_cast<CREATESTRUCTW *>(lparam);
|
|
@@ -5733,21 +6138,11 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5733
6138
|
if (def_hit != HTCLIENT) return def_hit;
|
|
5734
6139
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
5735
6140
|
ScreenToClient(hwnd, &point);
|
|
5736
|
-
RECT client = {};
|
|
5737
|
-
GetClientRect(hwnd, &client);
|
|
5738
6141
|
/* Top resize band: the caption removal moved the top
|
|
5739
6142
|
* frame INSIDE the client area, so hand its band back to
|
|
5740
|
-
* the system
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
* diagonal grips survive at the very top. */
|
|
5744
|
-
if (chrome_window->resizable && !IsZoomed(hwnd) && point.y >= 0 && point.y < hiddenFrameTopThickness(hwnd)) {
|
|
5745
|
-
const UINT dpi = dpiForWindow(hwnd);
|
|
5746
|
-
const int corner = systemMetricForDpi(SM_CXSIZEFRAME, dpi) + systemMetricForDpi(SM_CXPADDEDBORDER, dpi);
|
|
5747
|
-
if (point.x < corner) return HTTOPLEFT;
|
|
5748
|
-
if (point.x >= client.right - corner) return HTTOPRIGHT;
|
|
5749
|
-
return HTTOP;
|
|
5750
|
-
}
|
|
6143
|
+
* the system. */
|
|
6144
|
+
const LRESULT band_hit = topResizeBandHit(*chrome_window, point);
|
|
6145
|
+
if (band_hit != HTNOWHERE) return band_hit;
|
|
5751
6146
|
/* Caption buttons when DwmDefWindowProc stayed silent
|
|
5752
6147
|
* (composition off, older cores): DefWindowProc still
|
|
5753
6148
|
* performs minimize/maximize/close for these hit codes,
|
|
@@ -5780,6 +6175,22 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5780
6175
|
break;
|
|
5781
6176
|
}
|
|
5782
6177
|
}
|
|
6178
|
+
/* Chromeless windows hand the top frame to the client (see
|
|
6179
|
+
* windowReclaimsTopFrame) while restored. A maximized window keeps
|
|
6180
|
+
* DefWindowProc's inset: its frame parks the borders offscreen around
|
|
6181
|
+
* the work area, and the inset is what lands the client exactly on
|
|
6182
|
+
* it. */
|
|
6183
|
+
if (message == WM_NCCALCSIZE && wparam) {
|
|
6184
|
+
Window *chromeless_window = chromelessWindowForHwnd(host, hwnd);
|
|
6185
|
+
if (chromeless_window && windowReclaimsTopFrame(*chromeless_window) && !IsZoomed(hwnd)) {
|
|
6186
|
+
NCCALCSIZE_PARAMS *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(lparam);
|
|
6187
|
+
const LONG original_top = params->rgrc[0].top;
|
|
6188
|
+
const LRESULT def_result = DefWindowProcW(hwnd, WM_NCCALCSIZE, wparam, lparam);
|
|
6189
|
+
if (def_result != 0) return def_result;
|
|
6190
|
+
params->rgrc[0].top = original_top;
|
|
6191
|
+
return 0;
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
5783
6194
|
/* Chromeless windows (WS_POPUP, no caption): DefWindowProc owns the
|
|
5784
6195
|
* resize frame when one exists; the app's window-drag regions are
|
|
5785
6196
|
* the only caption there is, so a client-area hit inside one
|
|
@@ -5792,6 +6203,8 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5792
6203
|
if (def_hit != HTCLIENT) return def_hit;
|
|
5793
6204
|
POINT point = { (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam) };
|
|
5794
6205
|
ScreenToClient(hwnd, &point);
|
|
6206
|
+
const LRESULT band_hit = topResizeBandHit(*chromeless_window, point);
|
|
6207
|
+
if (band_hit != HTNOWHERE) return band_hit;
|
|
5795
6208
|
if (windowDragRegionHit(host, *chromeless_window, point)) return HTCAPTION;
|
|
5796
6209
|
return HTCLIENT;
|
|
5797
6210
|
}
|
|
@@ -5905,6 +6318,9 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5905
6318
|
applyWebViewFrame(main->second);
|
|
5906
6319
|
}
|
|
5907
6320
|
#endif
|
|
6321
|
+
/* Size, maximize/restore, minimize, and dpi changes
|
|
6322
|
+
* all land here. */
|
|
6323
|
+
syncTopResizeStrip(host, entry.first);
|
|
5908
6324
|
emit(host, entry.second, kResize);
|
|
5909
6325
|
}
|
|
5910
6326
|
}
|
|
@@ -5925,6 +6341,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
5925
6341
|
}
|
|
5926
6342
|
}
|
|
5927
6343
|
}
|
|
6344
|
+
emitMaximizedTransition(host, hwnd);
|
|
5928
6345
|
return 0;
|
|
5929
6346
|
case WM_SETFOCUS:
|
|
5930
6347
|
case WM_KILLFOCUS:
|
|
@@ -6010,7 +6427,11 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6010
6427
|
for (auto &entry : host->windows) emit(host, entry.second, kFrame);
|
|
6011
6428
|
}
|
|
6012
6429
|
return 0;
|
|
6430
|
+
case WM_WINDOWPOSCHANGING:
|
|
6431
|
+
clampChromelessMaximizedFrame(host, hwnd, reinterpret_cast<WINDOWPOS *>(lparam));
|
|
6432
|
+
break;
|
|
6013
6433
|
case WM_GETMINMAXINFO:
|
|
6434
|
+
applyChromelessMaximizedBounds(host, hwnd, reinterpret_cast<MINMAXINFO *>(lparam));
|
|
6014
6435
|
if (host) {
|
|
6015
6436
|
for (auto &entry : host->windows) {
|
|
6016
6437
|
Window &window = entry.second;
|
|
@@ -6019,9 +6440,9 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6019
6440
|
/* The declared floor is a CONTENT size in LOGICAL
|
|
6020
6441
|
* points; scale to physical pixels at this window's
|
|
6021
6442
|
* DPI, then convert to the outer track size for its
|
|
6022
|
-
* current style.
|
|
6023
|
-
* chrome (the custom WM_NCCALCSIZE hands
|
|
6024
|
-
*
|
|
6443
|
+
* current style. Styles that reclaim their top frame
|
|
6444
|
+
* carry no top chrome (the custom WM_NCCALCSIZE hands
|
|
6445
|
+
* it to the client), so their conversion skips it
|
|
6025
6446
|
* too. */
|
|
6026
6447
|
const UINT dpi = dpiForWindow(hwnd);
|
|
6027
6448
|
const double scale = (double)dpi / 96.0;
|
|
@@ -6034,7 +6455,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6034
6455
|
adjustWindowRectForDpi(&frame, style, has_menu, ex_style, dpi);
|
|
6035
6456
|
LONG outer_width = frame.right - frame.left;
|
|
6036
6457
|
LONG outer_height = frame.bottom - frame.top;
|
|
6037
|
-
if (
|
|
6458
|
+
if (windowReclaimsTopFrame(window, has_menu)) {
|
|
6038
6459
|
const SIZE outer = hiddenOuterSizeForContent(style, ex_style, has_menu, min_content_width, min_content_height, dpi);
|
|
6039
6460
|
outer_width = outer.cx;
|
|
6040
6461
|
outer_height = outer.cy;
|
|
@@ -6093,6 +6514,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wparam, LPARA
|
|
|
6093
6514
|
destroyNativeViewsForWindow(host, entry.first);
|
|
6094
6515
|
destroyChildWebViewsForWindow(host, entry.first);
|
|
6095
6516
|
entry.second.hwnd = nullptr;
|
|
6517
|
+
entry.second.top_resize_strip = nullptr;
|
|
6096
6518
|
/* Destroyed means closed, never hidden. */
|
|
6097
6519
|
entry.second.policy_hidden = false;
|
|
6098
6520
|
emit(host, entry.second, kWindowFrame);
|
|
@@ -6162,9 +6584,9 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6162
6584
|
* re-derives the frame if it lands on another monitor), then grow
|
|
6163
6585
|
* it to the outer size for this style so the client rect lands at
|
|
6164
6586
|
* the request. The menu bar is attached after creation, so account
|
|
6165
|
-
* for it here when menus are declared.
|
|
6166
|
-
* custom-calc shape (no top chrome) — plain adjustment
|
|
6167
|
-
* their client one caption band tall. */
|
|
6587
|
+
* for it here when menus are declared. Styles that reclaim their top
|
|
6588
|
+
* frame use the custom-calc shape (no top chrome) — plain adjustment
|
|
6589
|
+
* would land their client one caption band (or top border) tall. */
|
|
6168
6590
|
const bool has_menu = !host->menus.empty();
|
|
6169
6591
|
const UINT dpi = systemDpi();
|
|
6170
6592
|
const double scale = (double)dpi / 96.0;
|
|
@@ -6174,7 +6596,7 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6174
6596
|
adjustWindowRectForDpi(&frame, style, has_menu ? TRUE : FALSE, 0, dpi);
|
|
6175
6597
|
LONG outer_width = frame.right - frame.left;
|
|
6176
6598
|
LONG outer_height = frame.bottom - frame.top;
|
|
6177
|
-
if (
|
|
6599
|
+
if (windowReclaimsTopFrame(window, has_menu)) {
|
|
6178
6600
|
const SIZE outer = hiddenOuterSizeForContent(style, 0, has_menu, content_width, content_height, dpi);
|
|
6179
6601
|
outer_width = outer.cx;
|
|
6180
6602
|
outer_height = outer.cy;
|
|
@@ -6206,13 +6628,14 @@ static bool createNativeWindow(Host *host, Window &window) {
|
|
|
6206
6628
|
/* Before the first show, so a dark-scheme launch never flashes a
|
|
6207
6629
|
* light caption. */
|
|
6208
6630
|
applyStandardTitlebarColorScheme(window);
|
|
6209
|
-
if (windowUsesHiddenTitlebar(window))
|
|
6210
|
-
|
|
6631
|
+
if (windowUsesHiddenTitlebar(window)) applyHiddenTitlebarFrame(window);
|
|
6632
|
+
if (windowReclaimsTopFrame(window)) {
|
|
6211
6633
|
/* The create-time WM_NCCALCSIZE ran before this window was
|
|
6212
6634
|
* registered under its HWND, so the custom calc did not apply;
|
|
6213
6635
|
* force one now that the map entry resolves. The callers keep
|
|
6214
6636
|
* `window` referencing the stored map entry for exactly this. */
|
|
6215
6637
|
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
|
6638
|
+
syncTopResizeStrip(host, window.id);
|
|
6216
6639
|
}
|
|
6217
6640
|
/* Initialize WS_EX_LAYERED through the same per-pixel path used by
|
|
6218
6641
|
* canvas presents. A layered HWND is otherwise not displayable until
|
|
@@ -6267,6 +6690,14 @@ Host *native_sdk_windows_create(const char *app_name, size_t app_name_len, const
|
|
|
6267
6690
|
host->app_name = slice(app_name, app_name_len);
|
|
6268
6691
|
host->window_title = slice(window_title, window_title_len);
|
|
6269
6692
|
host->bundle_id = slice(bundle_id, bundle_id_len);
|
|
6693
|
+
/* The taskbar groups and pins windows by AppUserModelID. Name the
|
|
6694
|
+
* process by its bundle id, the id the installer's Start menu
|
|
6695
|
+
* shortcut carries, so the taskbar follows the app instead of the
|
|
6696
|
+
* path of the executable that happened to start it. */
|
|
6697
|
+
if (!host->bundle_id.empty()) {
|
|
6698
|
+
const std::wstring app_user_model_id = widen(host->bundle_id);
|
|
6699
|
+
SetCurrentProcessExplicitAppUserModelID(app_user_model_id.c_str());
|
|
6700
|
+
}
|
|
6270
6701
|
host->icon_path = slice(icon_path, icon_path_len);
|
|
6271
6702
|
Window window;
|
|
6272
6703
|
window.id = 1;
|
|
@@ -6548,6 +6979,7 @@ int native_sdk_windows_load_window_webview(Host *host, uint64_t window_id, const
|
|
|
6548
6979
|
host->webviews.erase(key);
|
|
6549
6980
|
return 0;
|
|
6550
6981
|
}
|
|
6982
|
+
syncTopResizeStrip(host, window_id);
|
|
6551
6983
|
}
|
|
6552
6984
|
|
|
6553
6985
|
ChildWebView &webview = found->second;
|
|
@@ -6903,6 +7335,45 @@ int native_sdk_windows_minimize_window(Host *host, uint64_t window_id) {
|
|
|
6903
7335
|
return 1;
|
|
6904
7336
|
}
|
|
6905
7337
|
|
|
7338
|
+
/* Maximize and restore-from-maximized for app-drawn window controls.
|
|
7339
|
+
* Returns 1 on success, 0 for an unknown or destroyed window, -1 when the
|
|
7340
|
+
* window is not resizable (the system offers it no maximize either), and
|
|
7341
|
+
* -2 when it is not on the glass: SW_MAXIMIZE and SW_RESTORE both show a
|
|
7342
|
+
* hidden window, which would bypass the policy-hidden and deferred-show
|
|
7343
|
+
* bookkeeping. The state change reaches the runtime through WM_SIZE. */
|
|
7344
|
+
int native_sdk_windows_maximize_window(Host *host, uint64_t window_id) {
|
|
7345
|
+
if (!host) return 0;
|
|
7346
|
+
auto found = host->windows.find(window_id);
|
|
7347
|
+
if (found == host->windows.end() || !found->second.hwnd) return 0;
|
|
7348
|
+
if (!found->second.resizable) return -1;
|
|
7349
|
+
if (!IsWindowVisible(found->second.hwnd)) return -2;
|
|
7350
|
+
ShowWindow(found->second.hwnd, SW_MAXIMIZE);
|
|
7351
|
+
return 1;
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
int native_sdk_windows_unmaximize_window(Host *host, uint64_t window_id) {
|
|
7355
|
+
if (!host) return 0;
|
|
7356
|
+
auto found = host->windows.find(window_id);
|
|
7357
|
+
if (found == host->windows.end() || !found->second.hwnd) return 0;
|
|
7358
|
+
if (!IsZoomed(found->second.hwnd)) return 1;
|
|
7359
|
+
if (!IsWindowVisible(found->second.hwnd)) return -2;
|
|
7360
|
+
ShowWindow(found->second.hwnd, SW_RESTORE);
|
|
7361
|
+
return 1;
|
|
7362
|
+
}
|
|
7363
|
+
|
|
7364
|
+
/* The user-close request: post the same WM_CLOSE the caption close button
|
|
7365
|
+
* and Alt+F4 produce, so the WM_CLOSE handler applies close_policy.
|
|
7366
|
+
* native_sdk_windows_close_window stays the app's unconditional close.
|
|
7367
|
+
* Posting keeps the close out of the runtime call that requested it.
|
|
7368
|
+
* Returns 1 when posted, 0 for an unknown or destroyed window, -1 when
|
|
7369
|
+
* the post fails. */
|
|
7370
|
+
int native_sdk_windows_request_close_window(Host *host, uint64_t window_id) {
|
|
7371
|
+
if (!host) return 0;
|
|
7372
|
+
auto found = host->windows.find(window_id);
|
|
7373
|
+
if (found == host->windows.end() || !found->second.hwnd) return 0;
|
|
7374
|
+
return PostMessageW(found->second.hwnd, WM_CLOSE, 0, 0) ? 1 : -1;
|
|
7375
|
+
}
|
|
7376
|
+
|
|
6906
7377
|
int native_sdk_windows_hide_window(Host *host, uint64_t window_id) {
|
|
6907
7378
|
if (!host) return 0;
|
|
6908
7379
|
auto found = host->windows.find(window_id);
|
|
@@ -7913,6 +8384,7 @@ int native_sdk_windows_create_webview(Host *host, uint64_t window_id, const char
|
|
|
7913
8384
|
if (!createChildWebView(host, key)) {
|
|
7914
8385
|
DestroyWindow(hwnd);
|
|
7915
8386
|
host->webviews.erase(key);
|
|
8387
|
+
syncTopResizeStrip(host, window_id);
|
|
7916
8388
|
return 0;
|
|
7917
8389
|
}
|
|
7918
8390
|
return 1;
|
|
@@ -7937,6 +8409,7 @@ int native_sdk_windows_set_webview_frame(Host *host, uint64_t window_id, const c
|
|
|
7937
8409
|
found->second.controller->put_Bounds(bounds);
|
|
7938
8410
|
}
|
|
7939
8411
|
#endif
|
|
8412
|
+
syncTopResizeStrip(host, window_id);
|
|
7940
8413
|
return 1;
|
|
7941
8414
|
}
|
|
7942
8415
|
|
|
@@ -8016,6 +8489,7 @@ int native_sdk_windows_close_webview(Host *host, uint64_t window_id, const char
|
|
|
8016
8489
|
#endif
|
|
8017
8490
|
if (found->second.hwnd) DestroyWindow(found->second.hwnd);
|
|
8018
8491
|
host->webviews.erase(found);
|
|
8492
|
+
syncTopResizeStrip(host, window_id);
|
|
8019
8493
|
return 1;
|
|
8020
8494
|
}
|
|
8021
8495
|
|