-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
IslandWindow.cpp
1896 lines (1702 loc) · 71 KB
/
IslandWindow.cpp
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
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "IslandWindow.h"
#include "../types/inc/Viewport.hpp"
#include "resource.h"
#include "icon.h"
#include "NotificationIcon.h"
#include <dwmapi.h>
#include <TerminalThemeHelpers.h>
extern "C" IMAGE_DOS_HEADER __ImageBase;
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Composition;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Hosting;
using namespace winrt::Windows::Foundation::Numerics;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal;
using namespace ::Microsoft::Console::Types;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
#define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS"
#define IDM_SYSTEM_MENU_BEGIN 0x1000
const UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
IslandWindow::IslandWindow() noexcept :
_interopWindowHandle{ nullptr },
_rootGrid{ nullptr },
_source{ nullptr },
_pfnCreateCallback{ nullptr }
{
}
IslandWindow::~IslandWindow()
{
_source.Close();
}
HWND IslandWindow::GetInteropHandle() const
{
return _interopWindowHandle;
}
// Method Description:
// - Create the actual window that we'll use for the application.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::MakeWindow() noexcept
{
WNDCLASS wc{};
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hInstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
wc.lpszClassName = XAML_HOSTING_WINDOW_CLASS_NAME;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(IDI_APPICON));
RegisterClass(&wc);
WINRT_ASSERT(!_window);
// Create the window with the default size here - During the creation of the
// window, the system will give us a chance to set its size in WM_CREATE.
// WM_CREATE will be handled synchronously, before CreateWindow returns.
//
// We need WS_EX_NOREDIRECTIONBITMAP for vintage style opacity, GH#603
//
// WS_EX_LAYERED acts REAL WEIRD with TerminalTrySetTransparentBackground,
// but it works just fine when the window is in the TOPMOST group. But if
// you enable it always, activating the window will remove our DWM frame
// entirely. Weird.
WINRT_VERIFY(CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP | (_alwaysOnTop ? WS_EX_TOPMOST : 0),
wc.lpszClassName,
L"Windows Terminal",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
nullptr,
nullptr,
wc.hInstance,
this));
WINRT_ASSERT(_window);
}
// Method Description:
// - Called when no tab is remaining to close the window.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::Close()
{
PostQuitMessage(0);
}
// Method Description:
// - Set a callback to be called when we process a WM_CREATE message. This gives
// the AppHost a chance to resize the window to the proper size.
// Arguments:
// - pfn: a function to be called during the handling of WM_CREATE. It takes two
// parameters:
// * HWND: the HWND of the window that's being created.
// * til::rect: The position on the screen that the system has proposed for our
// window.
// Return Value:
// - <none>
void IslandWindow::SetCreateCallback(std::function<void(const HWND, const til::rect&, LaunchMode& launchMode)> pfn) noexcept
{
_pfnCreateCallback = pfn;
}
// Method Description:
// - Set a callback to be called when the window is being resized by user. For given
// requested window dimension (width or height, whichever border is dragged) it should
// return a resulting window dimension that is actually set. It is used to make the
// window 'snap' to the underling terminal's character grid.
// Arguments:
// - pfn: a function that transforms requested to actual window dimension.
// pfn's parameters:
// * widthOrHeight: whether the dimension is width (true) or height (false)
// * dimension: The requested dimension that comes from user dragging a border
// of the window. It is in pixels and represents only the client area.
// pfn's return value:
// * A dimension of client area that the window should resize to.
// Return Value:
// - <none>
void IslandWindow::SetSnapDimensionCallback(std::function<float(bool, float)> pfn) noexcept
{
_pfnSnapDimensionCallback = pfn;
}
// Method Description:
// - Handles a WM_CREATE message. Calls our create callback, if one's been set.
// Arguments:
// - wParam: unused
// - lParam: the lParam of a WM_CREATE, which is a pointer to a CREATESTRUCTW
// Return Value:
// - <none>
void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexcept
{
// Get proposed window rect from create structure
auto pcs = reinterpret_cast<CREATESTRUCTW*>(lParam);
til::rect rc;
rc.left = pcs->x;
rc.top = pcs->y;
rc.right = rc.left + pcs->cx;
rc.bottom = rc.top + pcs->cy;
auto launchMode = LaunchMode::DefaultMode;
if (_pfnCreateCallback)
{
_pfnCreateCallback(_window.get(), rc, launchMode);
}
auto nCmdShow = SW_SHOW;
if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode))
{
nCmdShow = SW_MAXIMIZE;
}
ShowWindow(_window.get(), nCmdShow);
UpdateWindow(_window.get());
UpdateWindowIconForActiveMetrics(_window.get());
}
// Method Description:
// - Handles a WM_SIZING message, which occurs when user drags a window border
// or corner. It intercepts this resize action and applies 'snapping' i.e.
// aligns the terminal's size to its cell grid. We're given the window size,
// which we then adjust based on the terminal's properties (like font size).
// Arguments:
// - wParam: Specifies which edge of the window is being dragged.
// - lParam: Pointer to the requested window rectangle (this is, the one that
// originates from current drag action). It also acts as the return value
// (it's a ref parameter).
// Return Value:
// - <none>
LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam)
{
if (!_pfnSnapDimensionCallback)
{
// If we haven't been given the callback that would adjust the dimension,
// then we can't do anything, so just bail out.
return false;
}
auto winRect = reinterpret_cast<LPRECT>(lParam);
// Find nearest monitor.
auto hmon = MonitorFromRect(winRect, MONITOR_DEFAULTTONEAREST);
// This API guarantees that dpix and dpiy will be equal, but neither is an
// optional parameter so give two UINTs.
UINT dpix = USER_DEFAULT_SCREEN_DPI;
UINT dpiy = USER_DEFAULT_SCREEN_DPI;
// If this fails, we'll use the default of 96. I think it can only fail for
// bad parameters, which we won't have, so no big deal.
LOG_IF_FAILED(GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy));
const long minWidthScaled = minimumWidth * dpix / USER_DEFAULT_SCREEN_DPI;
const auto nonClientSize = GetTotalNonClientExclusiveSize(dpix);
auto clientWidth = winRect->right - winRect->left - nonClientSize.width;
clientWidth = std::max(minWidthScaled, clientWidth);
auto clientHeight = winRect->bottom - winRect->top - nonClientSize.height;
// If we're the quake window, prevent resizing on all sides except the
// bottom. This also applies to resizing with the Alt+Space menu
if (IsQuakeWindow() && wParam != WMSZ_BOTTOM)
{
// Stuff our current window size into the lParam, and return true. This
// will tell User32 to use our current dimensions to resize to.
::GetWindowRect(_window.get(), winRect);
return true;
}
if (wParam != WMSZ_TOP && wParam != WMSZ_BOTTOM)
{
// If user has dragged anything but the top or bottom border (so e.g. left border,
// top-right corner etc.), then this means that the width has changed. We thus ask to
// adjust this new width so that terminal(s) is/are aligned to their character grid(s).
clientWidth = gsl::narrow_cast<decltype(clientWidth)>(_pfnSnapDimensionCallback(true, gsl::narrow_cast<float>(clientWidth)));
}
if (wParam != WMSZ_LEFT && wParam != WMSZ_RIGHT)
{
// Analogous to above, but for height.
clientHeight = gsl::narrow_cast<decltype(clientHeight)>(_pfnSnapDimensionCallback(false, gsl::narrow_cast<float>(clientHeight)));
}
// Now make the window rectangle match the calculated client width and height,
// regarding which border the user is dragging. E.g. if user drags left border, then
// we make sure to adjust the 'left' component of rectangle and not the 'right'. Note
// that top-left and bottom-left corners also 'include' left border, hence we match
// this in multi-case switch.
// Set width
switch (wParam)
{
case WMSZ_LEFT:
case WMSZ_TOPLEFT:
case WMSZ_BOTTOMLEFT:
winRect->left = winRect->right - (clientWidth + nonClientSize.width);
break;
case WMSZ_RIGHT:
case WMSZ_TOPRIGHT:
case WMSZ_BOTTOMRIGHT:
winRect->right = winRect->left + (clientWidth + nonClientSize.width);
break;
}
// Set height
switch (wParam)
{
case WMSZ_BOTTOM:
case WMSZ_BOTTOMLEFT:
case WMSZ_BOTTOMRIGHT:
winRect->bottom = winRect->top + (clientHeight + nonClientSize.height);
break;
case WMSZ_TOP:
case WMSZ_TOPLEFT:
case WMSZ_TOPRIGHT:
winRect->top = winRect->bottom - (clientHeight + nonClientSize.height);
break;
}
return true;
}
// Method Description:
// - Handle the WM_MOVING message
// - If we're the quake window, then we don't want to be able to be moved.
// Immediately return our current window position, which will prevent us from
// being moved at all.
// Arguments:
// - lParam: a LPRECT with the proposed window position, that should be filled
// with the resultant position.
// Return Value:
// - true iff we handled this message.
LRESULT IslandWindow::_OnMoving(const WPARAM /*wParam*/, const LPARAM lParam)
{
auto winRect = reinterpret_cast<LPRECT>(lParam);
// If we're the quake window, prevent moving the window. If we don't do
// this, then Alt+Space...Move will still be able to move the window.
if (IsQuakeWindow())
{
// Stuff our current window into the lParam, and return true. This
// will tell User32 to use our current position to move to.
::GetWindowRect(_window.get(), winRect);
return true;
}
return false;
}
void IslandWindow::Initialize()
{
_source = DesktopWindowXamlSource{};
auto interop = _source.as<IDesktopWindowXamlSourceNative>();
winrt::check_hresult(interop->AttachToWindow(_window.get()));
// stash the child interop handle so we can resize it when the main hwnd is resized
interop->get_WindowHandle(&_interopWindowHandle);
_rootGrid = winrt::Windows::UI::Xaml::Controls::Grid();
_source.Content(_rootGrid);
// initialize the taskbar object
if (auto taskbar = wil::CoCreateInstanceNoThrow<ITaskbarList3>(CLSID_TaskbarList))
{
if (SUCCEEDED(taskbar->HrInit()))
{
_taskbar = std::move(taskbar);
}
}
_systemMenuNextItemId = IDM_SYSTEM_MENU_BEGIN;
// Enable vintage opacity by removing the XAML emergency backstop, GH#603.
// We don't really care if this failed or not.
TerminalTrySetTransparentBackground(true);
}
void IslandWindow::OnSize(const UINT width, const UINT height)
{
// update the interop window size
SetWindowPos(_interopWindowHandle, nullptr, 0, 0, width, height, SWP_SHOWWINDOW | SWP_NOACTIVATE);
if (_rootGrid)
{
const auto size = GetLogicalSize();
_rootGrid.Width(size.Width);
_rootGrid.Height(size.Height);
}
}
// Method Description:
// - Handles a WM_GETMINMAXINFO message, issued before the window sizing starts.
// This message allows to modify the minimal and maximal dimensions of the window.
// We focus on minimal dimensions here
// (the maximal dimension will be calculate upon maximizing)
// Our goal is to protect against to downsizing to less than minimal allowed dimensions,
// that might occur in the scenarios where _OnSizing is bypassed.
// An example of such scenario is anchoring the window to the top/bottom screen border
// in order to maximize window height (GH# 8026).
// The computation is similar to what we do in _OnSizing:
// we need to consider both the client area and non-client exclusive area sizes,
// while taking DPI into account as well.
// Arguments:
// - lParam: Pointer to the requested MINMAXINFO struct,
// a ptMinTrackSize field of which we want to update with the computed dimensions.
// It also acts as the return value (it's a ref parameter).
// Return Value:
// - <none>
void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam)
{
// Without a callback we don't know to snap the dimensions of the client area.
// Should not be a problem, the callback is not set early in the startup
// The initial dimensions will be set later on
if (!_pfnSnapDimensionCallback)
{
return;
}
auto hmon = MonitorFromWindow(GetHandle(), MONITOR_DEFAULTTONEAREST);
if (hmon == NULL)
{
return;
}
UINT dpix = USER_DEFAULT_SCREEN_DPI;
UINT dpiy = USER_DEFAULT_SCREEN_DPI;
GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy);
// From now we use dpix for all computations (same as in _OnSizing).
const auto nonClientSizeScaled = GetTotalNonClientExclusiveSize(dpix);
auto lpMinMaxInfo = reinterpret_cast<LPMINMAXINFO>(lParam);
lpMinMaxInfo->ptMinTrackSize.x = _calculateTotalSize(true, minimumWidth * dpix / USER_DEFAULT_SCREEN_DPI, nonClientSizeScaled.width);
lpMinMaxInfo->ptMinTrackSize.y = _calculateTotalSize(false, minimumHeight * dpiy / USER_DEFAULT_SCREEN_DPI, nonClientSizeScaled.height);
}
// Method Description:
// - Helper function that calculates a single dimension value, given initialWindow and nonClientSizes
// Arguments:
// - isWidth: parameter to pass to SnapDimensionCallback.
// True if the method is invoked for width computation, false if for height.
// - clientSize: the size of the client area (already)
// - nonClientSizeScaled: the exclusive non-client size (already scaled)
// Return Value:
// - The total dimension
long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize, const long nonClientSize)
{
return gsl::narrow_cast<int>(_pfnSnapDimensionCallback(isWidth, gsl::narrow_cast<float>(clientSize)) + nonClientSize);
}
[[nodiscard]] LRESULT IslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
{
switch (message)
{
case WM_HOTKEY:
{
_HotkeyPressedHandlers(static_cast<long>(wparam));
return 0;
}
case WM_GETMINMAXINFO:
{
_OnGetMinMaxInfo(wparam, lparam);
return 0;
}
case WM_CREATE:
{
_HandleCreateWindow(wparam, lparam);
return 0;
}
case WM_SETFOCUS:
{
if (_interopWindowHandle != nullptr)
{
// send focus to the child window
SetFocus(_interopWindowHandle);
return 0;
}
break;
}
case WM_ACTIVATE:
{
// wparam = 0 indicates the window was deactivated
const bool activated = LOWORD(wparam) != 0;
_WindowActivatedHandlers(activated);
if (_autoHideWindow && !activated)
{
if (_isQuakeWindow || _minimizeToNotificationArea)
{
HideWindow();
}
else
{
ShowWindow(GetHandle(), SW_MINIMIZE);
}
}
break;
}
case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONUP:
case WM_NCMBUTTONDOWN:
case WM_NCMBUTTONUP:
case WM_NCRBUTTONDOWN:
case WM_NCRBUTTONUP:
case WM_NCXBUTTONDOWN:
case WM_NCXBUTTONUP:
{
// If we clicked in the titlebar, raise an event so the app host can
// dispatch an appropriate event.
_DragRegionClickedHandlers();
break;
}
case WM_MENUCHAR:
{
// GH#891: return this LRESULT here to prevent the app from making a
// bell when alt+key is pressed. A menu is active and the user presses a
// key that does not correspond to any mnemonic or accelerator key,
return MAKELRESULT(0, MNC_CLOSE);
}
case WM_SIZING:
{
return _OnSizing(wparam, lparam);
}
case WM_SIZE:
{
if (wparam == SIZE_RESTORED || wparam == SIZE_MAXIMIZED)
{
_WindowVisibilityChangedHandlers(true);
_MaximizeChangedHandlers(wparam == SIZE_MAXIMIZED);
}
if (wparam == SIZE_MINIMIZED)
{
_WindowVisibilityChangedHandlers(false);
if (_isQuakeWindow)
{
ShowWindow(GetHandle(), SW_HIDE);
return 0;
}
}
break;
}
case WM_MOVING:
{
return _OnMoving(wparam, lparam);
}
case WM_MOVE:
{
_WindowMovedHandlers();
break;
}
case WM_CLOSE:
{
// If the user wants to close the app by clicking 'X' button,
// we hand off the close experience to the app layer. If all the tabs
// are closed, the window will be closed as well.
_WindowCloseButtonClickedHandlers();
return 0;
}
case WM_MOUSEWHEEL:
try
{
// This whole handler is a hack for GH#979.
//
// On some laptops, their trackpads won't scroll inactive windows
// _ever_. With our entire window just being one giant XAML Island, the
// touchpad driver thinks our entire window is inactive, and won't
// scroll the XAML island. On those types of laptops, we'll get a
// WM_MOUSEWHEEL here, in our root window, when the trackpad scrolls.
// We're going to take that message and manually plumb it through to our
// TermControl's, or anything else that implements IMouseWheelListener.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms645617(v=vs.85).aspx
// Important! Do not use the LOWORD or HIWORD macros to extract the x-
// and y- coordinates of the cursor position because these macros return
// incorrect results on systems with multiple monitors. Systems with
// multiple monitors can have negative x- and y- coordinates, and LOWORD
// and HIWORD treat the coordinates as unsigned quantities.
const til::point eventPoint{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) };
// This mouse event is relative to the display origin, not the window. Convert here.
const til::rect windowRect{ GetWindowRect() };
const auto origin = windowRect.origin();
const auto relative = eventPoint - origin;
// Convert to logical scaling before raising the event.
const auto scale = GetCurrentDpiScale();
const til::point real{ til::math::flooring, relative.x / scale, relative.y / scale };
const auto wheelDelta = static_cast<short>(HIWORD(wparam));
// Raise an event, so any listeners can handle the mouse wheel event manually.
_MouseScrolledHandlers(real, wheelDelta);
return 0;
}
CATCH_LOG();
break;
case WM_THEMECHANGED:
UpdateWindowIconForActiveMetrics(_window.get());
return 0;
case WM_WINDOWPOSCHANGING:
{
// GH#10274 - if the quake window gets moved to another monitor via aero
// snap (win+shift+arrows), then re-adjust the size for the new monitor.
if (IsQuakeWindow())
{
// Retrieve the suggested dimensions and make a rect and size.
auto lpwpos = (LPWINDOWPOS)lparam;
// We only need to apply restrictions if the position is changing.
// The SWP_ flags are confusing to read. This is
// "if we're not moving the window, do nothing."
if (WI_IsFlagSet(lpwpos->flags, SWP_NOMOVE))
{
break;
}
// Figure out the suggested dimensions and position.
RECT rcSuggested;
rcSuggested.left = lpwpos->x;
rcSuggested.top = lpwpos->y;
rcSuggested.right = rcSuggested.left + lpwpos->cx;
rcSuggested.bottom = rcSuggested.top + lpwpos->cy;
// Find the bounds of the current monitor, and the monitor that
// we're suggested to be on.
auto current = MonitorFromWindow(_window.get(), MONITOR_DEFAULTTONEAREST);
MONITORINFO currentInfo;
currentInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(current, ¤tInfo);
auto proposed = MonitorFromRect(&rcSuggested, MONITOR_DEFAULTTONEAREST);
MONITORINFO proposedInfo;
proposedInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(proposed, &proposedInfo);
// If the monitor changed...
if (til::rect{ proposedInfo.rcMonitor } !=
til::rect{ currentInfo.rcMonitor })
{
const auto newWindowRect{ _getQuakeModeSize(proposed) };
// Inform User32 that we want to be placed at the position
// and dimensions that _getQuakeModeSize returned. When we
// snap across monitor boundaries, this will re-evaluate our
// size for the new monitor.
lpwpos->x = newWindowRect.left;
lpwpos->y = newWindowRect.top;
lpwpos->cx = newWindowRect.width();
lpwpos->cy = newWindowRect.height();
return 0;
}
}
break;
}
case CM_NOTIFY_FROM_NOTIFICATION_AREA:
{
switch (LOWORD(lparam))
{
case NIN_SELECT:
case NIN_KEYSELECT:
{
_NotifyNotificationIconPressedHandlers();
return 0;
}
case WM_CONTEXTMENU:
{
const til::point eventPoint{ GET_X_LPARAM(wparam), GET_Y_LPARAM(wparam) };
_NotifyShowNotificationIconContextMenuHandlers(eventPoint);
return 0;
}
}
break;
}
case WM_MENUCOMMAND:
{
_NotifyNotificationIconMenuItemSelectedHandlers((HMENU)lparam, (UINT)wparam);
return 0;
}
case WM_SYSCOMMAND:
{
// the low 4 bits contain additional information (that we don't care about)
auto highBits = wparam & 0xFFF0;
if (highBits == SC_RESTORE || highBits == SC_MAXIMIZE)
{
_MaximizeChangedHandlers(highBits == SC_MAXIMIZE);
}
if (wparam == SC_RESTORE && _fullscreen)
{
_ShouldExitFullscreenHandlers();
return 0;
}
auto search = _systemMenuItems.find(LOWORD(wparam));
if (search != _systemMenuItems.end())
{
search->second.callback();
}
break;
}
case WM_SETTINGCHANGE:
{
const std::wstring param{ (wchar_t*)lparam };
// ImmersiveColorSet seems to be the notification that the OS theme
// changed. If that happens, let the app know, so it can hot-reload
// themes, color schemes that might depend on the OS theme
if (param == L"ImmersiveColorSet")
{
_UpdateSettingsRequestedHandlers();
}
break;
}
case WM_ENDSESSION:
{
// For WM_QUERYENDSESSION and WM_ENDSESSION, refer to:
//
// https://docs.microsoft.com/en-us/windows/win32/rstmgr/guidelines-for-applications
//
// The OS will send us a WM_QUERYENDSESSION when it's preparing an
// update for our app. It will then send us a WM_ENDSESSION, which gives
// us a small timeout (~30s) to actually shut down gracefully. After
// that timeout, it will send us a WM_CLOSE. If we still don't close
// after the WM_CLOSE, it'll force-kill us (causing a crash which will be
// bucketed to MoAppHang).
//
// If we need to do anything to prepare for being told to shutdown,
// start it in WM_QUERYENDSESSION. If (in the future) we need to prevent
// logoff, we can return false there. (DefWindowProc returns true)
//
// The OS is going to shut us down here. We will manually start a quit,
// so that we can persist the state. If we refuse to gracefully shut
// down here, the OS will crash us to forcefully terminate us. We choose
// to quit here, rather than just close, to skip over any warning
// dialogs (e.g. "Are you sure you want to close all tabs?") which might
// prevent a WM_CLOSE from cleanly closing the window.
//
// This will cause a appHost._RequestQuitAll, which will notify the
// monarch to collect up all the window state and save it.
TraceLoggingWrite(
g_hWindowsTerminalProvider,
"EndSession",
TraceLoggingDescription("Emitted when the OS has sent a WM_ENDSESSION"),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE),
TraceLoggingKeyword(TIL_KEYWORD_TRACE));
_AutomaticShutdownRequestedHandlers();
return true;
}
default:
// We'll want to receive this message when explorer.exe restarts
// so that we can re-add our icon to the notification area.
// This unfortunately isn't a switch case because we register the
// message at runtime.
if (message == WM_TASKBARCREATED)
{
_NotifyReAddNotificationIconHandlers();
return 0;
}
}
// TODO: handle messages here...
return base_type::MessageHandler(message, wparam, lparam);
}
// Method Description:
// - Called when the window has been resized (or maximized)
// Arguments:
// - width: the new width of the window _in pixels_
// - height: the new height of the window _in pixels_
void IslandWindow::OnResize(const UINT width, const UINT height)
{
if (_interopWindowHandle)
{
OnSize(width, height);
}
}
// Method Description:
// - Called when the window is minimized to the taskbar.
void IslandWindow::OnMinimize()
{
// TODO GH#1989 Stop rendering island content when the app is minimized.
if (_minimizeToNotificationArea)
{
HideWindow();
}
}
// Method Description:
// - Called when the window is restored from having been minimized.
void IslandWindow::OnRestore()
{
// TODO GH#1989 Stop rendering island content when the app is minimized.
}
void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content)
{
_rootGrid.Children().Clear();
_rootGrid.Children().Append(content);
}
// Method Description:
// - Get the dimensions of our non-client area, as a rect where each component
// represents that side.
// - The .left will be a negative number, to represent that the actual side of
// the non-client area is outside the border of our window. It's roughly 8px (
// * DPI scaling) to the left of the visible border.
// - The .right component will be positive, indicating that the nonclient border
// is in the positive-x direction from the edge of our client area.
// - This will also include our titlebar! It's in the nonclient area for us.
// Arguments:
// - dpi: the scaling that we should use to calculate the border sizes.
// Return Value:
// - a til::rect whose components represent the margins of the nonclient area,
// relative to the client area.
til::rect IslandWindow::GetNonClientFrame(const UINT dpi) const noexcept
{
const auto windowStyle = static_cast<DWORD>(GetWindowLong(_window.get(), GWL_STYLE));
RECT islandFrame{};
// If we failed to get the correct window size for whatever reason, log
// the error and go on. We'll use whatever the control proposed as the
// size of our window, which will be at least close.
LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi));
return til::rect{ islandFrame };
}
// Method Description:
// - Gets the difference between window and client area size.
// Arguments:
// - dpi: dpi of a monitor on which the window is placed
// Return Value
// - The size difference
til::size IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept
{
const auto islandFrame{ GetNonClientFrame(dpi) };
return {
islandFrame.right - islandFrame.left,
islandFrame.bottom - islandFrame.top
};
}
void IslandWindow::OnAppInitialized()
{
// Do a quick resize to force the island to paint
const auto size = GetPhysicalSize();
OnSize(size.width, size.height);
}
// Method Description:
// - Called when the app wants to change its theme. We'll update the root UI
// element of the entire XAML tree, so that all UI elements get the theme
// applied.
// Arguments:
// - arg: the ElementTheme to use as the new theme for the UI
// Return Value:
// - <none>
void IslandWindow::OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme)
{
_rootGrid.RequestedTheme(requestedTheme);
// Invalidate the window rect, so that we'll repaint any elements we're
// drawing ourselves to match the new theme
::InvalidateRect(_window.get(), nullptr, false);
}
// Method Description:
// - Updates our focus mode state. See _SetIsBorderless for more details.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::FocusModeChanged(const bool focusMode)
{
// Do nothing if the value was unchanged.
if (focusMode == _borderless)
{
return;
}
_SetIsBorderless(focusMode);
}
// Method Description:
// - Updates our fullscreen state. See _SetIsFullscreen for more details.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::FullscreenChanged(const bool fullscreen)
{
// Do nothing if the value was unchanged.
if (fullscreen == _fullscreen)
{
return;
}
_SetIsFullscreen(fullscreen);
}
// Method Description:
// - Enter or exit the "always on top" state. Before the window is created, this
// value will later be used when we create the window to create the window on
// top of all others. After the window is created, it will either enter the
// group of topmost windows, or exit the group of topmost windows.
// Arguments:
// - alwaysOnTop: whether we should be entering or exiting always on top mode.
// Return Value:
// - <none>
void IslandWindow::SetAlwaysOnTop(const bool alwaysOnTop)
{
_alwaysOnTop = alwaysOnTop;
const auto hwnd = GetHandle();
if (hwnd)
{
SetWindowPos(hwnd,
_alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST,
0, // the window dimensions are unused, because we're passing SWP_NOSIZE
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
}
// Method Description:
// - Posts a message to the window message queue that the window visibility has changed
// and should then be minimized or restored.
// Arguments:
// - showOrHide: True for show; false for hide.
// Return Value:
// - <none>
void IslandWindow::ShowWindowChanged(const bool showOrHide)
{
if (const auto hwnd = GetHandle())
{
// IMPORTANT!
//
// ONLY "restore" if already minimized. If the window is maximized or
// snapped, a restore will restore-down the window instead.
if (showOrHide == true && ::IsIconic(hwnd))
{
::PostMessage(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
}
else if (showOrHide == false)
{
::PostMessage(hwnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
}
}
}
// Method Description
// - Flash the taskbar icon, indicating to the user that something needs their attention
void IslandWindow::FlashTaskbar()
{
// Using 'false' as the boolean argument ensures that the taskbar is
// only flashed if the app window is not focused
FlashWindow(_window.get(), false);
}
// Method Description:
// - Sets the taskbar progress indicator
// - We follow the ConEmu style for the state and progress values,
// more details at https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
// Arguments:
// - state: indicates the progress state
// - progress: indicates the progress value
void IslandWindow::SetTaskbarProgress(const size_t state, const size_t progress)
{
if (_taskbar)
{
switch (state)
{
case 0:
// removes the taskbar progress indicator
_taskbar->SetProgressState(_window.get(), TBPF_NOPROGRESS);
break;
case 1:
// sets the progress value to value given by the 'progress' parameter
_taskbar->SetProgressState(_window.get(), TBPF_NORMAL);
_taskbar->SetProgressValue(_window.get(), progress, 100);
break;
case 2:
// sets the progress indicator to an error state
_taskbar->SetProgressState(_window.get(), TBPF_ERROR);
_taskbar->SetProgressValue(_window.get(), progress, 100);
break;
case 3:
// sets the progress indicator to an indeterminate state.
// FIRST, set the progress to "no progress". That'll clear out any
// progress value from the previous state. Otherwise, a transition
// from (error,x%) or (warning,x%) to indeterminate will leave the
// progress value unchanged, and not show the spinner.
_taskbar->SetProgressState(_window.get(), TBPF_NOPROGRESS);
_taskbar->SetProgressState(_window.get(), TBPF_INDETERMINATE);
break;
case 4:
// sets the progress indicator to a pause state
_taskbar->SetProgressState(_window.get(), TBPF_PAUSED);
_taskbar->SetProgressValue(_window.get(), progress, 100);
break;
default:
break;
}
}
}
// From GdiEngine::s_SetWindowLongWHelper
void SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept
{
// SetWindowLong has strange error handling. On success, it returns the
// previous Window Long value and doesn't modify the Last Error state. To
// deal with this, we set the last error to 0/S_OK first, call it, and if
// the previous long was 0, we check if the error was non-zero before
// reporting. Otherwise, we'll get an "Error: The operation has completed
// successfully." and there will be another screenshot on the internet
// making fun of Windows. See:
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx
SetLastError(0);
const auto lResult = SetWindowLongW(hWnd, nIndex, dwNewLong);
if (0 == lResult)
{
LOG_LAST_ERROR_IF(0 != GetLastError());
}
}
// Method Description:
// - This is a helper to figure out what the window styles should be, given the
// current state of flags like borderless mode and fullscreen mode.
// Arguments:
// - <none>
// Return Value:
// - a LONG with the appropriate flags set for our current window mode, to be used with GWL_STYLE
LONG IslandWindow::_getDesiredWindowStyle() const
{
auto windowStyle = GetWindowLongW(GetHandle(), GWL_STYLE);
// If we're both fullscreen and borderless, fullscreen mode takes precedence.