-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathweb_host.cpp
More file actions
2263 lines (2079 loc) · 85.1 KB
/
Copy pathweb_host.cpp
File metadata and controls
2263 lines (2079 loc) · 85.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "web_host.hpp"
#include "external_url.hpp"
#include "web_host_close_policy.hpp"
#include "webview2_runtime_probe.hpp"
#include "window_background.hpp"
#include "window_chrome.hpp"
#include "window_size.hpp"
#include "../utils/encoding.hpp"
#include "../utils/logger.hpp"
#include <functional>
#include <limits>
#ifdef _WIN32
# ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
# endif
# ifndef NOMINMAX
# define NOMINMAX
# endif
#endif
#include <webview/webview.h>
#include <algorithm>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <utility>
#include <vector>
#ifdef _WIN32
# include <windows.h>
# include <wrl.h> // Microsoft::WRL::Callback,挂 WebView2 事件 handler
#endif
#if !defined(_WIN32) && !defined(__APPLE__)
# include <dlfcn.h>
#endif
#ifdef __APPLE__
# include <CoreGraphics/CoreGraphics.h>
# import <AppKit/AppKit.h>
# include <objc/runtime.h>
#endif
namespace acecode::desktop {
namespace {
// 全局 close_request_handler — 只在主线程上写,窗口回调在同一 GUI 主线程上读。
std::function<bool()> g_close_handler;
// 系统文件拖放 handler(plan: 桌面控制台拖放文件 → 插入完整路径)。Windows 的
// WebView2 事件回调 / macOS swizzle 的拖放回调命中文件时调它,把路径交给 main.cpp
// eval 回前端。主线程 only(WebView2 事件与 AppKit 拖放均在 GUI 主线程)。
std::function<void(std::vector<std::string>)> g_file_drop_handler;
// A second Desktop process can carry more intent than "show the window".
// main.cpp uses this GUI-thread callback to consume its one-shot open request.
std::function<void()> g_existing_instance_focus_handler;
#ifdef __APPLE__
std::function<void(bool)> g_mac_window_state_handler;
bool g_mac_last_known_maximized = false;
NSWindow* g_mac_reopen_window = nil;
webview::webview* g_mac_quit_webview = nullptr;
using MacApplicationReopenImp = BOOL (*)(id, SEL, NSApplication*, BOOL);
MacApplicationReopenImp g_mac_original_reopen_imp = nullptr;
NSString* const kAceCodeFocusExistingNotification =
@"dev.acecode.desktop.focusExisting.v1";
NSWindow* mac_window_from_host(webview::webview& w) {
auto window_result = w.window();
if (!window_result.ok() || !window_result.value()) return nil;
return static_cast<NSWindow*>(window_result.value());
}
void notify_mac_window_state_if_changed(NSWindow* window) {
if (!window) return;
const bool maximized = [window isZoomed] == YES;
if (maximized == g_mac_last_known_maximized) return;
g_mac_last_known_maximized = maximized;
if (g_mac_window_state_handler) {
g_mac_window_state_handler(maximized);
}
}
void hide_mac_standard_button(NSWindow* window, NSWindowButton button) {
NSButton* button_view = [window standardWindowButton:button];
if (!button_view) return;
[button_view setHidden:YES];
}
void hide_mac_titlebar_container(NSWindow* window) {
NSView* content_view = [window contentView];
NSView* frame_view = [content_view superview];
if (!content_view || !frame_view) return;
[content_view setFrame:[frame_view bounds]];
[content_view setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
for (NSView* subview in [frame_view subviews]) {
if (subview == content_view) continue;
[subview setHidden:YES];
}
[frame_view setNeedsLayout:YES];
}
void configure_mac_window_chrome(webview::webview& w) {
NSWindow* window = mac_window_from_host(w);
if (!window) return;
NSWindowStyleMask style = [window styleMask];
style |= NSWindowStyleMaskTitled;
style |= NSWindowStyleMaskClosable;
style |= NSWindowStyleMaskMiniaturizable;
style |= NSWindowStyleMaskResizable;
style |= NSWindowStyleMaskFullSizeContentView;
[window setStyleMask:style];
[window setHasShadow:YES];
[window setTitleVisibility:NSWindowTitleHidden];
[window setTitlebarAppearsTransparent:YES];
[window setToolbar:nil];
[window setMovableByWindowBackground:NO];
NSSize min_size = [window minSize];
min_size.width = std::max(
min_size.width,
static_cast<CGFloat>(kMinimumDesktopWindowWidth));
min_size.height = std::max(min_size.height, static_cast<CGFloat>(240.0));
[window setMinSize:min_size];
hide_mac_standard_button(window, NSWindowCloseButton);
hide_mac_standard_button(window, NSWindowMiniaturizeButton);
hide_mac_standard_button(window, NSWindowZoomButton);
hide_mac_titlebar_container(window);
g_mac_last_known_maximized = [window isZoomed] == YES;
}
void show_mac_window(NSWindow* window) {
if (!window) return;
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
if ([window isMiniaturized]) {
[window deminiaturize:nil];
}
[window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
}
void hide_mac_window_to_tray(NSWindow* window) {
if (!window) return;
[window orderOut:nil];
// Keep the regular activation policy so the Dock icon remains available as
// a restore affordance. Window close is not application quit.
}
BOOL acecode_application_should_handle_reopen(
id self,
SEL command,
NSApplication* application,
BOOL has_visible_windows) {
if (g_mac_reopen_window &&
(![g_mac_reopen_window isVisible] ||
[g_mac_reopen_window isMiniaturized])) {
show_mac_window(g_mac_reopen_window);
}
if (g_mac_original_reopen_imp) {
return g_mac_original_reopen_imp(
self, command, application, has_visible_windows);
}
return YES;
}
void install_mac_application_reopen_handler(NSWindow* window) {
if (!window) return;
g_mac_reopen_window = window;
id delegate = [NSApp delegate];
if (!delegate) return;
Class cls = object_getClass(delegate);
if (!cls) return;
const SEL selector =
@selector(applicationShouldHandleReopen:hasVisibleWindows:);
if (class_addMethod(
cls,
selector,
reinterpret_cast<IMP>(
acecode_application_should_handle_reopen),
"c@:@c")) {
return;
}
Method method = class_getInstanceMethod(cls, selector);
if (!method) return;
IMP current = method_getImplementation(method);
if (current == reinterpret_cast<IMP>(
acecode_application_should_handle_reopen)) {
return;
}
g_mac_original_reopen_imp =
reinterpret_cast<MacApplicationReopenImp>(current);
method_setImplementation(
method,
reinterpret_cast<IMP>(
acecode_application_should_handle_reopen));
}
void acecode_mac_request_quit(
id /*application*/,
SEL /*command*/,
id sender) {
if (g_mac_quit_webview) {
g_mac_quit_webview->terminate();
return;
}
[NSApp terminate:sender];
}
id install_mac_focus_existing_observer(webview::webview& w) {
NSWindow* window = mac_window_from_host(w);
if (!window) return nil;
return [[NSDistributedNotificationCenter defaultCenter]
addObserverForName:kAceCodeFocusExistingNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(__unused NSNotification* note) {
show_mac_window(window);
if (g_existing_instance_focus_handler) {
g_existing_instance_focus_handler();
}
}];
}
NSEvent* mac_synthetic_left_mouse_event(NSWindow* window) {
if (!window) return nil;
return [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
location:[window mouseLocationOutsideOfEventStream]
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:[window windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
}
bool mac_perform_window_drag(NSWindow* window) {
if (!window) return false;
NSEvent* event = mac_synthetic_left_mouse_event(window);
if (!event) return false;
[window performWindowDragWithEvent:event];
notify_mac_window_state_if_changed(window);
return true;
}
bool mac_area_uses_left_edge(FramelessHitTestArea area) {
return area == FramelessHitTestArea::Left ||
area == FramelessHitTestArea::TopLeft ||
area == FramelessHitTestArea::BottomLeft;
}
bool mac_area_uses_right_edge(FramelessHitTestArea area) {
return area == FramelessHitTestArea::Right ||
area == FramelessHitTestArea::TopRight ||
area == FramelessHitTestArea::BottomRight;
}
bool mac_area_uses_top_edge(FramelessHitTestArea area) {
return area == FramelessHitTestArea::Top ||
area == FramelessHitTestArea::TopLeft ||
area == FramelessHitTestArea::TopRight;
}
bool mac_area_uses_bottom_edge(FramelessHitTestArea area) {
return area == FramelessHitTestArea::Bottom ||
area == FramelessHitTestArea::BottomLeft ||
area == FramelessHitTestArea::BottomRight;
}
NSSize mac_min_window_size(NSWindow* window) {
NSSize min_size = window ? [window minSize] : NSMakeSize(0, 0);
min_size.width = std::max(
min_size.width,
static_cast<CGFloat>(kMinimumDesktopWindowWidth));
min_size.height = std::max(min_size.height, static_cast<CGFloat>(240.0));
return min_size;
}
NSRect mac_resized_frame(NSRect start_frame,
NSPoint start_mouse,
NSPoint current_mouse,
NSSize min_size,
FramelessHitTestArea area) {
const CGFloat dx = current_mouse.x - start_mouse.x;
const CGFloat dy = current_mouse.y - start_mouse.y;
const CGFloat min_width = std::max(min_size.width, static_cast<CGFloat>(1.0));
const CGFloat min_height = std::max(min_size.height, static_cast<CGFloat>(1.0));
const CGFloat start_right = NSMaxX(start_frame);
const CGFloat start_top = NSMaxY(start_frame);
NSRect next = start_frame;
if (mac_area_uses_left_edge(area)) {
next.origin.x = start_frame.origin.x + dx;
next.size.width = start_frame.size.width - dx;
if (next.size.width < min_width) {
next.size.width = min_width;
next.origin.x = start_right - min_width;
}
} else if (mac_area_uses_right_edge(area)) {
next.size.width = std::max(min_width, start_frame.size.width + dx);
}
if (mac_area_uses_bottom_edge(area)) {
next.origin.y = start_frame.origin.y + dy;
next.size.height = start_frame.size.height - dy;
if (next.size.height < min_height) {
next.size.height = min_height;
next.origin.y = start_top - min_height;
}
} else if (mac_area_uses_top_edge(area)) {
next.size.height = std::max(min_height, start_frame.size.height + dy);
}
return next;
}
bool mac_track_window_resize(NSWindow* window, FramelessHitTestArea area) {
if (!window || [window isZoomed]) return false;
if (!mac_area_uses_left_edge(area) && !mac_area_uses_right_edge(area) &&
!mac_area_uses_top_edge(area) && !mac_area_uses_bottom_edge(area)) {
return false;
}
const NSRect start_frame = [window frame];
const NSPoint start_mouse = [NSEvent mouseLocation];
const NSSize min_size = mac_min_window_size(window);
const NSEventMask mask = NSEventMaskLeftMouseDragged | NSEventMaskLeftMouseUp;
while (([NSEvent pressedMouseButtons] & 1) != 0) {
@autoreleasepool {
NSEvent* event = [NSApp nextEventMatchingMask:mask
untilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
if (!event) continue;
const NSEventType type = [event type];
if (type == NSEventTypeLeftMouseUp) break;
if (type != NSEventTypeLeftMouseDragged) continue;
const NSRect next = mac_resized_frame(start_frame,
start_mouse,
[NSEvent mouseLocation],
min_size,
area);
[window setFrame:next display:YES animate:NO];
}
}
notify_mac_window_state_if_changed(window);
return true;
}
BOOL acecode_mac_window_should_close(id /*self*/, SEL /*cmd*/, id /*sender*/) {
return dispatch_wm_close(g_close_handler) == CloseDispatch::ConsumedByHandler
? NO
: YES;
}
void install_mac_close_handler(webview::webview& w) {
NSWindow* window = mac_window_from_host(w);
if (!window) return;
id delegate = [window delegate];
if (!delegate) return;
Class cls = object_getClass(delegate);
if (!cls) return;
class_addMethod(cls,
@selector(windowShouldClose:),
reinterpret_cast<IMP>(acecode_mac_window_should_close),
"c@:@");
}
// macOS WKWebView 不自动转发 Cmd+C/V/X/A 等标准快捷键。
// 设置包含 Edit 菜单的 NSMenu 后,系统会通过 responder chain
// 将这些 keyEquivalent 路由到 WKWebView,从而使快捷键生效。
void install_mac_edit_menu(webview::webview& w) {
g_mac_quit_webview = &w;
const SEL quit_selector = NSSelectorFromString(@"acecodeRequestQuit:");
class_addMethod(
[NSApplication class],
quit_selector,
reinterpret_cast<IMP>(acecode_mac_request_quit),
"v@:@");
NSMenu* main_menu = [[NSMenu alloc] init];
// Application menu (macOS 要求第一个菜单项为应用菜单)
NSMenuItem* app_menu_item = [[NSMenuItem alloc] init];
NSMenu* app_menu = [[NSMenu alloc] init];
NSMenuItem* quit_item =
[app_menu addItemWithTitle:@"Quit ACECode"
action:quit_selector
keyEquivalent:@"q"];
[quit_item setTarget:NSApp];
[app_menu_item setSubmenu:app_menu];
[main_menu addItem:app_menu_item];
// Edit menu — 提供给 WKWebView 响应 Cmd+C/V/X/A/Z
NSMenuItem* edit_menu_item = [[NSMenuItem alloc] init];
NSMenu* edit_menu = [[NSMenu alloc] initWithTitle:@"Edit"];
[edit_menu addItemWithTitle:@"Undo"
action:@selector(undo:)
keyEquivalent:@"z"];
NSMenuItem* redo_item = [edit_menu addItemWithTitle:@"Redo"
action:@selector(redo:)
keyEquivalent:@"z"];
[redo_item setKeyEquivalentModifierMask:NSEventModifierFlagCommand |
NSEventModifierFlagShift];
[edit_menu addItem:[NSMenuItem separatorItem]];
[edit_menu addItemWithTitle:@"Cut"
action:@selector(cut:)
keyEquivalent:@"x"];
[edit_menu addItemWithTitle:@"Copy"
action:@selector(copy:)
keyEquivalent:@"c"];
[edit_menu addItemWithTitle:@"Paste"
action:@selector(paste:)
keyEquivalent:@"v"];
[edit_menu addItemWithTitle:@"Select All"
action:@selector(selectAll:)
keyEquivalent:@"a"];
[edit_menu_item setSubmenu:edit_menu];
[main_menu addItem:edit_menu_item];
[NSApp setMainMenu:main_menu];
}
// ── macOS 系统文件拖放接管 ─────────────────────────────────────────────
// WKWebView 默认把 Finder 文件拖放当网页内拖放/导航处理,JS 拿不到完整路径。
// 这里 swizzle WKWebView 类的拖放方法:pasteboard 含 fileURL(Finder 拖来)→
// 取出本地路径并回传;否则调原实现,不破坏网页内拖放(如把图片拖进编辑区)。
typedef BOOL (*PerformDragImp)(id, SEL, id);
typedef NSDragOperation (*DragOpImp)(id, SEL, id);
static PerformDragImp g_orig_perform_drag = nullptr;
static DragOpImp g_orig_dragging_entered = nullptr;
static DragOpImp g_orig_dragging_updated = nullptr;
static bool mac_drag_has_file_urls(id sender) {
NSPasteboard* pb = [sender draggingPasteboard];
return [pb canReadObjectForClasses:@[ [NSURL class] ]
options:@{NSPasteboardURLReadingFileURLsOnlyKey : @YES}];
}
static BOOL ace_perform_drag_operation(id self, SEL cmd, id sender) {
if (mac_drag_has_file_urls(sender)) {
NSArray<NSURL*>* urls = [[sender draggingPasteboard]
readObjectsForClasses:@[ [NSURL class] ]
options:@{NSPasteboardURLReadingFileURLsOnlyKey : @YES}];
std::vector<std::string> paths;
for (NSURL* url in urls) {
if (url.path) {
const char* p = [url.path UTF8String];
if (p) paths.emplace_back(p);
}
}
if (!paths.empty() && g_file_drop_handler) {
g_file_drop_handler(paths);
return YES;
}
}
if (g_orig_perform_drag) return g_orig_perform_drag(self, cmd, sender);
return NO;
}
static NSDragOperation ace_dragging_entered(id self, SEL cmd, id sender) {
if (mac_drag_has_file_urls(sender)) return NSDragOperationCopy;
if (g_orig_dragging_entered) return g_orig_dragging_entered(self, cmd, sender);
return NSDragOperationNone;
}
static NSDragOperation ace_dragging_updated(id self, SEL cmd, id sender) {
if (mac_drag_has_file_urls(sender)) return NSDragOperationCopy;
if (g_orig_dragging_updated) return g_orig_dragging_updated(self, cmd, sender);
return NSDragOperationNone;
}
void install_mac_file_drop(webview::webview& w) {
auto widget_result = w.widget();
if (!widget_result.ok() || !widget_result.value()) return;
NSView* view = static_cast<NSView*>(widget_result.value());
if (![view isKindOfClass:[NSView class]]) return;
Class cls = object_getClass(view); // WKWebView 实际类
if (!cls) return;
// 补注册 fileURL 拖放类型(WKWebView 已注册网页拖放类型,合并而非覆盖)。
@try {
NSMutableArray* types = [[view registeredDraggedTypes] mutableCopy];
if (!types) types = [NSMutableArray array];
if (![types containsObject:NSPasteboardTypeFileURL]) {
[types addObject:NSPasteboardTypeFileURL];
[view registerForDraggedTypes:types];
}
} @catch (...) {
}
auto swizzle = [&](SEL sel, IMP repl, const char* types, IMP* saved_orig) {
if (Method m = class_getInstanceMethod(cls, sel)) {
if (saved_orig) *saved_orig = method_getImplementation(m);
method_setImplementation(m, repl);
} else {
class_addMethod(cls, sel, repl, types);
}
};
swizzle(@selector(performDragOperation:), (IMP)ace_perform_drag_operation, "c@:@",
reinterpret_cast<IMP*>(&g_orig_perform_drag));
swizzle(@selector(draggingEntered:), (IMP)ace_dragging_entered, "Q@:@",
reinterpret_cast<IMP*>(&g_orig_dragging_entered));
swizzle(@selector(draggingUpdated:), (IMP)ace_dragging_updated, "Q@:@",
reinterpret_cast<IMP*>(&g_orig_dragging_updated));
}
#endif
} // namespace
#ifdef _WIN32
namespace {
constexpr wchar_t kHostWindowClassName[] = L"ACECodeDesktopHostWindow";
constexpr wchar_t kHostWindowPreviousProcProperty[] = L"ACECodeDesktopHostPreviousProc";
constexpr wchar_t kHostWindowStartupMonitorProperty[] = L"ACECodeDesktopStartupMonitor";
constexpr int kFramelessDragHeightDip = 44;
// WM_USER 区私有消息:绕过 close_request_handler 直接走 DestroyWindow。
// 选 0x10 偏移留出 0..0xF 给未来扩展;远离 webview/Common Controls 常用的
// WM_USER..WM_USER+0x100 区段。
constexpr UINT kRequestQuitMsg = WM_USER + 0x10;
// 窗口最大化/还原状态变化 handler。同样 main thread only。WndProc 在 WM_SIZE
// 时检测 IsZoomed 与 g_last_known_maximized 是否不同,变化时触发并同步缓存。
// 缓存初始 false,WM_SIZE 第一次到达时若为 maximized 会被推送一次,frontend
// 也照样会通过 aceDesktop_isWindowMaximized 拿初始态;两者最终一致。
std::function<void(bool)> g_window_state_handler;
bool g_last_known_maximized = false;
std::function<void(bool)> g_window_visibility_handler;
bool g_last_known_window_visible = true;
void notify_window_visibility(bool visible) {
if (visible == g_last_known_window_visible) return;
g_last_known_window_visible = visible;
if (g_window_visibility_handler) g_window_visibility_handler(visible);
}
// WebView2 does not reliably turn a native top-level activation into a DOM
// window.focus event. The Desktop main process uses this callback to dispatch
// an explicit frontend event after the host has completed WM_ACTIVATE handling.
std::function<void()> g_window_focus_handler;
// 单例联动:第二次启动 acecode-desktop 会向已有 host window 派 focus msg,
// 让我们把窗口拉前 + 显示。同名 RegisterWindowMessageW 在两端拿到一致 UINT,
// 见 single_instance_win.cpp 头注。第一次访问时 lazily register,缓存到 static。
UINT focus_existing_instance_msg() {
static UINT id = ::RegisterWindowMessageW(L"ACECode_FocusExistingInstance_v1");
return id;
}
WNDPROC previous_host_window_proc(HWND hwnd) {
return reinterpret_cast<WNDPROC>(::GetPropW(hwnd, kHostWindowPreviousProcProperty));
}
LRESULT call_host_default_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
if (WNDPROC previous = previous_host_window_proc(hwnd)) {
return ::CallWindowProcW(previous, hwnd, msg, wparam, lparam);
}
return ::DefWindowProcW(hwnd, msg, wparam, lparam);
}
int dpi_scale(int value, UINT dpi) {
return static_cast<int>((static_cast<long long>(value) * static_cast<long long>(dpi)) / 96);
}
UINT monitor_dpi(HMONITOR monitor) {
if (monitor) {
if (HMODULE shcore = ::LoadLibraryW(L"Shcore.dll")) {
using GetDpiForMonitorFn = HRESULT(WINAPI*)(HMONITOR, int, UINT*, UINT*);
auto get_dpi = reinterpret_cast<GetDpiForMonitorFn>(
::GetProcAddress(shcore, "GetDpiForMonitor"));
UINT dpi_x = 96;
UINT dpi_y = 96;
if (get_dpi && SUCCEEDED(get_dpi(monitor, 0, &dpi_x, &dpi_y)) &&
dpi_x > 0) {
::FreeLibrary(shcore);
return dpi_x;
}
::FreeLibrary(shcore);
}
}
HDC hdc = ::GetDC(nullptr);
const int fallback_dpi = hdc ? ::GetDeviceCaps(hdc, LOGPIXELSX) : 96;
if (hdc) ::ReleaseDC(nullptr, hdc);
return fallback_dpi > 0 ? static_cast<UINT>(fallback_dpi) : 96U;
}
RECT monitor_work_rect(HMONITOR monitor) {
RECT fallback{
0,
0,
::GetSystemMetrics(SM_CXSCREEN),
::GetSystemMetrics(SM_CYSCREEN),
};
if (!monitor) return fallback;
MONITORINFO info{};
info.cbSize = sizeof(info);
if (!::GetMonitorInfoW(monitor, &info)) return fallback;
return info.rcWork;
}
void apply_minimum_track_size(HWND hwnd, MINMAXINFO* info) {
if (!info) return;
HMONITOR monitor = reinterpret_cast<HMONITOR>(
::GetPropW(hwnd, kHostWindowStartupMonitorProperty));
if (!monitor) {
monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
}
UINT dpi = monitor_dpi(monitor);
if (dpi == 0) {
dpi = ::GetDpiForWindow(hwnd);
}
if (dpi == 0) dpi = 96;
LONG preferred_width = static_cast<LONG>(
dpi_scale(kMinimumDesktopWindowWidth, dpi));
LONG maximum_width = std::numeric_limits<LONG>::max();
if (monitor) {
const RECT work_area = monitor_work_rect(monitor);
const auto safe_default = fit_desktop_window_to_safe_work_area(
{kDefaultDesktopWindowWidth, kDefaultDesktopWindowHeight},
{work_area.right - work_area.left, work_area.bottom - work_area.top},
static_cast<int>(dpi));
maximum_width = std::max<LONG>(1, safe_default.width);
preferred_width = std::min(preferred_width, maximum_width);
}
info->ptMinTrackSize.x = std::min(
maximum_width,
std::max<LONG>(info->ptMinTrackSize.x, std::max<LONG>(1, preferred_width)));
}
HMONITOR active_monitor() {
if (HWND fg = ::GetForegroundWindow()) {
if (HMONITOR monitor = ::MonitorFromWindow(fg, MONITOR_DEFAULTTONEAREST)) {
return monitor;
}
}
POINT pt{};
if (::GetCursorPos(&pt)) {
if (HMONITOR monitor = ::MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST)) {
return monitor;
}
}
return ::MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
}
void resize_webview_widget(HWND hwnd) {
HWND widget = ::FindWindowExW(hwnd, nullptr, L"webview_widget", nullptr);
if (!widget) return;
RECT client{};
if (!::GetClientRect(hwnd, &client)) return;
::MoveWindow(widget, 0, 0, client.right - client.left, client.bottom - client.top, TRUE);
}
void refresh_non_client_frame(HWND hwnd) {
RECT rect{};
if (!::GetWindowRect(hwnd, &rect)) return;
::SetWindowPos(hwnd,
nullptr,
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE |
SWP_NOSIZE);
}
void log_webview_setting_failure(const char* operation, HRESULT hr) {
LOG_WARN(std::string("[desktop] ") + operation +
" failed, hr=" + std::to_string(static_cast<long>(hr)));
}
bool set_dev_tools_enabled(ICoreWebView2* core, BOOL enabled) {
if (!core) return false;
ICoreWebView2Settings* settings = nullptr;
HRESULT hr = core->get_Settings(&settings);
if (FAILED(hr) || !settings) {
log_webview_setting_failure("get_Settings for DevTools", hr);
return false;
}
hr = settings->put_AreDevToolsEnabled(enabled);
settings->Release();
if (FAILED(hr)) {
log_webview_setting_failure("put_AreDevToolsEnabled", hr);
return false;
}
return true;
}
bool open_dev_tools_for_core(ICoreWebView2* core) {
if (!core) return false;
// webview/webview disables DevTools when constructed with debug=false.
// Re-enable it only for ACECode's explicit F11/bridge entry point.
if (!set_dev_tools_enabled(core, TRUE)) return false;
HRESULT hr = core->OpenDevToolsWindow();
if (FAILED(hr)) {
log_webview_setting_failure("OpenDevToolsWindow", hr);
return false;
}
return true;
}
void install_dev_tools_shortcut(ICoreWebView2Controller* controller) {
if (!controller) return;
using Microsoft::WRL::Callback;
EventRegistrationToken token{};
HRESULT hr = controller->add_AcceleratorKeyPressed(
Callback<ICoreWebView2AcceleratorKeyPressedEventHandler>(
[](ICoreWebView2Controller* sender,
ICoreWebView2AcceleratorKeyPressedEventArgs* args) -> HRESULT {
if (!sender || !args) return S_OK;
COREWEBVIEW2_KEY_EVENT_KIND kind{};
UINT virtual_key = 0;
if (FAILED(args->get_KeyEventKind(&kind)) ||
FAILED(args->get_VirtualKey(&virtual_key))) {
return S_OK;
}
const bool key_down =
kind == COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN ||
kind == COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN;
if (!key_down || virtual_key != VK_F11) return S_OK;
args->put_Handled(TRUE);
ICoreWebView2* core = nullptr;
if (SUCCEEDED(sender->get_CoreWebView2(&core)) && core) {
open_dev_tools_for_core(core);
core->Release();
}
return S_OK;
})
.Get(),
&token);
if (FAILED(hr)) {
log_webview_setting_failure("add_AcceleratorKeyPressed for F11 DevTools", hr);
}
}
void configure_browser_defaults(webview::webview& host) {
auto controller_result = host.browser_controller();
if (!controller_result.ok()) {
LOG_WARN("[desktop] browser_controller unavailable; browser defaults not configured");
return;
}
auto* controller = static_cast<ICoreWebView2Controller*>(controller_result.value());
if (!controller) return;
HRESULT hr = controller->put_ZoomFactor(1.0);
if (FAILED(hr)) {
log_webview_setting_failure("put_ZoomFactor", hr);
}
ICoreWebView2* core = nullptr;
hr = controller->get_CoreWebView2(&core);
if (FAILED(hr) || !core) {
log_webview_setting_failure("get_CoreWebView2", hr);
return;
}
install_dev_tools_shortcut(controller);
ICoreWebView2Settings* settings = nullptr;
hr = core->get_Settings(&settings);
core->Release();
if (FAILED(hr) || !settings) {
log_webview_setting_failure("get_Settings", hr);
return;
}
hr = settings->put_IsZoomControlEnabled(FALSE);
if (FAILED(hr)) {
log_webview_setting_failure("put_IsZoomControlEnabled", hr);
}
ICoreWebView2Settings5* settings5 = nullptr;
hr = settings->QueryInterface(IID_PPV_ARGS(&settings5));
if (SUCCEEDED(hr) && settings5) {
const HRESULT pinch_hr = settings5->put_IsPinchZoomEnabled(FALSE);
if (FAILED(pinch_hr)) {
log_webview_setting_failure("put_IsPinchZoomEnabled", pinch_hr);
}
settings5->Release();
}
ICoreWebView2Settings6* settings6 = nullptr;
hr = settings->QueryInterface(IID_PPV_ARGS(&settings6));
if (SUCCEEDED(hr) && settings6) {
const HRESULT swipe_hr = settings6->put_IsSwipeNavigationEnabled(FALSE);
if (FAILED(swipe_hr)) {
log_webview_setting_failure("put_IsSwipeNavigationEnabled", swipe_hr);
}
settings6->Release();
}
settings->Release();
}
// ── 窗口打底色(快速 resize 防黑边)────────────────────────────────────
// 窗口树三层(host HWND → webview_widget → WebView2 合成器)没有任何一层
// 自带背景 — 两个窗口类的 hbrBackground 都是 NULL,放大窗口时新暴露区域
// 不被擦除,肉眼看到黑闪;WebView2 自己的 DefaultBackgroundColor 默认白,
// 暗色主题下则闪白。修法与 Electron/Tauri 同构:三层涂同一个颜色,启动
// 默认取前端浅色 body 底色(kDefaultWindowBackground = #f5f5f2),前端
// ThemeProvider 再按实际主题经 aceDesktop_setWindowBackgroundColor 推送。
COLORREF background_colorref(WindowBackgroundColor color) {
return RGB(color.r, color.g, color.b);
}
// 换掉窗口**类**的背景刷。类刷子由 DefWindowProc 在 WM_ERASEBKGND 时使用,
// 恰好覆盖 webview 库注册的 webview_widget / webview 类(它们的 wndproc 把
// 未处理消息都交给 DefWindowProc,库源码零改动)。旧刷子恒为 CreateSolidBrush
// 产物(或 NULL),DeleteObject 安全 — 本文件从不使用 COLOR_* 系统色伪句柄。
void apply_class_background_brush(HWND hwnd, WindowBackgroundColor color) {
if (!hwnd || !::IsWindow(hwnd)) return;
HBRUSH brush = ::CreateSolidBrush(background_colorref(color));
if (!brush) return;
::SetLastError(ERROR_SUCCESS);
LONG_PTR previous = ::SetClassLongPtrW(
hwnd, GCLP_HBRBACKGROUND, reinterpret_cast<LONG_PTR>(brush));
if (previous == 0 && ::GetLastError() != ERROR_SUCCESS) {
// 类刷子没换上,把新刷子收回,避免 GDI 句柄泄漏。
::DeleteObject(brush);
return;
}
if (previous != 0) {
::DeleteObject(reinterpret_cast<HBRUSH>(previous));
}
}
// WebView2 合成器自己的打底色(页面未渲染区域,如导航间隙 / resize 追帧)。
// 启动首帧之前的窗口由 WEBVIEW2_DEFAULT_BACKGROUND_COLOR 环境变量负责
// (Impl 构造体首,官方文档:仅属性设置在生效前仍会闪默认白);运行时主题
// 切换走这里的属性更新。
void apply_webview2_default_background(webview::webview& host,
WindowBackgroundColor color) {
auto controller_result = host.browser_controller();
if (!controller_result.ok()) return;
auto* controller = static_cast<ICoreWebView2Controller*>(controller_result.value());
if (!controller) return;
ICoreWebView2Controller2* controller2 = nullptr;
HRESULT hr = controller->QueryInterface(IID_PPV_ARGS(&controller2));
if (FAILED(hr) || !controller2) {
log_webview_setting_failure("QueryInterface ICoreWebView2Controller2", hr);
return;
}
COREWEBVIEW2_COLOR background{255, color.r, color.g, color.b};
hr = controller2->put_DefaultBackgroundColor(background);
controller2->Release();
if (FAILED(hr)) {
log_webview_setting_failure("put_DefaultBackgroundColor", hr);
}
}
// 三层统一入口:host 窗口类 + webview_widget 类 + WebView2 合成器。
// host_hwnd 在 offscreen 路径是我们的 ACECodeDesktopHostWindow 类(注册时
// 已带默认刷,这里换成等价新刷无害),在降级路径是 webview 库自建的
// `webview` 类(NULL 刷,必须靠这里补上)。GUI 主线程 only。
void apply_window_background(webview::webview& host,
HWND host_hwnd,
WindowBackgroundColor color) {
apply_class_background_brush(host_hwnd, color);
auto widget_result = host.widget();
if (widget_result.ok() && widget_result.value()) {
apply_class_background_brush(static_cast<HWND>(widget_result.value()), color);
}
apply_webview2_default_background(host, color);
}
// ── Windows 系统文件拖放 + 外部新窗口接管 ─────────────────────────────
// 首选入口是页面把一次 drop 的完整 DOM File 批次作为 WebMessage additional
// objects 发回来。ICoreWebView2File::get_Path 给 native-confirmed absolute path,
// 不信任页面自己拼的 path 字符串,也不会把多选拆成若干不完整的导航事件。
//
// 旧 Runtime / bridge 不可用时仍保留 file:// NavigationStarting / NewWindowRequested
// 拦截:至少防整页跳转 / 打开文件,并为历史单文件拖放提供兼容兜底。新窗口中的
// http(s) 继续交给系统默认浏览器,普通同页导航不受影响。
constexpr wchar_t kWindowsFilesystemDropMessage[] =
L"acecode:native-filesystem-drop:v1";
bool win_is_file_uri(const std::wstring& uri) {
if (uri.size() < 5) return false;
auto lower = [](wchar_t c) { return static_cast<wchar_t>(::towlower(c)); };
return lower(uri[0]) == L'f' && lower(uri[1]) == L'i' && lower(uri[2]) == L'l' &&
lower(uri[3]) == L'e' && uri[4] == L':';
}
void dispatch_file_uri(const std::wstring& uri) {
if (g_file_drop_handler) {
g_file_drop_handler({acecode::wide_to_utf8(uri)});
}
}
std::vector<std::string> win_web_message_file_paths(
ICoreWebView2WebMessageReceivedEventArgs* args) {
if (!args) return {};
LPWSTR raw_message = nullptr;
const HRESULT message_hr = args->TryGetWebMessageAsString(&raw_message);
const std::wstring message = raw_message ? raw_message : L"";
::CoTaskMemFree(raw_message);
if (FAILED(message_hr) || message != kWindowsFilesystemDropMessage) return {};
Microsoft::WRL::ComPtr<ICoreWebView2WebMessageReceivedEventArgs2> args2;
if (
FAILED(args->QueryInterface(IID_PPV_ARGS(args2.GetAddressOf()))) || !args2) {
return {};
}
Microsoft::WRL::ComPtr<ICoreWebView2ObjectCollectionView> objects;
if (FAILED(args2->get_AdditionalObjects(objects.GetAddressOf())) || !objects) {
return {};
}
UINT32 count = 0;
if (FAILED(objects->get_Count(&count)) || count == 0) return {};
std::vector<std::string> paths;
paths.reserve(count);
for (UINT32 index = 0; index < count; ++index) {
Microsoft::WRL::ComPtr<IUnknown> object;
if (FAILED(objects->GetValueAtIndex(index, object.GetAddressOf())) || !object) {
continue;
}
Microsoft::WRL::ComPtr<ICoreWebView2File> file;
if (FAILED(object.As(&file)) || !file) continue;
LPWSTR raw_path = nullptr;
const HRESULT path_hr = file->get_Path(&raw_path);
const std::string path = raw_path ? acecode::wide_to_utf8(raw_path) : "";
::CoTaskMemFree(raw_path);
if (FAILED(path_hr) || path.empty()) continue;
if (std::find(paths.begin(), paths.end(), path) == paths.end()) {
paths.push_back(path);
}
}
return paths;
}
void install_win_webview_navigation_handlers(webview::webview& host) {
auto controller_result = host.browser_controller();
if (!controller_result.ok()) return;
auto* controller = static_cast<ICoreWebView2Controller*>(controller_result.value());
if (!controller) return;
ICoreWebView2* core = nullptr;
if (FAILED(controller->get_CoreWebView2(&core)) || !core) return;
using Microsoft::WRL::Callback;
EventRegistrationToken drop_message_token{};
core->add_WebMessageReceived(
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[](ICoreWebView2*,
ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
auto paths = win_web_message_file_paths(args);
if (!paths.empty() && g_file_drop_handler) {
g_file_drop_handler(std::move(paths));
}
return S_OK;
})
.Get(),
&drop_message_token);
EventRegistrationToken nav_token{};
core->add_NavigationStarting(
Callback<ICoreWebView2NavigationStartingEventHandler>(
[](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
if (!args) return S_OK;
LPWSTR uri = nullptr;
if (SUCCEEDED(args->get_Uri(&uri)) && uri) {
std::wstring u(uri);
::CoTaskMemFree(uri);
if (win_is_file_uri(u)) {
args->put_Cancel(TRUE);
dispatch_file_uri(u);
}
}
return S_OK;
})
.Get(),
&nav_token);
EventRegistrationToken win_token{};