From d4c527607a734f37ea93bd57e27355d54ed51dde Mon Sep 17 00:00:00 2001 From: mcpiroman <38111589+mcpiroman@users.noreply.github.com> Date: Wed, 8 Jan 2020 22:19:23 +0100 Subject: [PATCH] Snap to character grid when resizing window (#3181) When user resizes window, snap the size to align with the character grid (like e.g. putty, mintty and most unix terminals). Properly resolves arbitrary pane configuration (even with different font sizes and padding) trying to align each pane as close as possible. It also fixes terminal minimum size enforcement which was not quite well handled, especially with multiple panes. This PR does not however try to keep the terminals aligned at other user actions (e.g. font change or pane split). That is to be tracked by some other activity. Snapping is resolved in the pane tree, recursively, so it (hopefully) works for any possible layout. Along the way I had to clean up some things as so to make the resulting code not so cumbersome: 1. Pane.cpp: Replaced _firstPercent and _secondPercent with single _desiredSplitPosition to reduce invariants - these had to be kept in sync so their sum always gives 1 (and were not really a percent). The desired part refers to fact that since panes are aligned, there is usually some deviation from that ratio. 2. Pane.cpp: Fixed _GetMinSize() - it was improperly accounting for split direction 3. TerminalControl: Made dedicated member for padding instead of reading it from a control itself. This is because the winrt property functions turned out to be slow and this algorithm needs to access it many times. I also cached scrollbar width for the same reason. 4. AppHost: Moved window to client size resolution to virtual method, where IslandWindow and NonClientIslandWindow have their own implementations (as opposite to pointer casting). One problem with current implementation is I had to make a long call chain from the window that requests snapping to the (root) pane that implements it: IslandWindow -> AppHost's callback -> App -> TerminalPage -> Tab -> Pane. I don't know if this can be done better. ## Validation Steps Performed Spam split pane buttons, randomly change font sizes with ctrl+mouse wheel and drag the window back and forth. Closes #2834 Closes #2277 --- src/cascadia/TerminalApp/AppLogic.cpp | 7 + src/cascadia/TerminalApp/AppLogic.h | 1 + src/cascadia/TerminalApp/AppLogic.idl | 1 + .../TerminalApp/Pane.LayoutSizeNode.cpp | 73 +++ src/cascadia/TerminalApp/Pane.cpp | 424 +++++++++++++++--- src/cascadia/TerminalApp/Pane.h | 57 ++- src/cascadia/TerminalApp/Tab.cpp | 21 + src/cascadia/TerminalApp/Tab.h | 2 + src/cascadia/TerminalApp/TerminalPage.cpp | 8 + src/cascadia/TerminalApp/TerminalPage.h | 2 + .../TerminalApp/lib/TerminalAppLib.vcxproj | 1 + .../lib/TerminalAppLib.vcxproj.filters | 4 + src/cascadia/TerminalControl/TermControl.cpp | 45 +- src/cascadia/TerminalControl/TermControl.h | 6 +- src/cascadia/TerminalControl/TermControl.idl | 3 + src/cascadia/WindowsTerminal/AppHost.cpp | 24 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 135 ++++++ src/cascadia/WindowsTerminal/IslandWindow.h | 4 + .../WindowsTerminal/NonClientIslandWindow.cpp | 26 ++ .../WindowsTerminal/NonClientIslandWindow.h | 2 + 20 files changed, 765 insertions(+), 81 deletions(-) create mode 100644 src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index b9376a24603..4b371253640 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -443,6 +443,13 @@ namespace winrt::TerminalApp::implementation return _settings->GlobalSettings().GetShowTabsInTitlebar(); } + // Method Description: + // - See Pane::CalcSnappedDimension + float AppLogic::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const + { + return _root->CalcSnappedDimension(widthOrHeight, dimension); + } + // Method Description: // - Attempt to load the settings. If we fail for any reason, returns an error. // Return Value: diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 01affca291c..44aec6eaf22 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -38,6 +38,7 @@ namespace winrt::TerminalApp::implementation winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); bool GetShowTabsInTitlebar(); + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; Windows::UI::Xaml::UIElement GetRoot() noexcept; diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index e99211bcf06..6ddc6676b6c 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -35,6 +35,7 @@ namespace TerminalApp Windows.UI.Xaml.ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); Boolean GetShowTabsInTitlebar(); + Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); void WindowCloseButtonClicked(); diff --git a/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp b/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp new file mode 100644 index 00000000000..f3cc8647ff1 --- /dev/null +++ b/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Pane.h" + +Pane::LayoutSizeNode::LayoutSizeNode(const float minSize) : + size{ minSize }, + isMinimumSize{ true }, + firstChild{ nullptr }, + secondChild{ nullptr }, + nextFirstChild{ nullptr }, + nextSecondChild{ nullptr } +{ +} + +Pane::LayoutSizeNode::LayoutSizeNode(const LayoutSizeNode& other) : + size{ other.size }, + isMinimumSize{ other.isMinimumSize }, + firstChild{ other.firstChild ? std::make_unique(*other.firstChild) : nullptr }, + secondChild{ other.secondChild ? std::make_unique(*other.secondChild) : nullptr }, + nextFirstChild{ other.nextFirstChild ? std::make_unique(*other.nextFirstChild) : nullptr }, + nextSecondChild{ other.nextSecondChild ? std::make_unique(*other.nextSecondChild) : nullptr } +{ +} + +// Method Description: +// - Makes sure that this node and all its descendants equal the supplied node. +// This may be more efficient that copy construction since it will reuse its +// allocated children. +// Arguments: +// - other: Node to take the values from. +// Return Value: +// - itself +Pane::LayoutSizeNode& Pane::LayoutSizeNode::operator=(const LayoutSizeNode& other) +{ + size = other.size; + isMinimumSize = other.isMinimumSize; + + _AssignChildNode(firstChild, other.firstChild.get()); + _AssignChildNode(secondChild, other.secondChild.get()); + _AssignChildNode(nextFirstChild, other.nextFirstChild.get()); + _AssignChildNode(nextSecondChild, other.nextSecondChild.get()); + + return *this; +} + +// Method Description: +// - Performs assignment operation on a single child node reusing +// - current one if present. +// Arguments: +// - nodeField: Reference to our field holding concerned node. +// - other: Node to take the values from. +// Return Value: +// - +void Pane::LayoutSizeNode::_AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode) +{ + if (newNode) + { + if (nodeField) + { + *nodeField = *newNode; + } + else + { + nodeField = std::make_unique(*newNode); + } + } + else + { + nodeField.release(); + } +} diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 90bef8e32bf..2ccb21da1e4 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -74,7 +74,7 @@ void Pane::ResizeContent(const Size& newSize) if (_splitState == SplitState::Vertical) { - const auto paneSizes = _GetPaneSizes(width); + const auto paneSizes = _CalcChildrenSizes(width); const Size firstSize{ paneSizes.first, height }; const Size secondSize{ paneSizes.second, height }; @@ -83,7 +83,7 @@ void Pane::ResizeContent(const Size& newSize) } else if (_splitState == SplitState::Horizontal) { - const auto paneSizes = _GetPaneSizes(height); + const auto paneSizes = _CalcChildrenSizes(height); const Size firstSize{ width, paneSizes.first }; const Size secondSize{ width, paneSizes.second }; @@ -92,6 +92,17 @@ void Pane::ResizeContent(const Size& newSize) } } +// Method Description: +// - Recalculates and reapplies sizes of all descendant panes. +// Arguments: +// - +// Return Value: +// - +void Pane::Relayout() +{ + ResizeContent(_root.ActualSize()); +} + // Method Description: // - Adjust our child percentages to increase the size of one of our children // and decrease the size of the other. @@ -123,30 +134,9 @@ bool Pane::_Resize(const Direction& direction) gsl::narrow_cast(_root.ActualHeight()) }; // actualDimension is the size in DIPs of this pane in the direction we're // resizing. - auto actualDimension = changeWidth ? actualSize.Width : actualSize.Height; + const auto actualDimension = changeWidth ? actualSize.Width : actualSize.Height; - const auto firstMinSize = _firstChild->_GetMinSize(); - const auto secondMinSize = _secondChild->_GetMinSize(); - - // These are the minimum amount of space we need for each of our children - const auto firstMinDimension = (changeWidth ? firstMinSize.Width : firstMinSize.Height) + PaneBorderSize; - const auto secondMinDimension = (changeWidth ? secondMinSize.Width : secondMinSize.Height) + PaneBorderSize; - - const auto firstMinPercent = firstMinDimension / actualDimension; - const auto secondMinPercent = secondMinDimension / actualDimension; - - // Make sure that the first pane doesn't get bigger than the space we need - // to reserve for the second. - const auto firstMaxPercent = 1.0f - secondMinPercent; - - if (firstMaxPercent < firstMinPercent) - { - return false; - } - - _firstPercent = std::clamp(_firstPercent.value() - amount, firstMinPercent, firstMaxPercent); - // Update the other child to fill the remaining percent - _secondPercent = 1.0f - _firstPercent.value(); + _desiredSplitPosition = _ClampSplitPosition(changeWidth, _desiredSplitPosition - amount, actualDimension); // Resize our columns to match the new percentages. ResizeContent(actualSize); @@ -342,6 +332,20 @@ void Pane::_ControlConnectionStateChangedHandler(const TermControl& /*sender*/, } } +// Event Description: +// - Called when our control gains focus. We'll use this to trigger our GotFocus +// callback. The tab that's hosting us should have registered a callback which +// can be used to mark us as active. +// Arguments: +// - +// Return Value: +// - +void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& /* sender */, + RoutedEventArgs const& /* args */) +{ + _GotFocusHandlers(shared_from_this()); +} + // Method Description: // - Fire our Closed event to tell our parent that we should be removed. // Arguments: @@ -756,7 +760,7 @@ void Pane::_SetupChildCloseHandlers() // row/cols. The middle one is for the separator. The first and third are for // each of the child panes, and are given a size in pixels, based off the // availiable space, and the percent of the space they respectively consume, -// which is stored in _firstPercent and _secondPercent. +// which is stored in _desiredSplitPosition // - Does nothing if our split state is currently set to SplitState::None // Arguments: // - rootSize: The dimensions in pixels that this pane (and its children should consume.) @@ -769,7 +773,7 @@ void Pane::_CreateRowColDefinitions(const Size& rootSize) _root.ColumnDefinitions().Clear(); // Create two columns in this grid: one for each pane - const auto paneSizes = _GetPaneSizes(rootSize.Width); + const auto paneSizes = _CalcChildrenSizes(rootSize.Width); auto firstColDef = Controls::ColumnDefinition(); firstColDef.Width(GridLengthHelper::FromPixels(paneSizes.first)); @@ -785,7 +789,7 @@ void Pane::_CreateRowColDefinitions(const Size& rootSize) _root.RowDefinitions().Clear(); // Create two rows in this grid: one for each pane - const auto paneSizes = _GetPaneSizes(rootSize.Height); + const auto paneSizes = _CalcChildrenSizes(rootSize.Height); auto firstRowDef = Controls::RowDefinition(); firstRowDef.Height(GridLengthHelper::FromPixels(paneSizes.first)); @@ -1031,11 +1035,7 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState _gotFocusRevoker.revoke(); _splitState = actualSplitType; - - _firstPercent = { Half }; - _secondPercent = { Half }; - - _CreateSplitContent(); + _desiredSplitPosition = Half; // Remove any children we currently have. We can't add the existing // TermControl to a new grid until we do this. @@ -1050,6 +1050,8 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState _control = { nullptr }; _secondChild = std::make_shared(profile, control); + _CreateSplitContent(); + _root.Children().Append(_firstChild->GetRootElement()); _root.Children().Append(_secondChild->GetRootElement()); @@ -1066,24 +1068,303 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState // Method Description: // - Gets the size in pixels of each of our children, given the full size they // should fill. Since these children own their own separators (borders), this -// size is their portion of our _entire_ size. +// size is their portion of our _entire_ size. If specified size is lower than +// required then children will be of minimum size. Snaps first child to grid +// but not the second. // Arguments: // - fullSize: the amount of space in pixels that should be filled by our -// children and their separators +// children and their separators. Can be arbitrarily low. // Return Value: // - a pair with the size of our first child and the size of our second child, // respectively. -std::pair Pane::_GetPaneSizes(const float& fullSize) +std::pair Pane::_CalcChildrenSizes(const float fullSize) const +{ + const auto widthOrHeight = _splitState == SplitState::Vertical; + const auto snappedSizes = _CalcSnappedChildrenSizes(widthOrHeight, fullSize).lower; + + // Keep the first pane snapped and give the second pane all remaining size + return { + snappedSizes.first, + fullSize - snappedSizes.first + }; +} + +// Method Description: +// - Gets the size in pixels of each of our children, given the full size they should +// fill. Each child is snapped to char grid as close as possible. If called multiple +// times with fullSize argument growing, then both returned sizes are guaranteed to be +// non-decreasing (it's a monotonically increasing function). This is important so that +// user doesn't get any pane shrank when they actually expand the window or parent pane. +// That is also required by the layout algorithm. +// Arguments: +// - widthOrHeight: if true, operates on width, otherwise on height. +// - fullSize: the amount of space in pixels that should be filled by our children and +// their separator. Can be arbitrarily low. +// Return Value: +// - a structure holding the result of this calculation. The 'lower' field represents the +// children sizes that would fit in the fullSize, but might (and usually do) not fill it +// completely. The 'higher' field represents the size of the children if they slightly exceed +// the fullSize, but are snapped. If the children can be snapped and also exactly match +// the fullSize, then both this fields have the same value that represent this situation. +Pane::SnapChildrenSizeResult Pane::_CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const { if (_IsLeaf()) { THROW_HR(E_FAIL); } - const auto firstSize = fullSize * _firstPercent.value(); - const auto secondSize = fullSize * _secondPercent.value(); + // First we build a tree of nodes corresponding to the tree of our descendant panes. + // Each node represents a size of given pane. At the beginning, each node has the minimum + // size that the corresponding pane can have; so has the our (root) node. We then gradually + // expand our node (which in turn expands some of the child nodes) until we hit the desired + // size. Since each expand step (done in _AdvanceSnappedDimension()) guarantees that all the + // sizes will be snapped, our return values is also snapped. + // Why do we do it this, iterative way? Why can't we just split the given size by + // _desiredSplitPosition and snap it latter? Because it's hardly doable, if possible, to also + // fulfill the monotonicity requirement that way. As the fullSize increases, the proportional + // point that separates children panes also moves and cells sneak in the available area in + // unpredictable way, regardless which child has the snap priority or whether we snap them + // upward, downward or to nearest. + // With present way we run the same sequence of actions regardless to the fullSize value and + // only just stop at various moments when the built sizes reaches it. Eventually, this could + // be optimized for simple cases like when both children are both leaves with the same character + // size, but it doesn't seem to be beneficial. + + auto sizeTree = _CreateMinSizeTree(widthOrHeight); + LayoutSizeNode lastSizeTree{ sizeTree }; + + while (sizeTree.size < fullSize) + { + lastSizeTree = sizeTree; + _AdvanceSnappedDimension(widthOrHeight, sizeTree); - return { firstSize, secondSize }; + if (sizeTree.size == fullSize) + { + // If we just hit exactly the requested value, then just return the + // current state of children. + return { { sizeTree.firstChild->size, sizeTree.secondChild->size }, + { sizeTree.firstChild->size, sizeTree.secondChild->size } }; + } + } + + // We exceeded the requested size in the loop above, so lastSizeTree will have + // the last good sizes (so that children fit in) and sizeTree has the next possible + // snapped sizes. Return them as lower and higher snap possibilities. + return { { lastSizeTree.firstChild->size, lastSizeTree.secondChild->size }, + { sizeTree.firstChild->size, sizeTree.secondChild->size } }; +} + +// Method Description: +// - Adjusts given dimension (width or height) so that all descendant terminals +// align with their character grids as close as possible. Snaps to closes match +// (either upward or downward). Also makes sure to fit in minimal sizes of the panes. +// Arguments: +// - widthOrHeight: if true operates on width, otherwise on height +// - dimension: a dimension (width or height) to snap +// Return Value: +// - A value corresponding to the next closest snap size for this Pane, either upward or downward +float Pane::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const +{ + const auto [lower, higher] = _CalcSnappedDimension(widthOrHeight, dimension); + return dimension - lower < higher - dimension ? lower : higher; +} + +// Method Description: +// - Adjusts given dimension (width or height) so that all descendant terminals +// align with their character grids as close as possible. Also makes sure to +// fit in minimal sizes of the panes. +// Arguments: +// - widthOrHeight: if true operates on width, otherwise on height +// - dimension: a dimension (width or height) to be snapped +// Return Value: +// - pair of floats, where first value is the size snapped downward (not greater then +// requested size) and second is the size snapped upward (not lower than requested size). +// If requested size is already snapped, then both returned values equal this value. +Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const float dimension) const +{ + if (_IsLeaf()) + { + // If we're a leaf pane, align to the grid of controlling terminal + + const auto minSize = _GetMinSize(); + const auto minDimension = widthOrHeight ? minSize.Width : minSize.Height; + + if (dimension <= minDimension) + { + return { minDimension, minDimension }; + } + + float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension); + if (widthOrHeight) + { + lower += WI_IsFlagSet(_borders, Borders::Left) ? PaneBorderSize : 0; + lower += WI_IsFlagSet(_borders, Borders::Right) ? PaneBorderSize : 0; + } + else + { + lower += WI_IsFlagSet(_borders, Borders::Top) ? PaneBorderSize : 0; + lower += WI_IsFlagSet(_borders, Borders::Bottom) ? PaneBorderSize : 0; + } + + if (lower == dimension) + { + // If we happen to be already snapped, then just return this size + // as both lower and higher values. + return { lower, lower }; + } + else + { + const auto cellSize = _control.CharacterDimensions(); + const auto higher = lower + (widthOrHeight ? cellSize.Width : cellSize.Height); + return { lower, higher }; + } + } + else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) + { + // If we're resizing along separator axis, snap to the closest possibility + // given by our children panes. + + const auto firstSnapped = _firstChild->_CalcSnappedDimension(widthOrHeight, dimension); + const auto secondSnapped = _secondChild->_CalcSnappedDimension(widthOrHeight, dimension); + return { + std::max(firstSnapped.lower, secondSnapped.lower), + std::min(firstSnapped.higher, secondSnapped.higher) + }; + } + else + { + // If we're resizing perpendicularly to separator axis, calculate the sizes + // of child panes that would fit the given size. We use same algorithm that + // is used for real resize routine, but exclude the remaining empty space that + // would appear after the second pane. This will be the 'downward' snap possibility, + // while the 'upward' will be given as a side product of the layout function. + + const auto childSizes = _CalcSnappedChildrenSizes(widthOrHeight, dimension); + return { + childSizes.lower.first + childSizes.lower.second, + childSizes.higher.first + childSizes.higher.second + }; + } +} + +// Method Description: +// - Increases size of given LayoutSizeNode to match next possible 'snap'. In case of leaf +// pane this means the next cell of the terminal. Otherwise it means that one of its children +// advances (recursively). It expects the given node and its descendants to have either +// already snapped or minimum size. +// Arguments: +// - widthOrHeight: if true operates on width, otherwise on height. +// - sizeNode: a layouting node that corresponds to this pane. +// Return Value: +// - +void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) const +{ + if (_IsLeaf()) + { + // We're a leaf pane, so just add one more row or column (unless isMinimumSize + // is true, see below). + + if (sizeNode.isMinimumSize) + { + // If the node is of its minimum size, this size might not be snapped (it might + // be, say, half a character, or fixed 10 pixels), so snap it upward. It might + // however be already snapped, so add 1 to make sure it really increases + // (not strictly necessary but to avoid surprises). + sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher; + } + else + { + const auto cellSize = _control.CharacterDimensions(); + sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height; + } + } + else + { + // We're a parent pane, so we have to advance dimension of our children panes. In + // fact, we advance only one child (chosen later) to keep the growth fine-grained. + + // To choose which child pane to advance, we actually need to know their advanced sizes + // in advance (oh), to see which one would 'fit' better. Often, this is already cached + // by the previous invocation of this function in nextFirstChild and nextSecondChild + // fields of given node. If not, we need to calculate them now. + if (sizeNode.nextFirstChild == nullptr) + { + sizeNode.nextFirstChild = std::make_unique(*sizeNode.firstChild); + _firstChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextFirstChild); + } + if (sizeNode.nextSecondChild == nullptr) + { + sizeNode.nextSecondChild = std::make_unique(*sizeNode.secondChild); + _secondChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextSecondChild); + } + + const auto nextFirstSize = sizeNode.nextFirstChild->size; + const auto nextSecondSize = sizeNode.nextSecondChild->size; + + // Choose which child pane to advance. + bool advanceFirstOrSecond; + if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) + { + // If we're growing along separator axis, choose the child that + // wants to be smaller than the other, so that the resulting size + // will be the smallest. + advanceFirstOrSecond = nextFirstSize < nextSecondSize; + } + else + { + // If we're growing perpendicularly to separator axis, choose a + // child so that their size ratio is closer to that we're trying + // to maintain (this is, the relative separator position is closer + // to the _desiredSplitPosition field). + + const auto firstSize = sizeNode.firstChild->size; + const auto secondSize = sizeNode.secondChild->size; + + // Because we rely on equality check, these calculations have to be + // immune to floating point errors. In common situation where both panes + // have the same character sizes and _desiredSplitPosition is 0.5 (or + // some simple fraction) both ratios will often be the same, and if so + // we always take the left child. It could be right as well, but it's + // important that it's consistent: that it would always go + // 1 -> 2 -> 1 -> 2 -> 1 -> 2 and not like 1 -> 1 -> 2 -> 2 -> 2 -> 1 + // which would look silly to the user but which occur if there was + // a non-floating-point-safe math. + const auto deviation1 = nextFirstSize - (nextFirstSize + secondSize) * _desiredSplitPosition; + const auto deviation2 = -1 * (firstSize - (firstSize + nextSecondSize) * _desiredSplitPosition); + advanceFirstOrSecond = deviation1 <= deviation2; + } + + // Here we advance one of our children. Because we already know the appropriate + // (advanced) size that given child would need to have, we simply assign that size + // to it. We then advance its 'next*' size (nextFirstChild or nextSecondChild) so + // the invariant holds (as it will likely be used by the next invocation of this + // function). The other child's next* size remains unchanged because its size + // haven't changed either. + if (advanceFirstOrSecond) + { + *sizeNode.firstChild = *sizeNode.nextFirstChild; + _firstChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextFirstChild); + } + else + { + *sizeNode.secondChild = *sizeNode.nextSecondChild; + _secondChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextSecondChild); + } + + // Since the size of one of our children has changed we need to update our size as well. + if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) + { + sizeNode.size = std::max(sizeNode.firstChild->size, sizeNode.secondChild->size); + } + else + { + sizeNode.size = sizeNode.firstChild->size + sizeNode.secondChild->size; + } + } + + // Because we have grown, we're certainly no longer of our + // minimal size (if we've ever been). + sizeNode.isMinimumSize = false; } // Method Description: @@ -1103,10 +1384,10 @@ Size Pane::_GetMinSize() const auto newWidth = controlSize.Width; auto newHeight = controlSize.Height; - newWidth += WI_IsFlagSet(_borders, Borders::Left) ? CombinedPaneBorderSize : 0; - newWidth += WI_IsFlagSet(_borders, Borders::Right) ? CombinedPaneBorderSize : 0; - newHeight += WI_IsFlagSet(_borders, Borders::Top) ? CombinedPaneBorderSize : 0; - newHeight += WI_IsFlagSet(_borders, Borders::Bottom) ? CombinedPaneBorderSize : 0; + newWidth += WI_IsFlagSet(_borders, Borders::Left) ? PaneBorderSize : 0; + newWidth += WI_IsFlagSet(_borders, Borders::Right) ? PaneBorderSize : 0; + newHeight += WI_IsFlagSet(_borders, Borders::Top) ? PaneBorderSize : 0; + newHeight += WI_IsFlagSet(_borders, Borders::Bottom) ? PaneBorderSize : 0; return { newWidth, newHeight }; } @@ -1115,25 +1396,58 @@ Size Pane::_GetMinSize() const const auto firstSize = _firstChild->_GetMinSize(); const auto secondSize = _secondChild->_GetMinSize(); - const auto newWidth = firstSize.Width + secondSize.Width; - const auto newHeight = firstSize.Height + secondSize.Height; + const auto minWidth = _splitState == SplitState::Vertical ? + firstSize.Width + secondSize.Width : + std::max(firstSize.Width, secondSize.Width); + const auto minHeight = _splitState == SplitState::Horizontal ? + firstSize.Height + secondSize.Height : + std::max(firstSize.Height, secondSize.Height); - return { newWidth, newHeight }; + return { minWidth, minHeight }; } } -// Event Description: -// - Called when our control gains focus. We'll use this to trigger our GotFocus -// callback. The tab that's hosting us should have registered a callback which -// can be used to mark us as active. +// Method Description: +// - Builds a tree of LayoutSizeNode that matches the tree of panes. Each node +// has minimum size that the corresponding pane can have. // Arguments: -// - +// - widthOrHeight: if true operates on width, otherwise on height // Return Value: -// - -void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& /* sender */, - RoutedEventArgs const& /* args */) +// - Root node of built tree that matches this pane. +Pane::LayoutSizeNode Pane::_CreateMinSizeTree(const bool widthOrHeight) const { - _GotFocusHandlers(shared_from_this()); + const auto size = _GetMinSize(); + LayoutSizeNode node(widthOrHeight ? size.Width : size.Height); + if (!_IsLeaf()) + { + node.firstChild = std::make_unique(_firstChild->_CreateMinSizeTree(widthOrHeight)); + node.secondChild = std::make_unique(_secondChild->_CreateMinSizeTree(widthOrHeight)); + } + + return node; +} + +// Method Description: +// - Adjusts split position so that no child pane is smaller then its +// minimum size +// Arguments: +// - widthOrHeight: if true, operates on width, otherwise on height. +// - requestedValue: split position value to be clamped +// - totalSize: size (width or height) of the parent pane +// Return Value: +// - split position (value in range <0.0, 1.0>) +float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) const +{ + const auto firstMinSize = _firstChild->_GetMinSize(); + const auto secondMinSize = _secondChild->_GetMinSize(); + + const auto firstMinDimension = widthOrHeight ? firstMinSize.Width : firstMinSize.Height; + const auto secondMinDimension = widthOrHeight ? secondMinSize.Width : secondMinSize.Height; + + const auto minSplitPosition = firstMinDimension / totalSize; + const auto maxSplitPosition = 1.0f - (secondMinDimension / totalSize); + + return std::clamp(requestedValue, minSplitPosition, maxSplitPosition); } // Function Description: diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 7ed910f3238..ec57a46dd72 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -54,6 +54,7 @@ class Pane : public std::enable_shared_from_this void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings, const GUID& profile); void ResizeContent(const winrt::Windows::Foundation::Size& newSize); + void Relayout(); bool ResizePane(const winrt::TerminalApp::Direction& direction); bool NavigateFocus(const winrt::TerminalApp::Direction& direction); @@ -61,6 +62,7 @@ class Pane : public std::enable_shared_from_this std::pair, std::shared_ptr> Split(winrt::TerminalApp::SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; void Close(); @@ -68,6 +70,10 @@ class Pane : public std::enable_shared_from_this DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); private: + struct SnapSizeResult; + struct SnapChildrenSizeResult; + struct LayoutSizeNode; + winrt::Windows::UI::Xaml::Controls::Grid _root{}; winrt::Windows::UI::Xaml::Controls::Border _border{}; winrt::Microsoft::Terminal::TerminalControl::TermControl _control{ nullptr }; @@ -77,8 +83,7 @@ class Pane : public std::enable_shared_from_this std::shared_ptr _firstChild{ nullptr }; std::shared_ptr _secondChild{ nullptr }; winrt::TerminalApp::SplitState _splitState{ winrt::TerminalApp::SplitState::None }; - std::optional _firstPercent{ std::nullopt }; - std::optional _secondPercent{ std::nullopt }; + float _desiredSplitPosition; bool _lastActive{ false }; std::optional _profile{ std::nullopt }; @@ -113,12 +118,17 @@ class Pane : public std::enable_shared_from_this void _FocusFirstChild(); void _ControlConnectionStateChangedHandler(const winrt::Microsoft::Terminal::TerminalControl::TermControl& sender, const winrt::Windows::Foundation::IInspectable& /*args*/); + void _ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Windows::UI::Xaml::RoutedEventArgs const& e); - std::pair _GetPaneSizes(const float& fullSize); + std::pair _CalcChildrenSizes(const float fullSize) const; + SnapChildrenSizeResult _CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const; + SnapSizeResult _CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; + void _AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) const; winrt::Windows::Foundation::Size _GetMinSize() const; - void _ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, - winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + LayoutSizeNode _CreateMinSizeTree(const bool widthOrHeight) const; + float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) const; winrt::TerminalApp::SplitState _convertAutomaticSplitState(const winrt::TerminalApp::SplitState& splitType) const; // Function Description: @@ -156,4 +166,41 @@ class Pane : public std::enable_shared_from_this } static void _SetupResources(); + + struct SnapSizeResult + { + float lower; + float higher; + }; + + struct SnapChildrenSizeResult + { + std::pair lower; + std::pair higher; + }; + + // Helper structure that builds a (roughly) binary tree corresponding + // to the pane tree. Used for layouting panes with snapped sizes. + struct LayoutSizeNode + { + float size; + bool isMinimumSize; + std::unique_ptr firstChild; + std::unique_ptr secondChild; + + // These two fields hold next possible snapped values of firstChild and + // secondChild. Although that could be calculated from these fields themself, + // it would be wasteful as we have to know these values more often than for + // simple increment. Hence we cache that here. + std::unique_ptr nextFirstChild; + std::unique_ptr nextSecondChild; + + LayoutSizeNode(const float minSize); + LayoutSizeNode(const LayoutSizeNode& other); + + LayoutSizeNode& operator=(const LayoutSizeNode& other); + + private: + void _AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode); + }; }; diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index b53ddf6bb0f..5c0c952d945 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -248,6 +248,13 @@ void Tab::SplitPane(winrt::TerminalApp::SplitState splitType, const GUID& profil _AttachEventHandlersToPane(second); } +// Method Description: +// - See Pane::CalcSnappedDimension +float Tab::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const +{ + return _rootPane->CalcSnappedDimension(widthOrHeight, dimension); +} + // Method Description: // - Update the size of our panes to fill the new given size. This happens when // the window is resized. @@ -326,6 +333,20 @@ void Tab::_AttachEventHandlersToControl(const TermControl& control) tab->SetTabText(tab->GetActiveTitle()); } }); + + // This is called when the terminal changes its font size or sets it for the first + // time (because when we just create terminal via its ctor it has invalid font size). + // On the latter event, we tell the root pane to resize itself so that its descendants + // (including ourself) can properly snap to character grids. In future, we may also + // want to do that on regular font changes. + control.FontSizeChanged([this](const int /* fontWidth */, + const int /* fontHeight */, + const bool isInitialChange) { + if (isInitialChange) + { + _rootPane->Relayout(); + } + }); } // Method Description: diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 575ec446b35..48c13461302 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -25,6 +25,8 @@ class Tab : public std::enable_shared_from_this bool CanSplitPane(winrt::TerminalApp::SplitState splitType); void SplitPane(winrt::TerminalApp::SplitState splitType, const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; + void UpdateIcon(const winrt::hstring iconPath); void ResizeContent(const winrt::Windows::Foundation::Size& newSize); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 52fe18f3be2..63210042ba3 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1121,6 +1121,14 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - See Pane::CalcSnappedDimension + float TerminalPage::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const + { + const auto focusedTabIndex = _GetFocusedTabIndex(); + return _tabs[focusedTabIndex]->CalcSnappedDimension(widthOrHeight, dimension); + } + // Method Description: // - Place `copiedData` into the clipboard as text. Triggered when a // terminal control raises it's CopyToClipboard event. diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 56beac2851e..0be45149fef 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -29,6 +29,8 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; + void CloseWindow(); // -------------------------------- WinRT Events --------------------------------- diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index eef3ce1637d..358feab1d28 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -133,6 +133,7 @@ + Create diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters index 2840fa83810..03e86156bdb 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj.filters @@ -56,6 +56,9 @@ tab + + pane + @@ -103,6 +106,7 @@ tab + diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index b9b42bd467e..5a99be21d8c 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -594,7 +594,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Initialize our font with the renderer // We don't have to care about DPI. We'll get a change message immediately if it's not 96 // and react accordingly. - _UpdateFont(); + _UpdateFont(true); const COORD windowSize{ static_cast(windowWidth), static_cast(windowHeight) }; @@ -1421,7 +1421,10 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // font change. This method will *not* change the buffer/viewport size // to account for the new glyph dimensions. Callers should make sure to // appropriately call _DoResize after this method is called. - void TermControl::_UpdateFont() + // Arguments: + // - initialUpdate: whether this font update should be considered as being + // concerned with initialization process. Value forwarded to event handler. + void TermControl::_UpdateFont(const bool initialUpdate) { auto lock = _terminal->LockForWriting(); @@ -1430,6 +1433,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // TODO: MSFT:20895307 If the font doesn't exist, this doesn't // actually fail. We need a way to gracefully fallback. _renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont); + + const auto actualNewSize = _actualFont.GetSize(); + _fontSizeChangedHandlers(actualNewSize.X, actualNewSize.Y, initialUpdate); } // Method Description: @@ -1886,13 +1892,41 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation } // Account for the size of any padding - auto thickness = _ParseThicknessFromPadding(_settings.Padding()); - width += thickness.Left + thickness.Right; - height += thickness.Top + thickness.Bottom; + const auto padding = _swapChainPanel.Margin(); + width += padding.Left + padding.Right; + height += padding.Top + padding.Bottom; return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } + // Method Description: + // - Adjusts given dimension (width or height) so that it aligns to the character grid. + // The snap is always downward. + // Arguments: + // - widthOrHeight: if true operates on width, otherwise on height + // - dimension: a dimension (width or height) to be snapped + // Return Value: + // - A dimension that would be aligned to the character grid. + float TermControl::SnapDimensionToGrid(const bool widthOrHeight, const float dimension) const + { + const auto fontSize = _actualFont.GetSize(); + const auto fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; + + const auto padding = _swapChainPanel.Margin(); + auto nonTerminalArea = gsl::narrow_cast(widthOrHeight ? + padding.Left + padding.Right : + padding.Top + padding.Bottom); + + if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible) + { + nonTerminalArea += gsl::narrow_cast(_scrollBar.ActualWidth()); + } + + const auto gridSize = dimension - nonTerminalArea; + const int cells = static_cast(gridSize / fontDimension); + return cells * fontDimension + nonTerminalArea; + } + // Method Description: // - Create XAML Thickness object based on padding props provided. // Used for controlling the TermControl XAML Grid container's Padding prop. @@ -2108,6 +2142,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Winrt events need a method for adding a callback to the event and removing the callback. // These macros will define them both for you. DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs); + DEFINE_EVENT(TermControl, FontSizeChanged, _fontSizeChangedHandlers, TerminalControl::FontSizeChangedEventArgs); DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index a5b47afb33c..56ba48e2bf8 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -67,6 +67,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void Close(); Windows::Foundation::Size CharacterDimensions() const; Windows::Foundation::Size MinimumSize() const; + float SnapDimensionToGrid(const bool widthOrHeight, const float dimension) const; void ScrollViewport(int viewTop); void KeyboardScrollViewport(int viewTop); @@ -94,6 +95,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // clang-format off // -------------------------------- WinRT Events --------------------------------- DECLARE_EVENT(TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs); + DECLARE_EVENT(FontSizeChanged, _fontSizeChangedHandlers, TerminalControl::FontSizeChangedEventArgs); DECLARE_EVENT(ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs); @@ -173,9 +175,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void _InitializeBackgroundBrush(); void _BackgroundColorChanged(const uint32_t color); bool _InitializeTerminal(); - void _UpdateFont(); - void _KeyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + void _UpdateFont(const bool initialUpdate = false); void _SetFontSize(int fontSize); + void _KeyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); void _CharacterHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::CharacterReceivedRoutedEventArgs const& e); void _PointerPressedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void _PointerMovedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 107901490f1..b07a1435deb 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -4,6 +4,7 @@ namespace Microsoft.Terminal.TerminalControl { delegate void TitleChangedEventArgs(String newTitle); + delegate void FontSizeChangedEventArgs(Int32 width, Int32 height, Boolean isInitialChange); delegate void ScrollPositionChangedEventArgs(Int32 viewTop, Int32 viewHeight, Int32 bufferLength); runtimeclass CopyToClipboardEventArgs @@ -28,6 +29,7 @@ namespace Microsoft.Terminal.TerminalControl void UpdateSettings(Microsoft.Terminal.Settings.IControlSettings newSettings); event TitleChangedEventArgs TitleChanged; + event FontSizeChangedEventArgs FontSizeChanged; event Windows.Foundation.TypedEventHandler CopyToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; @@ -43,6 +45,7 @@ namespace Microsoft.Terminal.TerminalControl void Close(); Windows.Foundation.Size CharacterDimensions { get; }; Windows.Foundation.Size MinimumSize { get; }; + Single SnapDimensionToGrid(Boolean widthOrHeight, Single dimension); void ScrollViewport(Int32 viewTop); void KeyboardScrollViewport(Int32 viewTop); diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 75130865b1f..d503f931877 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -40,6 +40,11 @@ AppHost::AppHost() noexcept : std::placeholders::_3); _window->SetCreateCallback(pfn); + _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::AppLogic::CalcSnappedDimension, + _logic, + std::placeholders::_1, + std::placeholders::_2)); + _window->MakeWindow(); } @@ -197,20 +202,11 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter const short islandHeight = Utils::ClampToShortMax( static_cast(ceil(initialSize.Y)), 1); - RECT islandFrame = {}; - bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpix); - // If we failed to get the correct window size for whatever reason, log - // the error and go on. We'll use whatever the control proposed as the - // size of our window, which will be at least close. - LOG_LAST_ERROR_IF(!succeeded); - - if (_useNonClientArea) - { - islandFrame.top = -NonClientIslandWindow::topBorderVisibleHeight; - } - - adjustedWidth = -islandFrame.left + islandWidth + islandFrame.right; - adjustedHeight = -islandFrame.top + islandHeight + islandFrame.bottom; + // Get the size of a window we'd need to host that client rect. This will + // add the titlebar space. + const auto nonClientSize = _window->GetTotalNonClientExclusiveSize(dpix); + adjustedWidth = islandWidth + nonClientSize.cx; + adjustedHeight = islandHeight + nonClientSize.cy; } const COORD origin{ gsl::narrow(proposedRect.left), diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 74cb0492d7c..d0fc574ab55 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -93,6 +93,26 @@ void IslandWindow::SetCreateCallback(std::function +void IslandWindow::SetSnapDimensionCallback(std::function pfn) noexcept +{ + _pfnSnapDimensionCallback = pfn; +} + // Method Description: // - Handles a WM_CREATE message. Calls our create callback, if one's been set. // Arguments: @@ -126,6 +146,95 @@ void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexce UpdateWindow(_window.get()); } +// Method Description: +// - Handles a WM_SIZING message, which occurs when user drags a window border +// or corner. It intercepts this resize action and applies 'snapping' i.e. +// aligns the terminal's size to its cell grid. We're given the window size, +// which we then adjust based on the terminal's properties (like font size). +// Arguments: +// - wParam: Specifies which edge of the window is being dragged. +// - lParam: Pointer to the requested window rectangle (this is, the one that +// originates from current drag action). It also acts as the return value +// (it's a ref parameter). +// Return Value: +// - +LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam) +{ + if (!_pfnSnapDimensionCallback) + { + // If we haven't been given the callback that would adjust the dimension, + // then we can't do anything, so just bail out. + return FALSE; + } + + LPRECT winRect = reinterpret_cast(lParam); + + // Find nearest monitor. + HMONITOR hmon = MonitorFromRect(winRect, MONITOR_DEFAULTTONEAREST); + + // This API guarantees that dpix and dpiy will be equal, but neither is an + // optional parameter so give two UINTs. + UINT dpix = USER_DEFAULT_SCREEN_DPI; + UINT dpiy = USER_DEFAULT_SCREEN_DPI; + // If this fails, we'll use the default of 96. + GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy); + + const auto nonClientSize = GetTotalNonClientExclusiveSize(dpix); + auto clientWidth = winRect->right - winRect->left - nonClientSize.cx; + auto clientHeight = winRect->bottom - winRect->top - nonClientSize.cy; + + if (wParam != WMSZ_TOP && wParam != WMSZ_BOTTOM) + { + // If user has dragged anything but the top or bottom border (so e.g. left border, + // top-right corner etc.), then this means that the width has changed. We thus ask to + // adjust this new width so that terminal(s) is/are aligned to their character grid(s). + clientWidth = gsl::narrow_cast(_pfnSnapDimensionCallback(true, gsl::narrow_cast(clientWidth))); + } + if (wParam != WMSZ_LEFT && wParam != WMSZ_RIGHT) + { + // Analogous to above, but for height. + clientHeight = gsl::narrow_cast(_pfnSnapDimensionCallback(false, gsl::narrow_cast(clientHeight))); + } + + // Now make the window rectangle match the calculated client width and height, + // regarding which border the user is dragging. E.g. if user drags left border, then + // we make sure to adjust the 'left' component of rectangle and not the 'right'. Note + // that top-left and bottom-left corners also 'include' left border, hence we match + // this in multi-case switch. + + // Set width + switch (wParam) + { + case WMSZ_LEFT: + case WMSZ_TOPLEFT: + case WMSZ_BOTTOMLEFT: + winRect->left = winRect->right - (clientWidth + nonClientSize.cx); + break; + case WMSZ_RIGHT: + case WMSZ_TOPRIGHT: + case WMSZ_BOTTOMRIGHT: + winRect->right = winRect->left + (clientWidth + nonClientSize.cx); + break; + } + + // Set height + switch (wParam) + { + case WMSZ_BOTTOM: + case WMSZ_BOTTOMLEFT: + case WMSZ_BOTTOMRIGHT: + winRect->bottom = winRect->top + (clientHeight + nonClientSize.cy); + break; + case WMSZ_TOP: + case WMSZ_TOPLEFT: + case WMSZ_TOPRIGHT: + winRect->top = winRect->bottom - (clientHeight + nonClientSize.cy); + break; + } + + return TRUE; +} + void IslandWindow::Initialize() { const bool initialized = (_interopWindowHandle != nullptr); @@ -205,6 +314,10 @@ void IslandWindow::OnSize(const UINT width, const UINT height) // key that does not correspond to any mnemonic or accelerator key, return MAKELRESULT(0, MNC_CLOSE); } + case WM_SIZING: + { + return _OnSizing(wparam, lparam); + } case WM_CLOSE: { // If the user wants to close the app by clicking 'X' button, @@ -277,6 +390,28 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) _rootGrid.Children().Append(content); } +// Method Description: +// - Gets the difference between window and client area size. +// Arguments: +// - dpi: dpi of a monitor on which the window is placed +// Return Value +// - The size difference +SIZE IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept +{ + const auto windowStyle = static_cast(GetWindowLong(_window.get(), GWL_STYLE)); + RECT islandFrame{}; + + // If we failed to get the correct window size for whatever reason, log + // the error and go on. We'll use whatever the control proposed as the + // size of our window, which will be at least close. + LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi)); + + return { + islandFrame.right - islandFrame.left, + islandFrame.bottom - islandFrame.top + }; +} + void IslandWindow::OnAppInitialized() { // Do a quick resize to force the island to paint diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index d6485554e78..77f8a07364d 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -29,10 +29,12 @@ class IslandWindow : virtual void OnAppInitialized(); virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + virtual SIZE GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; virtual void Initialize(); void SetCreateCallback(std::function pfn) noexcept; + void SetSnapDimensionCallback(std::function pfn) noexcept; void ToggleFullscreen(); @@ -87,8 +89,10 @@ class IslandWindow : winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; std::function _pfnCreateCallback; + std::function _pfnSnapDimensionCallback; void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; + [[nodiscard]] LRESULT _OnSizing(const WPARAM wParam, const LPARAM lParam); bool _fullscreen{ false }; RECT _fullscreenWindowSize; diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 4f58ec92362..fc85e78f977 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -398,6 +398,32 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept return HTCAPTION; } +// Method Description: +// - Gets the difference between window and client area size. +// Arguments: +// - dpi: dpi of a monitor on which the window is placed +// Return Value +// - The size difference +SIZE NonClientIslandWindow::GetTotalNonClientExclusiveSize(UINT dpi) const noexcept +{ + const auto windowStyle = static_cast(GetWindowLong(_window.get(), GWL_STYLE)); + RECT islandFrame{}; + + // If we failed to get the correct window size for whatever reason, log + // the error and go on. We'll use whatever the control proposed as the + // size of our window, which will be at least close. + LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi)); + + islandFrame.top = -topBorderVisibleHeight; + + const auto titleBarHeight = _titlebar ? static_cast(_titlebar.ActualHeight()) : 0; + + return { + islandFrame.right - islandFrame.left, + islandFrame.bottom - islandFrame.top + titleBarHeight + }; +} + // Method Description: // - Updates the borders of our window frame, using DwmExtendFrameIntoClientArea. // Arguments: diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 2b0bbd9ec27..6fdec25c827 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -36,6 +36,8 @@ class NonClientIslandWindow : public IslandWindow [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; + virtual SIZE GetTotalNonClientExclusiveSize(UINT dpi) const noexcept override; + void Initialize() override; void OnAppInitialized() override;