diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index 6280ae37e72..e25ebf98549 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -433,7 +433,15 @@ void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data) publicTerminal->SendOutput(data); } -HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double height, _Out_ COORD* dimensions) +/// +/// Triggers a terminal resize using the new width and height in pixel. +/// +/// Terminal pointer. +/// New width of the terminal in pixels. +/// New height of the terminal in pixels +/// Out parameter containing the columns and rows that fit the new size. +/// HRESULT of the attempted resize. +HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions) { const auto publicTerminal = static_cast(terminal); @@ -446,10 +454,55 @@ HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double heig static_cast(height), 0)); - const SIZE windowSize{ static_cast(width), static_cast(height) }; + const SIZE windowSize{ width, height }; return publicTerminal->Refresh(windowSize, dimensions); } +/// +/// Helper method for resizing the terminal using character column and row counts +/// +/// Pointer to the terminal object. +/// New terminal size in row and column count. +/// Out parameter with the new size of the renderer. +/// HRESULT of the attempted resize. +HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ COORD dimensionsInCharacters, _Out_ SIZE* dimensionsInPixels) +{ + RETURN_HR_IF_NULL(E_INVALIDARG, dimensionsInPixels); + + const auto publicTerminal = static_cast(terminal); + + const auto viewInCharacters = Viewport::FromDimensions({ 0, 0 }, { (dimensionsInCharacters.X), (dimensionsInCharacters.Y) }); + const auto viewInPixels = publicTerminal->_renderEngine->GetViewportInPixels(viewInCharacters); + + dimensionsInPixels->cx = viewInPixels.Width(); + dimensionsInPixels->cy = viewInPixels.Height(); + + COORD unused{ 0, 0 }; + + return TerminalTriggerResize(terminal, viewInPixels.Width(), viewInPixels.Height(), &unused); +} + +/// +/// Calculates the amount of rows and columns that fit in the provided width and height. +/// +/// Terminal pointer +/// Width of the terminal area to calculate. +/// Height of the terminal area to calculate. +/// Out parameter containing the columns and rows that fit the new size. +/// HRESULT of the calculation. +HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions) +{ + const auto publicTerminal = static_cast(terminal); + + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, { width, height }); + const auto viewInCharacters = publicTerminal->_renderEngine->GetViewportInCharacters(viewInPixels); + + dimensions->X = viewInCharacters.Width(); + dimensions->Y = viewInCharacters.Height(); + + return S_OK; +} + void _stdcall TerminalDpiChanged(void* terminal, int newDpi) { const auto publicTerminal = static_cast(terminal); @@ -760,18 +813,6 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font publicTerminal->Refresh(windowSize, &dimensions); } -// Resizes the terminal to the specified rows and columns. -HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions) -{ - const auto publicTerminal = static_cast(terminal); - - auto lock = publicTerminal->_terminal->LockForWriting(); - publicTerminal->_terminal->ClearSelection(); - publicTerminal->_renderer->TriggerRedrawAll(); - - return publicTerminal->_terminal->UserResize(dimensions); -} - void _stdcall TerminalBlinkCursor(void* terminal) { const auto publicTerminal = static_cast(terminal); diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp index 09f7819f312..f26f69ccac5 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.hpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.hpp @@ -27,8 +27,9 @@ extern "C" { __declspec(dllexport) HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal); __declspec(dllexport) void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data); __declspec(dllexport) void _stdcall TerminalRegisterScrollCallback(void* terminal, void __stdcall callback(int, int, int)); -__declspec(dllexport) HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double height, _Out_ COORD* dimensions); -__declspec(dllexport) HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions); +__declspec(dllexport) HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); +__declspec(dllexport) HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ COORD dimensions, _Out_ SIZE* dimensionsInPixels); +__declspec(dllexport) HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); __declspec(dllexport) void _stdcall TerminalDpiChanged(void* terminal, int newDpi); __declspec(dllexport) void _stdcall TerminalUserScroll(void* terminal, int viewTop); __declspec(dllexport) void _stdcall TerminalClearSelection(void* terminal); @@ -90,7 +91,9 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo std::optional _singleClickTouchdownPos; friend HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal); - friend HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions); + friend HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); + friend HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ COORD dimensions, _Out_ SIZE* dimensionsInPixels); + friend HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ short width, _In_ short height, _Out_ COORD* dimensions); friend void _stdcall TerminalDpiChanged(void* terminal, int newDpi); friend void _stdcall TerminalUserScroll(void* terminal, int viewTop); friend void _stdcall TerminalClearSelection(void* terminal); diff --git a/src/cascadia/WpfTerminalControl/NativeMethods.cs b/src/cascadia/WpfTerminalControl/NativeMethods.cs index 0e40d646652..34fe3085746 100644 --- a/src/cascadia/WpfTerminalControl/NativeMethods.cs +++ b/src/cascadia/WpfTerminalControl/NativeMethods.cs @@ -7,7 +7,6 @@ namespace Microsoft.Terminal.Wpf { using System; using System.Runtime.InteropServices; - using System.Windows.Automation.Provider; #pragma warning disable SA1600 // Elements should be documented internal static class NativeMethods @@ -187,10 +186,13 @@ public enum SetWindowPosFlags : uint public static extern void TerminalSendOutput(IntPtr terminal, string lpdata); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalTriggerResize(IntPtr terminal, double width, double height, out COORD dimensions); + public static extern uint TerminalTriggerResize(IntPtr terminal, short width, short height, out COORD dimensions); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalResize(IntPtr terminal, COORD dimensions); + public static extern uint TerminalTriggerResizeWithDimension(IntPtr terminal, [MarshalAs(UnmanagedType.Struct)] COORD dimensions, out SIZE dimensionsInPixels); + + [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern uint TerminalCalculateResize(IntPtr terminal, short width, short height, out COORD dimensions); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] public static extern void TerminalDpiChanged(IntPtr terminal, int newDpi); @@ -205,10 +207,10 @@ public enum SetWindowPosFlags : uint public static extern void TerminalUserScroll(IntPtr terminal, int viewTop); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalStartSelection(IntPtr terminal, NativeMethods.COORD cursorPosition, bool altPressed); + public static extern uint TerminalStartSelection(IntPtr terminal, COORD cursorPosition, bool altPressed); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] - public static extern uint TerminalMoveSelection(IntPtr terminal, NativeMethods.COORD cursorPosition); + public static extern uint TerminalMoveSelection(IntPtr terminal, COORD cursorPosition); [DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] public static extern void TerminalClearSelection(IntPtr terminal); @@ -278,10 +280,24 @@ public struct COORD public short X; /// - /// The x-coordinate of the point. + /// The y-coordinate of the point. /// public short Y; } + + [StructLayout(LayoutKind.Sequential)] + public struct SIZE + { + /// + /// The x size. + /// + public int cx; + + /// + /// The y size. + /// + public int cy; + } } #pragma warning restore SA1600 // Elements should be documented } diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index 2f5c6839455..b242faa0f14 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -20,20 +20,6 @@ namespace Microsoft.Terminal.Wpf /// public class TerminalContainer : HwndHost { - private static void UnpackKeyMessage(IntPtr wParam, IntPtr lParam, out ushort vkey, out ushort scanCode, out ushort flags) - { - ulong scanCodeAndFlags = (((ulong)lParam) & 0xFFFF0000) >> 16; - scanCode = (ushort)(scanCodeAndFlags & 0x00FFu); - flags = (ushort)(scanCodeAndFlags & 0xFF00u); - vkey = (ushort)wParam; - } - - private static void UnpackCharMessage(IntPtr wParam, IntPtr lParam, out char character, out ushort scanCode, out ushort flags) - { - UnpackKeyMessage(wParam, lParam, out ushort vKey, out scanCode, out flags); - character = (char)vKey; - } - private ITerminalConnection connection; private IntPtr hwnd; private IntPtr terminal; @@ -77,15 +63,33 @@ public TerminalContainer() internal event EventHandler UserScrolled; /// - /// Gets the character rows available to the terminal. + /// Gets or sets a value indicating whether if the renderer should automatically resize to fill the control + /// on user action. + /// + public bool AutoFill { get; set; } = true; + + /// + /// Gets the current character rows available to the terminal. /// internal int Rows { get; private set; } /// - /// Gets the character columns available to the terminal. + /// Gets the current character columns available to the terminal. /// internal int Columns { get; private set; } + /// + /// Gets the maximum amount of character rows that can fit in this control. + /// + /// This will be in sync with unless is set to false. + internal int MaxRows { get; private set; } + + /// + /// Gets the maximum amount of character columns that can fit in this control. + /// + /// This will be in sync with unless is set to false. + internal int MaxColumns { get; private set; } + /// /// Gets the window handle of the terminal. /// @@ -162,34 +166,50 @@ internal string GetSelectedText() var dpiScale = VisualTreeHelper.GetDpi(this); NativeMethods.COORD dimensions; - NativeMethods.TerminalTriggerResize(this.terminal, renderSize.Width * dpiScale.DpiScaleX, renderSize.Height * dpiScale.DpiScaleY, out dimensions); + NativeMethods.TerminalTriggerResize( + this.terminal, + Convert.ToInt16(renderSize.Width * dpiScale.DpiScaleX), + Convert.ToInt16(renderSize.Height * dpiScale.DpiScaleY), + out dimensions); this.Rows = dimensions.Y; this.Columns = dimensions.X; - this.connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); + this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); return (dimensions.Y, dimensions.X); } /// - /// Resizes the terminal. + /// Resizes the terminal using row and column count as the new size. /// /// Number of rows to show. /// Number of columns to show. - internal void Resize(uint rows, uint columns) + /// pair with the new width and height size in pixels for the renderer. + internal (int width, int height) Resize(uint rows, uint columns) { + NativeMethods.SIZE dimensionsInPixels; NativeMethods.COORD dimensions = new NativeMethods.COORD { X = (short)columns, Y = (short)rows, }; - NativeMethods.TerminalResize(this.terminal, dimensions); + NativeMethods.TerminalTriggerResizeWithDimension(this.terminal, dimensions, out dimensionsInPixels); + + // If AutoFill is true, keep Rows and Columns in sync with MaxRows and MaxColumns. + // Otherwise, MaxRows and MaxColumns will be set on startup and on control resize by the user. + if (this.AutoFill) + { + this.MaxColumns = dimensions.X; + this.MaxRows = dimensions.Y; + } - this.Rows = dimensions.Y; this.Columns = dimensions.X; + this.Rows = dimensions.Y; - this.connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); + this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); + + return (dimensionsInPixels.cx, dimensionsInPixels.cy); } /// @@ -238,6 +258,20 @@ protected override void DestroyWindowCore(HandleRef hwnd) this.terminal = IntPtr.Zero; } + private static void UnpackKeyMessage(IntPtr wParam, IntPtr lParam, out ushort vkey, out ushort scanCode, out ushort flags) + { + ulong scanCodeAndFlags = (((ulong)lParam) & 0xFFFF0000) >> 16; + scanCode = (ushort)(scanCodeAndFlags & 0x00FFu); + flags = (ushort)(scanCodeAndFlags & 0xFF00u); + vkey = (ushort)wParam; + } + + private static void UnpackCharMessage(IntPtr wParam, IntPtr lParam, out char character, out ushort scanCode, out ushort flags) + { + UnpackKeyMessage(wParam, lParam, out ushort vKey, out scanCode, out flags); + character = (char)vKey; + } + private void TerminalContainer_GotFocus(object sender, RoutedEventArgs e) { e.Handled = true; @@ -299,13 +333,28 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam break; } - NativeMethods.TerminalTriggerResize(this.terminal, windowpos.cx, windowpos.cy, out var dimensions); + NativeMethods.COORD dimensions; - this.connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); - this.Columns = dimensions.X; - this.Rows = dimensions.Y; + // We only trigger a resize if we want to fill to maximum size. + if (this.AutoFill) + { + NativeMethods.TerminalTriggerResize(this.terminal, (short)windowpos.cx, (short)windowpos.cy, out dimensions); + + this.Columns = dimensions.X; + this.Rows = dimensions.Y; + this.MaxColumns = dimensions.X; + this.MaxRows = dimensions.Y; + } + else + { + NativeMethods.TerminalCalculateResize(this.terminal, (short)windowpos.cx, (short)windowpos.cy, out dimensions); + this.MaxColumns = dimensions.X; + this.MaxRows = dimensions.Y; + } + this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); break; + case NativeMethods.WindowMessage.WM_MOUSEWHEEL: var delta = (short)(((long)wParam) >> 16); this.UserScrolled?.Invoke(this, delta); @@ -360,7 +409,7 @@ private void OnScroll(int viewTop, int viewHeight, int bufferSize) private void OnWrite(string data) { - this.connection?.WriteInput(data); + this.Connection?.WriteInput(data); } } } diff --git a/src/cascadia/WpfTerminalControl/TerminalControl.xaml b/src/cascadia/WpfTerminalControl/TerminalControl.xaml index b820e73df71..90a1c89205a 100644 --- a/src/cascadia/WpfTerminalControl/TerminalControl.xaml +++ b/src/cascadia/WpfTerminalControl/TerminalControl.xaml @@ -6,8 +6,9 @@ xmlns:local="clr-namespace:Microsoft.Terminal.Wpf" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" - Focusable="True"> - + Focusable="True" + x:Name="terminalUserControl"> + diff --git a/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs b/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs index ef8f6b30ae0..8862f1dcf1d 100644 --- a/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs +++ b/src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs @@ -5,6 +5,7 @@ namespace Microsoft.Terminal.Wpf { + using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -17,6 +18,8 @@ public partial class TerminalControl : UserControl { private int accumulatedDelta = 0; + private (int width, int height) terminalRendererSize; + /// /// Initializes a new instance of the class. /// @@ -32,15 +35,35 @@ public TerminalControl() } /// - /// Gets the character rows available to the terminal. + /// Gets the current character rows available to the terminal. /// public int Rows => this.termContainer.Rows; /// - /// Gets the character columns available to the terminal. + /// Gets the current character columns available to the terminal. /// public int Columns => this.termContainer.Columns; + /// + /// Gets the maximum amount of character rows that can fit in this control. + /// + public int MaxRows => this.termContainer.MaxRows; + + /// + /// Gets the maximum amount of character columns that can fit in this control. + /// + public int MaxColumns => this.termContainer.MaxColumns; + + /// + /// Gets or sets a value indicating whether if the renderer should automatically resize to fill the control + /// on user action. + /// + public bool AutoFill + { + get => this.termContainer.AutoFill; + set => this.termContainer.AutoFill = value; + } + /// /// Sets the connection to a terminal backend. /// @@ -68,6 +91,13 @@ public void SetTheme(TerminalTheme theme, string fontFamily, short fontSize) } this.termContainer.SetTheme(theme, fontFamily, fontSize); + + // DefaultBackground uses Win32 COLORREF syntax which is BGR instead of RGB. + byte b = Convert.ToByte((theme.DefaultBackground >> 16) & 0xff); + byte g = Convert.ToByte((theme.DefaultBackground >> 8) & 0xff); + byte r = Convert.ToByte(theme.DefaultBackground & 0xff); + + this.terminalGrid.Background = new SolidColorBrush(Color.FromRgb(r, g, b)); } /// @@ -86,7 +116,21 @@ public string GetSelectedText() /// Number of columns to display. public void Resize(uint rows, uint columns) { - this.termContainer.Resize(rows, columns); + var dpiScale = VisualTreeHelper.GetDpi(this); + + this.terminalRendererSize = this.termContainer.Resize(rows, columns); + + double marginWidth = ((this.terminalUserControl.RenderSize.Width * dpiScale.DpiScaleX) - this.terminalRendererSize.width) / dpiScale.DpiScaleX; + double marginHeight = ((this.terminalUserControl.RenderSize.Height * dpiScale.DpiScaleY) - this.terminalRendererSize.height) / dpiScale.DpiScaleY; + + // Make space for the scrollbar. + marginWidth -= this.scrollbar.Width; + + // Prevent negative margin size. + marginWidth = marginWidth < 0 ? 0 : marginWidth; + marginHeight = marginHeight < 0 ? 0 : marginHeight; + + this.terminalGrid.Margin = new Thickness(0, 0, marginWidth, marginHeight); } /// @@ -99,6 +143,27 @@ public void Resize(uint rows, uint columns) return this.termContainer.TriggerResize(rendersize); } + /// + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + // Renderer will not resize on control resize. We have to manually recalculate the margin to fill in the space. + if (this.AutoFill == false && this.terminalRendererSize.width != 0 && this.terminalRendererSize.height != 0) + { + var dpiScale = VisualTreeHelper.GetDpi(this); + + double width = ((sizeInfo.NewSize.Width * dpiScale.DpiScaleX) - this.terminalRendererSize.width) / dpiScale.DpiScaleX; + double height = ((sizeInfo.NewSize.Height * dpiScale.DpiScaleY) - this.terminalRendererSize.height) / dpiScale.DpiScaleY; + + // Prevent negative margin size. + width = width < 0 ? 0 : width; + height = height < 0 ? 0 : height; + + this.terminalGrid.Margin = new Thickness(0, 0, width, height); + } + + base.OnRenderSizeChanged(sizeInfo); + } + private void TerminalControl_GotFocus(object sender, RoutedEventArgs e) { e.Handled = true; diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index daabbc8a894..7e182afb74e 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1780,12 +1780,20 @@ CATCH_RETURN(); [[nodiscard]] Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) noexcept { - const short widthInChars = gsl::narrow_cast(viewInPixels.Width() / _glyphCell.width()); - const short heightInChars = gsl::narrow_cast(viewInPixels.Height() / _glyphCell.height()); + const short widthInChars = base::saturated_cast(viewInPixels.Width() / _glyphCell.width()); + const short heightInChars = base::saturated_cast(viewInPixels.Height() / _glyphCell.height()); return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars }); } +[[nodiscard]] Viewport DxEngine::GetViewportInPixels(const Viewport& viewInCharacters) noexcept +{ + const short widthInPixels = base::saturated_cast(viewInCharacters.Width() * _glyphCell.width()); + const short heightInPixels = base::saturated_cast(viewInCharacters.Height() * _glyphCell.height()); + + return Viewport::FromDimensions(viewInCharacters.Origin(), { widthInPixels, heightInPixels }); +} + // Routine Description: // - Sets the DPI in this renderer // Arguments: diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index a6b6240ab3e..c59b89c5a8f 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -111,6 +111,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; [[nodiscard]] ::Microsoft::Console::Types::Viewport GetViewportInCharacters(const ::Microsoft::Console::Types::Viewport& viewInPixels) noexcept; + [[nodiscard]] ::Microsoft::Console::Types::Viewport GetViewportInPixels(const ::Microsoft::Console::Types::Viewport& viewInCharacters) noexcept; float GetScaling() const noexcept;