diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..412aede
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+**/bin/
+**/obj/
+/Publish/
diff --git a/Directory.Build.targets b/Directory.Build.targets
index cbf11f5..0a6e040 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -4,8 +4,8 @@
false
false
- true
- true
+ true
+ true
false
diff --git a/Microsoft.Terminal.WinUI3/HwndHost.cs b/Microsoft.Terminal.WinUI3/HwndHost.cs
index 0a3f2ad..ac0cde0 100644
--- a/Microsoft.Terminal.WinUI3/HwndHost.cs
+++ b/Microsoft.Terminal.WinUI3/HwndHost.cs
@@ -26,6 +26,23 @@ namespace System.Windows.Interop {
/// A mostly complete hackish reimplementation of the WPF HwndHost, draw code specifically removed as Terminal did not need but can be ported
///
internal abstract class HwndHost : FrameworkElement, IDisposable, IKeyboardInputSink {
+ [System.Runtime.InteropServices.DllImport("gdi32.dll", SetLastError = true)]
+ private static extern IntPtr CreateRectRgn(int left, int top, int right, int bottom);
+
+ [System.Runtime.InteropServices.DllImport("gdi32.dll", SetLastError = true)]
+ [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
+ private static extern bool DeleteObject(IntPtr hObject);
+
+ [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
+ private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn,
+ [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)] bool redraw);
+
+ private double? _leftOverlayBoundaryInXamlRootDips;
+ private bool _hasAppliedWindowRegion;
+ private int _lastRegionLeft = -1;
+ private int _lastRegionWidth = -1;
+ private int _lastRegionHeight = -1;
+
static HwndHost() {
}
@@ -67,12 +84,26 @@ public IntPtr Handle {
if (_hwnd != HWND.Null) {
if (!PInvoke.IsWindow(_hwnd)) {
_hwnd = HWND.Null;
- }
- }
-
+ }
+}
return _hwnd;
}
}
+
+ internal void SetLeftOverlayBoundary(double? boundaryInXamlRootDips) {
+ if (_leftOverlayBoundaryInXamlRootDips == boundaryInXamlRootDips) {
+ return;
+ }
+
+ _leftOverlayBoundaryInXamlRootDips = boundaryInXamlRootDips;
+ _lastRegionLeft = -1;
+ _lastRegionWidth = -1;
+ _lastRegionHeight = -1;
+
+ if (!_hwnd.IsNull) {
+ QueueWindowPositionUpdate();
+ }
+ }
protected HWND XAMLParentWindow {
get {
if (_XAMLParentWindow.IsNull)
@@ -149,6 +180,7 @@ private void xamlParentWndProc(object sender, WindowMessageEventArgs e) {
protected WindowMessageMonitor xamlParentMessageMonitor;
protected WindowMessageMonitor _hwndSubclassHook;
+ private long? _visibilityChangedToken;
///
/// An event that is notified of all unhandled messages received
/// by the hosted window.
@@ -177,7 +209,8 @@ public event DpiChangedEventHandler DpiChanged {
///
protected virtual void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) {
//RaiseEvent(new DpiChangedEventArgs(oldDpi, newDpi, HwndHost.DpiChangedEvent, this));
- UpdateWindowPos();
+ InvalidateWindowPositionCache();
+ QueueWindowPositionUpdate();
}
@@ -422,12 +455,12 @@ public void UpdateWindowPos() {
Rect GetBoundsRelativeToXAMLWindowRasterScaled() {
if (this.Parent == null || XAMLParentWindow.IsNull)
throw new Exception("We are not part of the UI or the parent is not yet set");
- var Pt = GetTransformToXamlRootContent().TransformPoint(
- new Point(0, 0)
- );
+ var point = GetTransformToXamlRootContent().TransformPoint(new Point(0, 0));
+ var rasterizationScale = this.XamlRoot.RasterizationScale;
+ point = new Point(point.X * rasterizationScale, point.Y * rasterizationScale);
var toSize = ActualSize.ToSize();
- toSize = new Size(toSize.Width * this.XamlRoot.RasterizationScale, toSize.Height * this.XamlRoot.RasterizationScale);
- return AdjustRectForDpi(new Rect(Pt, toSize));
+ toSize = new Size(toSize.Width * rasterizationScale, toSize.Height * rasterizationScale);
+ return AdjustRectForDpi(new Rect(point, toSize));
}
///
@@ -455,7 +488,7 @@ private Rect AdjustRectForDpi(Rect rcRect) {
return rcRect;
double dpiRatio = DpiParentToChildRatio;
- return new(rcRect.Left / dpiRatio, rcRect.Top / dpiRatio, rcRect.Right / dpiRatio, rcRect.Bottom / dpiRatio);
+ return new(rcRect.X / dpiRatio, rcRect.Y / dpiRatio, rcRect.Width / dpiRatio, rcRect.Height / dpiRatio);
}
///
@@ -481,12 +514,28 @@ protected virtual void Dispose(bool disposing) {
// Verify the thread has access to the context.
#pragma warning suppress 6519
+ this.SizeChanged -= OnSizeChanged;
+ this.PreviewKeyDown -= OnKeyDown;
+ this.PreviewKeyUp -= OnKeyUp;
+ if (_visibilityChangedToken is long visibilityChangedToken) {
+ this.UnregisterPropertyChangedCallback(UIElement.VisibilityProperty, visibilityChangedToken);
+ _visibilityChangedToken = null;
+ }
+
+ if (xamlParentMessageMonitor != null) {
+ xamlParentMessageMonitor.WindowMessageReceived -= xamlParentWndProc;
+ xamlParentMessageMonitor.Dispose();
+ xamlParentMessageMonitor = null;
+ _XAMLParentWindow = HWND.Null;
+ }
+
// Remove our subclass. Even if this fails, it will be forcably removed
// when the window is destroyed.
if (_hwndSubclassHook != null) {
// Check if it is trusted (WebOC and AddInHost), call CriticalDetach to avoid the Demand.
+ _hwndSubclassHook.WindowMessageReceived -= SubclassWndProc;
_hwndSubclassHook.Dispose();
_hwndSubclassHook = null;
@@ -537,6 +586,20 @@ private void OnDispatcherShutdown(object sender, object e) {
///
protected abstract void DestroyWindowCore(HWND hwnd);
+ ///
+ /// Called when the hosted HWND is destroyed outside .
+ /// Implementations can invalidate native state without destroying the HWND
+ /// reentrantly from inside WM_NCDESTROY.
+ ///
+ protected virtual void OnHostedWindowDestroyed(HWND hwnd) {
+ }
+
+ ///
+ /// Called after the hosted HWND has accepted its XAML layout rectangle.
+ ///
+ protected virtual void OnHostedWindowPositionApplied(int width, int height) {
+ }
+
///
@@ -550,9 +613,17 @@ protected virtual void WndProc(WindowMessageEventArgs e) {
var msg = e.Message;
switch ((WindowsMessages)e.Message.MessageId) {
- case WindowsMessages.NCDESTROY:
+ case WindowsMessages.NCDESTROY: {
+ var destroyedHwnd = _hwnd == HWND.Null ? new HWND(msg.Hwnd) : _hwnd;
_hwnd = HWND.Null;
+ InvalidateWindowPositionCache();
+ _hasAppliedWindowRegion = false;
+ _lastRegionLeft = -1;
+ _lastRegionWidth = -1;
+ _lastRegionHeight = -1;
+ OnHostedWindowDestroyed(destroyedHwnd);
break;
+ }
// When layout happens, we first calculate the right size/location then call SetWindowPos.
// We only allow the changes that are coming from Avalon layout. The hwnd is not allowed to change by itself.
@@ -641,21 +712,112 @@ private IntPtr OnWmGetObject(IntPtr wparam, IntPtr lparam) {
///
///
protected virtual void OnWindowPositionChanged(Rect rcBoundingBox) {
- if (_isDisposed) {
+ if (_isDisposed || _isUpdatingWindowPosition || _hwnd == HWND.Null || !PInvoke.IsWindow(_hwnd)) {
return;
}
+ _isUpdatingWindowPosition = true;
+ try {
+ var x = (int)rcBoundingBox.X;
+ var y = (int)rcBoundingBox.Y;
+ var width = Math.Max(0, (int)rcBoundingBox.Width);
+ var height = Math.Max(0, (int)rcBoundingBox.Height);
+ if (_hasLastWindowPosition &&
+ _lastWindowX == x &&
+ _lastWindowY == y &&
+ _lastWindowWidth == width &&
+ _lastWindowHeight == height) {
+ ApplyWindowRegion(rcBoundingBox);
+ return;
+ }
+
+ // HWND_BOTTOM controls the z-order only among HWND siblings. XAML siblings are
+ // compositor content, so they still suffer from HwndHost airspace. Keep the
+ // renderer at the bottom of the HWND sibling order, then clip the portion that
+ // is covered by the left-side XAML overlay.
+ NativeWindowResizeOrder.Apply(
+ () => PInvoke.SetWindowPos(_hwnd,
+ new HWND(1),
+ x,
+ y,
+ width,
+ height,
+ SET_WINDOW_POS_FLAGS.SWP_ASYNCWINDOWPOS
+ | SET_WINDOW_POS_FLAGS.SWP_NOCOPYBITS
+ | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE),
+ () => {
+ _hasLastWindowPosition = true;
+ _lastWindowX = x;
+ _lastWindowY = y;
+ _lastWindowWidth = width;
+ _lastWindowHeight = height;
+ OnHostedWindowPositionApplied(width, height);
+ });
+
+ ApplyWindowRegion(rcBoundingBox);
+ } finally {
+ _isUpdatingWindowPosition = false;
+ }
+ }
- PInvoke.SetWindowPos(_hwnd,
- HWND.Null,
- (int)rcBoundingBox.X,
- (int)rcBoundingBox.Y,
- (int)rcBoundingBox.Width,
- (int)rcBoundingBox.Height,
- SET_WINDOW_POS_FLAGS.SWP_ASYNCWINDOWPOS
- | SET_WINDOW_POS_FLAGS.SWP_NOZORDER
- | SET_WINDOW_POS_FLAGS.SWP_NOCOPYBITS
- | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE);
+ private void InvalidateWindowPositionCache() {
+ _hasLastWindowPosition = false;
+ }
+
+ private void ApplyWindowRegion(Rect rcBoundingBox) {
+ if (_hwnd.IsNull) {
+ return;
+ }
+
+ var width = Math.Max(0, (int)Math.Ceiling(rcBoundingBox.Width));
+ var height = Math.Max(0, (int)Math.Ceiling(rcBoundingBox.Height));
+ var clipLeft = 0;
+
+ if (_leftOverlayBoundaryInXamlRootDips is double boundary && XamlRoot != null) {
+ var boundaryInChildPixels = boundary * XamlRoot.RasterizationScale;
+ if (_hasDpiAwarenessContextTransition) {
+ boundaryInChildPixels /= DpiParentToChildRatio;
+ }
+
+ clipLeft = Math.Clamp(
+ (int)Math.Ceiling(boundaryInChildPixels - rcBoundingBox.X),
+ 0,
+ width);
+ }
+
+ if (clipLeft <= 0) {
+ if (_hasAppliedWindowRegion) {
+ SetWindowRgn(_hwnd.Value, IntPtr.Zero, true);
+ _hasAppliedWindowRegion = false;
+ }
+ _lastRegionLeft = 0;
+ _lastRegionWidth = width;
+ _lastRegionHeight = height;
+ return;
+ }
+
+ if (_hasAppliedWindowRegion &&
+ _lastRegionLeft == clipLeft &&
+ _lastRegionWidth == width &&
+ _lastRegionHeight == height) {
+ return;
+ }
+
+ var region = CreateRectRgn(clipLeft, 0, width, height);
+ if (region == IntPtr.Zero) {
+ return;
+ }
+
+ // After a successful SetWindowRgn call USER32 owns the HRGN.
+ if (SetWindowRgn(_hwnd.Value, region, true) == 0) {
+ DeleteObject(region);
+ return;
+ }
+
+ _hasAppliedWindowRegion = true;
+ _lastRegionLeft = clipLeft;
+ _lastRegionWidth = width;
+ _lastRegionHeight = height;
}
///
@@ -703,10 +865,10 @@ private void Initialize(bool fTrusted) {
- this.LayoutUpdated += (_, _) => UpdateWindowPos();
+ this.SizeChanged += OnSizeChanged;
this.PreviewKeyDown += OnKeyDown;
this.PreviewKeyUp += OnKeyUp;
- this.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, OnVisibleChanged);
+ _visibilityChangedToken = this.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, OnVisibleChanged);
}
@@ -743,8 +905,42 @@ private void OnSourceChanged(object sender) {
BuildOrReparentWindow();
}
- private void OnLayoutUpdated(object sender, object e) {
- UpdateWindowPos();
+ private void OnSizeChanged(object sender, SizeChangedEventArgs e) {
+ var hasValidSize = e.NewSize.Width > 0 && e.NewSize.Height > 0;
+ if (!NativeWindowPositionUpdatePolicy.ShouldQueue(
+ _isDisposed,
+ _isUpdatingWindowPosition,
+ hasValidSize,
+ _lastXamlSizeForWindowPosition == e.NewSize)) {
+ return;
+ }
+
+ _lastXamlSizeForWindowPosition = e.NewSize;
+ QueueWindowPositionUpdate();
+ }
+
+ private void QueueWindowPositionUpdate() {
+ if (_isDisposed || _windowPositionUpdateQueued) {
+ return;
+ }
+
+ _windowPositionUpdateQueued = true;
+ try {
+ if (DispatcherQueue.TryEnqueue(() => {
+ _windowPositionUpdateQueued = false;
+ if (_isDisposed) {
+ return;
+ }
+
+ UpdateWindowPos();
+ })) {
+ return;
+ }
+ } catch (Exception ex) {
+ Debug.WriteLine($"Unable to queue hosted window position update: {ex}");
+ }
+
+ _windowPositionUpdateQueued = false;
}
private void OnEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) {
@@ -774,7 +970,8 @@ private void OnVisibleChanged(DependencyObject sender, DependencyProperty dp) {
PInvoke.ShowWindowAsync(_hwnd, ActivateOnShow ? SHOW_WINDOW_CMD.SW_SHOW : SHOW_WINDOW_CMD.SW_SHOWNA);
else
PInvoke.ShowWindowAsync(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
- UpdateWindowPos();
+
+ QueueWindowPositionUpdate();
}
// This routine handles the following cases:
@@ -806,13 +1003,13 @@ private void BuildOrReparentWindow() {
// We now have a parent window, so we can create the child
// window.
BuildWindow(XAMLParentWindow);
- this.LayoutUpdated += OnLayoutUpdated;
//this.IsEnabledChanged += _handlerEnabledChanged;
//this.propertych += _handlerVisibleChanged;
} else if (XAMLParentWindow != PInvoke.GetParent(_hwnd)) {
// We have a different parent window. Just reparent the
// child window under the new parent window.
PInvoke.SetParent(_hwnd, XAMLParentWindow);
+ InvalidateWindowPositionCache();
}
} else if (Handle != IntPtr.Zero) {
// Reparent the window to notification-only window provided by SystemResources
@@ -851,7 +1048,12 @@ private unsafe void BuildWindow(HWND hwndParent) {
DemandIfUntrusted();
// Allow the derived class to build our HWND.
+ InvalidateWindowPositionCache();
_hwnd = BuildWindowCore(hwndParent);
+ _hasAppliedWindowRegion = false;
+ _lastRegionLeft = -1;
+ _lastRegionWidth = -1;
+ _lastRegionHeight = -1;
if (_hwnd == IntPtr.Zero || !PInvoke.IsWindow(_hwnd)) {
throw new InvalidOperationException("ChildWindowNotCreated");
}
@@ -912,7 +1114,7 @@ private unsafe void BuildWindow(HWND hwndParent) {
private void DispatcherInvoke(Action action, Microsoft.UI.Dispatching.DispatcherQueuePriority priority = Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal) =>
UWPHelpers.Enqueue(this.DispatcherQueue, action, priority);
- private bool CheckAccess() => false;
+ private bool CheckAccess() => this.DispatcherQueue.HasThreadAccess;
private void DestroyWindow() {
// Destroy the window if we are hosting one.
if (Handle == IntPtr.Zero)
@@ -931,6 +1133,7 @@ private void DestroyWindow() {
var hwnd = _hwnd;
_hwnd = HWND.Null;
+ InvalidateWindowPositionCache();
DestroyWindowCore(hwnd);
}
@@ -954,6 +1157,14 @@ private void SubclassWndProc(object sender, WindowMessageEventArgs e) {
private HWND _hwnd;
private Size _desiredSize;
+ private bool _isUpdatingWindowPosition;
+ private bool _windowPositionUpdateQueued;
+ private Size _lastXamlSizeForWindowPosition;
+ private bool _hasLastWindowPosition;
+ private int _lastWindowX;
+ private int _lastWindowY;
+ private int _lastWindowWidth;
+ private int _lastWindowHeight;
///
/// True when the parent of and
diff --git a/Microsoft.Terminal.WinUI3/ITerminalConnection.cs b/Microsoft.Terminal.WinUI3/ITerminalConnection.cs
new file mode 100644
index 0000000..acef865
--- /dev/null
+++ b/Microsoft.Terminal.WinUI3/ITerminalConnection.cs
@@ -0,0 +1,43 @@
+//
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+
+namespace Microsoft.Terminal.Wpf
+{
+ using System;
+
+ ///
+ /// Represents a connection to a terminal backend, generally a pty interface.
+ ///
+ public interface ITerminalConnection
+ {
+ ///
+ /// Event that should be triggered when the terminal backend has new data for the terminal control.
+ ///
+ event EventHandler TerminalOutput;
+
+ ///
+ /// Inform the backend that the terminal control is ready to start sending and receiving data.
+ ///
+ void Start();
+
+ ///
+ /// Write user input to the backend.
+ ///
+ /// The data to be written to the terminal backend.
+ void WriteInput(string data);
+
+ ///
+ /// Resize the terminal backend.
+ ///
+ /// The number of rows to resize to.
+ /// The number of columns to resize to.
+ void Resize(uint rows, uint columns);
+
+ ///
+ /// Shut down the terminal backend process.
+ ///
+ void Close();
+ }
+}
diff --git a/Microsoft.Terminal.WinUI3/LICENSE-EasyWindowsTerminalControl b/Microsoft.Terminal.WinUI3/LICENSE-EasyWindowsTerminalControl
new file mode 100644
index 0000000..e11e8fa
--- /dev/null
+++ b/Microsoft.Terminal.WinUI3/LICENSE-EasyWindowsTerminalControl
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+