// // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // namespace Microsoft.Terminal.Wpf { using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Threading; /// /// The container class that hosts the native hwnd terminal. /// /// /// This class is only left public since xaml cannot work with internal classes. /// public class TerminalContainer : HwndHost { private ITerminalConnection connection; private IntPtr hwnd; private IntPtr terminal; private DispatcherTimer blinkTimer; private NativeMethods.ScrollCallback scrollCallback; private NativeMethods.WriteCallback writeCallback; /// /// Initializes a new instance of the class. /// public TerminalContainer() { this.MessageHook += this.TerminalContainer_MessageHook; this.Focusable = true; var blinkTime = NativeMethods.GetCaretBlinkTime(); if (blinkTime != uint.MaxValue) { this.blinkTimer = new DispatcherTimer(); this.blinkTimer.Interval = TimeSpan.FromMilliseconds(blinkTime); this.blinkTimer.Tick += (_, __) => { if (this.terminal != IntPtr.Zero) { NativeMethods.TerminalBlinkCursor(this.terminal); } }; } } /// /// Event that is fired when the terminal buffer scrolls from text output. /// internal event EventHandler<(int viewTop, int viewHeight, int bufferSize)> TerminalScrolled; /// /// Event that is fired when the user engages in a mouse scroll over the terminal hwnd. /// internal event EventHandler UserScrolled; /// /// Gets or sets a value indicating whether if the renderer should automatically resize to fill the control /// on user action. /// internal bool AutoResize { get; set; } = true; /// /// Gets or sets the size of the parent user control that hosts the terminal hwnd. /// /// Control size is in device independent units, but for simplicity all sizes should be scaled. internal Size TerminalControlSize { get; set; } /// /// Gets or sets the size of the terminal renderer. /// internal Size TerminalRendererSize { get; set; } /// /// Gets the current character rows available to the terminal. /// internal int Rows { get; private set; } /// /// Gets the current character columns available to the terminal. /// internal int Columns { get; private set; } /// /// Gets the window handle of the terminal. /// internal IntPtr Hwnd => this.hwnd; /// /// Sets the connection to the terminal backend. /// internal ITerminalConnection Connection { private get { return this.connection; } set { if (this.connection != null) { this.connection.TerminalOutput -= this.Connection_TerminalOutput; } this.connection = value; this.connection.TerminalOutput += this.Connection_TerminalOutput; this.connection.Start(); } } /// /// Manually invoke a scroll of the terminal buffer. /// /// The top line to show in the terminal. internal void UserScroll(int viewTop) { NativeMethods.TerminalUserScroll(this.terminal, viewTop); } /// /// Sets the theme for the terminal. This includes font family, size, color, as well as background and foreground colors. /// /// The color theme for the terminal to use. /// The font family to use in the terminal. /// The font size to use in the terminal. internal void SetTheme(TerminalTheme theme, string fontFamily, short fontSize) { var dpiScale = VisualTreeHelper.GetDpi(this); NativeMethods.TerminalSetTheme(this.terminal, theme, fontFamily, fontSize, (int)dpiScale.PixelsPerInchX); // Validate before resizing that we have a non-zero size. if (!this.RenderSize.IsEmpty && !this.TerminalControlSize.IsEmpty && this.TerminalControlSize.Width != 0 && this.TerminalControlSize.Height != 0) { this.Resize(this.TerminalControlSize); } } /// /// Gets the selected text from the terminal renderer and clears the selection. /// /// The selected text, empty if no text is selected. internal string GetSelectedText() { if (NativeMethods.TerminalIsSelectionActive(this.terminal)) { return NativeMethods.TerminalGetSelection(this.terminal); } return string.Empty; } /// /// Triggers a resize of the terminal with the given size, redrawing the rendered text. /// /// Size of the rendering window. internal void Resize(Size renderSize) { if (renderSize.Width == 0 || renderSize.Height == 0) { throw new ArgumentException("Terminal column or row count cannot be 0.", nameof(renderSize)); } NativeMethods.TerminalTriggerResize( this.terminal, Convert.ToInt16(renderSize.Width), Convert.ToInt16(renderSize.Height), out NativeMethods.COORD dimensions); this.Rows = dimensions.Y; this.Columns = dimensions.X; this.TerminalRendererSize = renderSize; this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); } /// /// 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) { if (rows == 0) { throw new ArgumentException("Terminal row count cannot be 0.", nameof(rows)); } else if (columns == 0) { throw new ArgumentException("Terminal column count cannot be 0.", nameof(columns)); } NativeMethods.SIZE dimensionsInPixels; NativeMethods.COORD dimensions = new NativeMethods.COORD { X = (short)columns, Y = (short)rows, }; NativeMethods.TerminalTriggerResizeWithDimension(this.terminal, dimensions, out dimensionsInPixels); this.Columns = dimensions.X; this.Rows = dimensions.Y; this.TerminalRendererSize = new Size() { Width = dimensionsInPixels.cx, Height = dimensionsInPixels.cy, }; this.Connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); } /// /// Calculates the rows and columns that would fit in the given size. /// /// DPI scaled size. /// Amount of rows and columns that would fit the given size. internal (uint columns, uint rows) CalculateRowsAndColumns(Size size) { NativeMethods.TerminalCalculateResize(this.terminal, (short)size.Width, (short)size.Height, out NativeMethods.COORD dimensions); return ((uint)dimensions.X, (uint)dimensions.Y); } /// /// Triggers the terminal resize event if more space is available in the terminal control. /// internal void RaiseResizedIfDrawSpaceIncreased() { (var columns, var rows) = this.CalculateRowsAndColumns(this.TerminalControlSize); if (this.Columns < columns || this.Rows < rows) { this.connection?.Resize(rows, columns); } } /// protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) { if (this.terminal != IntPtr.Zero) { NativeMethods.TerminalDpiChanged(this.terminal, (int)(NativeMethods.USER_DEFAULT_SCREEN_DPI * newDpi.DpiScaleX)); } } /// protected override HandleRef BuildWindowCore(HandleRef hwndParent) { var dpiScale = VisualTreeHelper.GetDpi(this); NativeMethods.CreateTerminal(hwndParent.Handle, out this.hwnd, out this.terminal); this.scrollCallback = this.OnScroll; this.writeCallback = this.OnWrite; NativeMethods.TerminalRegisterScrollCallback(this.terminal, this.scrollCallback); NativeMethods.TerminalRegisterWriteCallback(this.terminal, this.writeCallback); // If the saved DPI scale isn't the default scale, we push it to the terminal. if (dpiScale.PixelsPerInchX != NativeMethods.USER_DEFAULT_SCREEN_DPI) { NativeMethods.TerminalDpiChanged(this.terminal, (int)dpiScale.PixelsPerInchX); } if (NativeMethods.GetFocus() == this.hwnd) { this.blinkTimer?.Start(); } else { NativeMethods.TerminalSetCursorVisible(this.terminal, false); } return new HandleRef(this, this.hwnd); } /// protected override void DestroyWindowCore(HandleRef hwnd) { NativeMethods.DestroyTerminal(this.terminal); 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 IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (hwnd == this.hwnd) { switch ((NativeMethods.WindowMessage)msg) { case NativeMethods.WindowMessage.WM_SETFOCUS: this.blinkTimer?.Start(); break; case NativeMethods.WindowMessage.WM_KILLFOCUS: this.blinkTimer?.Stop(); break; case NativeMethods.WindowMessage.WM_MOUSEACTIVATE: this.Focus(); NativeMethods.SetFocus(this.hwnd); break; case NativeMethods.WindowMessage.WM_SYSKEYDOWN: // fallthrough case NativeMethods.WindowMessage.WM_KEYDOWN: { // WM_KEYDOWN lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown NativeMethods.TerminalSetCursorVisible(this.terminal, true); UnpackKeyMessage(wParam, lParam, out ushort vkey, out ushort scanCode, out ushort flags); NativeMethods.TerminalSendKeyEvent(this.terminal, vkey, scanCode, flags, true); this.blinkTimer?.Start(); break; } case NativeMethods.WindowMessage.WM_SYSKEYUP: // fallthrough case NativeMethods.WindowMessage.WM_KEYUP: { // WM_KEYUP lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keyup UnpackKeyMessage(wParam, lParam, out ushort vkey, out ushort scanCode, out ushort flags); NativeMethods.TerminalSendKeyEvent(this.terminal, (ushort)wParam, scanCode, flags, false); break; } case NativeMethods.WindowMessage.WM_CHAR: { // WM_CHAR lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-char UnpackCharMessage(wParam, lParam, out char character, out ushort scanCode, out ushort flags); NativeMethods.TerminalSendCharEvent(this.terminal, character, scanCode, flags); break; } case NativeMethods.WindowMessage.WM_WINDOWPOSCHANGED: var windowpos = (NativeMethods.WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(NativeMethods.WINDOWPOS)); if (((NativeMethods.SetWindowPosFlags)windowpos.flags).HasFlag(NativeMethods.SetWindowPosFlags.SWP_NOSIZE) || (windowpos.cx == 0 && windowpos.cy == 0)) { break; } NativeMethods.COORD dimensions; if (this.AutoResize) { NativeMethods.TerminalTriggerResize(this.terminal, (short)windowpos.cx, (short)windowpos.cy, out dimensions); this.Columns = dimensions.X; this.Rows = dimensions.Y; this.TerminalRendererSize = new Size() { Width = windowpos.cx, Height = windowpos.cy, }; } else { // Calculate the new columns and rows that fit the total control size and alert the control to redraw the margins. NativeMethods.TerminalCalculateResize(this.terminal, (short)this.TerminalControlSize.Width, (short)this.TerminalControlSize.Height, out dimensions); } 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); //break; } } return IntPtr.Zero; } private void LeftClickHandler(int lParam) { var altPressed = NativeMethods.GetKeyState((int)NativeMethods.VirtualKey.VK_MENU) < 0; var x = (short)(((int)lParam << 16) >> 16); var y = (short)((int)lParam >> 16); NativeMethods.COORD cursorPosition = new NativeMethods.COORD() { X = x, Y = y, }; NativeMethods.TerminalStartSelection(this.terminal, cursorPosition, altPressed); } private void MouseMoveHandler(int wParam, int lParam) { if (((int)wParam & 0x0001) == 1) { var x = (short)(((int)lParam << 16) >> 16); var y = (short)((int)lParam >> 16); NativeMethods.COORD cursorPosition = new NativeMethods.COORD() { X = x, Y = y, }; NativeMethods.TerminalMoveSelection(this.terminal, cursorPosition); } } private void Connection_TerminalOutput(object sender, TerminalOutputEventArgs e) { if (this.terminal != IntPtr.Zero) { NativeMethods.TerminalSendOutput(this.terminal, e.Data); } } private void OnScroll(int viewTop, int viewHeight, int bufferSize) { this.TerminalScrolled?.Invoke(this, (viewTop, viewHeight, bufferSize)); } private void OnWrite(string data) { this.Connection?.WriteInput(data); } } }