// // 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.GotFocus += this.TerminalContainer_GotFocus; 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 the character rows available to the terminal. /// internal int Rows { get; private set; } /// /// Gets the character columns available to the terminal. /// internal int Columns { get; private set; } 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); this.TriggerResize(this.RenderSize); } /// /// Triggers a refresh of the terminal with the given size. /// /// Size of the rendering window. /// Tuple with rows and columns. internal (int rows, int columns) TriggerResize(Size renderSize) { var dpiScale = VisualTreeHelper.GetDpi(this); NativeMethods.COORD dimensions; NativeMethods.TerminalTriggerResize(this.terminal, renderSize.Width * dpiScale.DpiScaleX, renderSize.Height * dpiScale.DpiScaleY, out dimensions); this.Rows = dimensions.Y; this.Columns = dimensions.X; this.connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); return (dimensions.Y, dimensions.X); } /// /// Resizes the terminal. /// /// Number of rows to show. /// Number of columns to show. internal void Resize(uint rows, uint columns) { NativeMethods.COORD dimensions = new NativeMethods.COORD { X = (short)columns, Y = (short)rows, }; NativeMethods.TerminalResize(this.terminal, dimensions); this.Rows = dimensions.Y; this.Columns = dimensions.X; this.connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); } /// 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 void TerminalContainer_GotFocus(object sender, RoutedEventArgs e) { e.Handled = true; NativeMethods.SetFocus(this.hwnd); } 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(); NativeMethods.TerminalSetCursorVisible(this.terminal, false); break; case NativeMethods.WindowMessage.WM_MOUSEACTIVATE: this.Focus(); NativeMethods.SetFocus(this.hwnd); break; case NativeMethods.WindowMessage.WM_KEYDOWN: NativeMethods.TerminalSetCursorVisible(this.terminal, true); NativeMethods.TerminalClearSelection(this.terminal); NativeMethods.TerminalSendKeyEvent(this.terminal, wParam); this.blinkTimer?.Start(); break; case NativeMethods.WindowMessage.WM_CHAR: NativeMethods.TerminalSendCharEvent(this.terminal, (char)wParam); 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)) { break; } NativeMethods.TerminalTriggerResize(this.terminal, windowpos.cx, windowpos.cy, out var dimensions); this.connection?.Resize((uint)dimensions.Y, (uint)dimensions.X); this.Columns = dimensions.X; this.Rows = dimensions.Y; break; case NativeMethods.WindowMessage.WM_MOUSEWHEEL: var delta = ((int)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); } } }