Skip to content

Commit

Permalink
wpf: fix margin calculations and resize events (#7892)
Browse files Browse the repository at this point in the history
  • Loading branch information
javierdlg authored Oct 13, 2020
1 parent cb96aa7 commit d2d462f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 85 deletions.
1 change: 1 addition & 0 deletions .github/actions/spell-check/dictionary/microsoft.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ tasklist
tdbuildteamid
vcruntime
visualstudio
VSTHRD
wlk
wslpath
wtl
Expand Down
97 changes: 57 additions & 40 deletions src/cascadia/WpfTerminalControl/TerminalContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,29 +66,28 @@ public TerminalContainer()
/// Gets or sets a value indicating whether if the renderer should automatically resize to fill the control
/// on user action.
/// </summary>
public bool AutoFill { get; set; } = true;
internal bool AutoResize { get; set; } = true;

/// <summary>
/// Gets the current character rows available to the terminal.
/// Gets or sets the size of the parent user control that hosts the terminal hwnd.
/// </summary>
internal int Rows { get; private set; }
/// <remarks>Control size is in device independent units, but for simplicity all sizes should be scaled.</remarks>
internal Size TerminalControlSize { get; set; }

/// <summary>
/// Gets the current character columns available to the terminal.
/// Gets or sets the size of the terminal renderer.
/// </summary>
internal int Columns { get; private set; }
internal Size TerminalRendererSize { get; set; }

/// <summary>
/// Gets the maximum amount of character rows that can fit in this control.
/// Gets the current character rows available to the terminal.
/// </summary>
/// <remarks>This will be in sync with <see cref="Rows"/> unless <see cref="AutoFill"/> is set to false.</remarks>
internal int MaxRows { get; private set; }
internal int Rows { get; private set; }

/// <summary>
/// Gets the maximum amount of character columns that can fit in this control.
/// Gets the current character columns available to the terminal.
/// </summary>
/// <remarks>This will be in sync with <see cref="Columns"/> unless <see cref="AutoFill"/> is set to false.</remarks>
internal int MaxColumns { get; private set; }
internal int Columns { get; private set; }

/// <summary>
/// Gets the window handle of the terminal.
Expand Down Expand Up @@ -139,7 +138,7 @@ internal void SetTheme(TerminalTheme theme, string fontFamily, short fontSize)

NativeMethods.TerminalSetTheme(this.terminal, theme, fontFamily, fontSize, (int)dpiScale.PixelsPerInchX);

this.TriggerResize(this.RenderSize);
this.Resize(this.TerminalControlSize);
}

/// <summary>
Expand All @@ -157,35 +156,30 @@ internal string GetSelectedText()
}

/// <summary>
/// Triggers a refresh of the terminal with the given size.
/// Triggers a resize of the terminal with the given size, redrawing the rendered text.
/// </summary>
/// <param name="renderSize">Size of the rendering window.</param>
/// <returns>Tuple with rows and columns.</returns>
internal (int rows, int columns) TriggerResize(Size renderSize)
internal void Resize(Size renderSize)
{
var dpiScale = VisualTreeHelper.GetDpi(this);

NativeMethods.COORD dimensions;
NativeMethods.TerminalTriggerResize(
this.terminal,
Convert.ToInt16(renderSize.Width * dpiScale.DpiScaleX),
Convert.ToInt16(renderSize.Height * dpiScale.DpiScaleY),
out dimensions);
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);
return (dimensions.Y, dimensions.X);
}

/// <summary>
/// Resizes the terminal using row and column count as the new size.
/// </summary>
/// <param name="rows">Number of rows to show.</param>
/// <param name="columns">Number of columns to show.</param>
/// <returns><see cref="long"/> pair with the new width and height size in pixels for the renderer.</returns>
internal (int width, int height) Resize(uint rows, uint columns)
internal void Resize(uint rows, uint columns)
{
NativeMethods.SIZE dimensionsInPixels;
NativeMethods.COORD dimensions = new NativeMethods.COORD
Expand All @@ -196,20 +190,41 @@ internal string GetSelectedText()

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.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);
}

/// <summary>
/// Calculates the rows and columns that would fit in the given size.
/// </summary>
/// <param name="size">DPI scaled size.</param>
/// <returns>Amount of rows and columns that would fit the given size.</returns>
internal (uint columns, uint rows) CalculateRowsAndColumns(Size size)
{
NativeMethods.TerminalCalculateResize(this.terminal, (short)size.Width, (short)size.Height, out NativeMethods.COORD dimensions);

return (dimensionsInPixels.cx, dimensionsInPixels.cy);
return ((uint)dimensions.X, (uint)dimensions.Y);
}

/// <summary>
/// Triggers the terminal resize event if more space is available in the terminal control.
/// </summary>
internal void RaiseResizedIfDrawSpaceIncreased()
{
(var columns, var rows) = this.CalculateRowsAndColumns(this.TerminalControlSize);

if (this.Columns < columns || this.Rows < rows)
{
this.connection?.Resize(rows, columns);
}
}

/// <inheritdoc/>
Expand Down Expand Up @@ -335,21 +350,23 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam

NativeMethods.COORD dimensions;

// We only trigger a resize if we want to fill to maximum size.
if (this.AutoFill)
if (this.AutoResize)
{
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;

this.TerminalRendererSize = new Size()
{
Width = windowpos.cx,
Height = windowpos.cy,
};
}
else
{
NativeMethods.TerminalCalculateResize(this.terminal, (short)windowpos.cx, (short)windowpos.cy, out dimensions);
this.MaxColumns = dimensions.X;
this.MaxRows = dimensions.Y;
// 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);
Expand Down
129 changes: 84 additions & 45 deletions src/cascadia/WpfTerminalControl/TerminalControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
namespace Microsoft.Terminal.Wpf
{
using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Task = System.Threading.Tasks.Task;

/// <summary>
/// A basic terminal control. This control can receive and render standard VT100 sequences.
Expand All @@ -18,7 +20,13 @@ public partial class TerminalControl : UserControl
{
private int accumulatedDelta = 0;

private (int width, int height) terminalRendererSize;
/// <summary>
/// Gets size of the terminal renderer.
/// </summary>
private Size TerminalRendererSize
{
get => this.termContainer.TerminalRendererSize;
}

/// <summary>
/// Initializes a new instance of the <see cref="TerminalControl"/> class.
Expand All @@ -44,35 +52,22 @@ public TerminalControl()
/// </summary>
public int Columns => this.termContainer.Columns;

/// <summary>
/// Gets the maximum amount of character rows that can fit in this control.
/// </summary>
public int MaxRows => this.termContainer.MaxRows;

/// <summary>
/// Gets the maximum amount of character columns that can fit in this control.
/// </summary>
public int MaxColumns => this.termContainer.MaxColumns;

/// <summary>
/// Gets or sets a value indicating whether if the renderer should automatically resize to fill the control
/// on user action.
/// </summary>
public bool AutoFill
public bool AutoResize
{
get => this.termContainer.AutoFill;
set => this.termContainer.AutoFill = value;
get => this.termContainer.AutoResize;
set => this.termContainer.AutoResize = value;
}

/// <summary>
/// Sets the connection to a terminal backend.
/// </summary>
public ITerminalConnection Connection
{
set
{
this.termContainer.Connection = value;
}
set => this.termContainer.Connection = value;
}

/// <summary>
Expand Down Expand Up @@ -114,56 +109,100 @@ public string GetSelectedText()
/// </summary>
/// <param name="rows">Number of rows to display.</param>
/// <param name="columns">Number of columns to display.</param>
public void Resize(uint rows, uint columns)
/// <param name="cancellationToken">Cancellation token for this task.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task ResizeAsync(uint rows, uint columns, CancellationToken cancellationToken)
{
var dpiScale = VisualTreeHelper.GetDpi(this);

this.terminalRendererSize = this.termContainer.Resize(rows, columns);
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);
#pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs
await this.Dispatcher.BeginInvoke(
new Action(delegate() { this.terminalGrid.Margin = this.CalculateMargins(); }),
System.Windows.Threading.DispatcherPriority.Render);
#pragma warning restore VSTHRD001 // Avoid legacy thread switching APIs
}

/// <summary>
/// Resizes the terminal to the specified dimensions.
/// </summary>
/// <param name="rendersize">Rendering size for the terminal.</param>
/// <param name="rendersize">Rendering size for the terminal in device independent units.</param>
/// <returns>A tuple of (int, int) representing the number of rows and columns in the terminal.</returns>
public (int rows, int columns) TriggerResize(Size rendersize)
{
return this.termContainer.TriggerResize(rendersize);
var dpiScale = VisualTreeHelper.GetDpi(this);
rendersize.Width *= dpiScale.DpiScaleX;
rendersize.Height *= dpiScale.DpiScaleY;

this.termContainer.Resize(rendersize);

return (this.Rows, this.Columns);
}

/// <inheritdoc/>
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);
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;
// termContainer requires scaled sizes.
this.termContainer.TerminalControlSize = new Size()
{
Width = (sizeInfo.NewSize.Width - this.scrollbar.ActualWidth) * dpiScale.DpiScaleX,
Height = sizeInfo.NewSize.Height * dpiScale.DpiScaleY,
};

// Prevent negative margin size.
width = width < 0 ? 0 : width;
height = height < 0 ? 0 : height;
if (!this.AutoResize)
{
// Renderer will not resize on control resize. We have to manually calculate the margin to fill in the space.
this.terminalGrid.Margin = this.CalculateMargins(sizeInfo.NewSize);

this.terminalGrid.Margin = new Thickness(0, 0, width, height);
// Margins stop resize events, therefore we have to manually check if more space is available and raise
// a resize event if needed.
this.termContainer.RaiseResizedIfDrawSpaceIncreased();
}

base.OnRenderSizeChanged(sizeInfo);
}

/// <summary>
/// Calculates the margins that should surround the terminal renderer, if any.
/// </summary>
/// <param name="controlSize">New size of the control. Uses the control's current size if not provided.</param>
/// <returns>The new terminal control margin thickness in device independent units.</returns>
private Thickness CalculateMargins(Size controlSize = default)
{
var dpiScale = VisualTreeHelper.GetDpi(this);
double width = 0, height = 0;

if (controlSize == default)
{
controlSize = new Size()
{
Width = this.terminalUserControl.ActualWidth,
Height = this.terminalUserControl.ActualHeight,
};
}

// During initialization, the terminal renderer size will be 0 and the terminal renderer
// draws on all available space. Therefore no margins are needed until resized.
if (this.TerminalRendererSize.Width != 0)
{
width = controlSize.Width - (this.TerminalRendererSize.Width / dpiScale.DpiScaleX);
}

if (this.TerminalRendererSize.Height != 0)
{
height = controlSize.Height - (this.TerminalRendererSize.Height / dpiScale.DpiScaleX);
}

width -= this.scrollbar.ActualWidth;

// Prevent negative margin size.
width = width < 0 ? 0 : width;
height = height < 0 ? 0 : height;

return new Thickness(0, 0, width, height);
}

private void TerminalControl_GotFocus(object sender, RoutedEventArgs e)
{
e.Handled = true;
Expand Down

0 comments on commit d2d462f

Please sign in to comment.