@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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +47 -23
  3. package/build/cli.zig +22 -0
  4. package/native-sdk.d.ts +2 -0
  5. package/package.json +10 -10
  6. package/packages/core/package-lock.json +2 -2
  7. package/packages/core/package.json +1 -1
  8. package/schemas/app.schema.json +9 -1
  9. package/skill-data/ts-core/SKILL.md +2 -0
  10. package/src/app_runner/app_assets.zig +85 -1
  11. package/src/app_runner/root.zig +63 -3
  12. package/src/app_runner/ts_core_main.zig +8 -0
  13. package/src/platform/macos/appkit_host.h +9 -0
  14. package/src/platform/macos/appkit_host.m +366 -0
  15. package/src/platform/macos/root.zig +29 -6
  16. package/src/platform/null_platform.zig +66 -0
  17. package/src/platform/root.zig +1 -0
  18. package/src/platform/types.zig +46 -0
  19. package/src/platform/windows/root.zig +174 -0
  20. package/src/platform/windows/scriptc_mingw_compat.c +248 -0
  21. package/src/platform/windows/webview2_host.cpp +500 -26
  22. package/src/primitives/app_manifest/root.zig +1 -0
  23. package/src/primitives/app_manifest/tests.zig +35 -0
  24. package/src/primitives/app_manifest/types.zig +12 -0
  25. package/src/primitives/app_manifest/validation.zig +44 -6
  26. package/src/root.zig +1 -0
  27. package/src/runtime/bridge_responses.zig +2 -0
  28. package/src/runtime/channel_sources.zig +49 -1
  29. package/src/runtime/core.zig +5 -0
  30. package/src/runtime/effects.zig +12 -1
  31. package/src/runtime/flow.zig +6 -0
  32. package/src/runtime/flow_reference.zig +6 -0
  33. package/src/runtime/root.zig +4 -0
  34. package/src/runtime/single_instance.zig +893 -0
  35. package/src/runtime/tests.zig +1 -0
  36. package/src/runtime/window_controls.zig +73 -0
  37. package/src/runtime/window_controls_tests.zig +192 -0
  38. package/src/runtime/window_storage.zig +3 -0
  39. package/src/tooling/manifest.zig +111 -3
  40. package/src/tooling/package.zig +98 -8
  41. package/src/tooling/raw_manifest.zig +7 -0
  42. package/src/tooling/root.zig +2 -0
  43. package/src/tooling/templates.zig +53 -3
  44. package/src/tooling/update.zig +228 -10
  45. package/src/tooling/windows_installer.zig +245 -0
  46. package/src/tooling/zip_writer.zig +211 -0
  47. package/src/updater/engine.zig +936 -0
  48. package/src/updater/feed.zig +261 -12
  49. package/src/updater/macos.zig +187 -0
  50. package/src/updater/root.zig +41 -0
  51. package/src/updater/windows.zig +470 -0
  52. package/src/windows_install/config.zig +247 -0
  53. package/src/windows_install/main.zig +690 -0
  54. package/src/windows_install/payload.zig +146 -0
  55. package/src/windows_install/switch.zig +184 -0
  56. package/src/windows_install/tests.zig +11 -0
  57. package/src/windows_install/win32.zig +248 -0
  58. 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.empty() ? std::string(".") : 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 (chrome_window->resizable && !IsZoomed(chrome_window->hwnd) && point.y >= 0 && point.y < hiddenFrameTopThickness(chrome_window->hwnd)) return HTTRANSPARENT;
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, nullptr, nullptr, Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
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 — restored and resizable windows only (a
5741
- * maximized window does not resize, a fixed one never
5742
- * does). Corner slivers widen to the side borders so the
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. Hidden titlebar styles carry no top
6023
- * chrome (the custom WM_NCCALCSIZE hands the caption
6024
- * band to the client), so their conversion skips it
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 (windowUsesHiddenTitlebar(window)) {
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. Hidden styles use the
6166
- * custom-calc shape (no top chrome) — plain adjustment would land
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 (windowUsesHiddenTitlebar(window)) {
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
- applyHiddenTitlebarFrame(window);
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