1526 lines
58 KiB
C++
1526 lines
58 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "Pane.h"
|
|
#include "Profile.h"
|
|
#include "CascadiaSettings.h"
|
|
|
|
using namespace winrt::Windows::Foundation;
|
|
using namespace winrt::Windows::UI;
|
|
using namespace winrt::Windows::UI::Xaml;
|
|
using namespace winrt::Windows::UI::Core;
|
|
using namespace winrt::Windows::UI::Xaml::Media;
|
|
using namespace winrt::Microsoft::Terminal::Settings;
|
|
using namespace winrt::Microsoft::Terminal::TerminalControl;
|
|
using namespace winrt::Microsoft::Terminal::TerminalConnection;
|
|
using namespace winrt::TerminalApp;
|
|
using namespace TerminalApp;
|
|
|
|
static const int PaneBorderSize = 2;
|
|
static const int CombinedPaneBorderSize = 2 * PaneBorderSize;
|
|
static const float Half = 0.50f;
|
|
|
|
winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_focusedBorderBrush = { nullptr };
|
|
winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_unfocusedBorderBrush = { nullptr };
|
|
|
|
Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocused) :
|
|
_control{ control },
|
|
_lastActive{ lastFocused },
|
|
_profile{ profile }
|
|
{
|
|
_root.Children().Append(_border);
|
|
_border.Child(_control);
|
|
|
|
_connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
|
|
|
|
// On the first Pane's creation, lookup resources we'll use to theme the
|
|
// Pane, including the brushed to use for the focused/unfocused border
|
|
// color.
|
|
if (s_focusedBorderBrush == nullptr || s_unfocusedBorderBrush == nullptr)
|
|
{
|
|
_SetupResources();
|
|
}
|
|
|
|
// Register an event with the control to have it inform us when it gains focus.
|
|
_gotFocusRevoker = control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler });
|
|
|
|
// When our border is tapped, make sure to transfer focus to our control.
|
|
// LOAD-BEARING: This will NOT work if the border's BorderBrush is set to
|
|
// Colors::Transparent! The border won't get Tapped events, and they'll fall
|
|
// through to something else.
|
|
_border.Tapped([this](auto&, auto& e) {
|
|
_FocusFirstChild();
|
|
e.Handled(true);
|
|
});
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the size of this pane. Resizes each of our columns so they have the
|
|
// same relative sizes, given the newSize.
|
|
// - Because we're just manually setting the row/column sizes in pixels, we have
|
|
// to be told our new size, we can't just use our own OnSized event, because
|
|
// that _won't fire when we get smaller_.
|
|
// Arguments:
|
|
// - newSize: the amount of space that this pane has to fill now.
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::ResizeContent(const Size& newSize)
|
|
{
|
|
const auto width = newSize.Width;
|
|
const auto height = newSize.Height;
|
|
|
|
_CreateRowColDefinitions(newSize);
|
|
|
|
if (_splitState == SplitState::Vertical)
|
|
{
|
|
const auto paneSizes = _CalcChildrenSizes(width);
|
|
|
|
const Size firstSize{ paneSizes.first, height };
|
|
const Size secondSize{ paneSizes.second, height };
|
|
_firstChild->ResizeContent(firstSize);
|
|
_secondChild->ResizeContent(secondSize);
|
|
}
|
|
else if (_splitState == SplitState::Horizontal)
|
|
{
|
|
const auto paneSizes = _CalcChildrenSizes(height);
|
|
|
|
const Size firstSize{ width, paneSizes.first };
|
|
const Size secondSize{ width, paneSizes.second };
|
|
_firstChild->ResizeContent(firstSize);
|
|
_secondChild->ResizeContent(secondSize);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Recalculates and reapplies sizes of all descendant panes.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
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.
|
|
// - Adjusts the separation amount by 5%
|
|
// - Does nothing if the direction doesn't match our current split direction
|
|
// Arguments:
|
|
// - direction: the direction to move our separator. If it's down or right,
|
|
// we'll be increasing the size of the first of our children. Else, we'll be
|
|
// decreasing the size of our first child.
|
|
// Return Value:
|
|
// - false if we couldn't resize this pane in the given direction, else true.
|
|
bool Pane::_Resize(const Direction& direction)
|
|
{
|
|
if (!DirectionMatchesSplit(direction, _splitState))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float amount = .05f;
|
|
if (direction == Direction::Right || direction == Direction::Down)
|
|
{
|
|
amount = -amount;
|
|
}
|
|
|
|
// Make sure we're not making a pane explode here by resizing it to 0 characters.
|
|
const bool changeWidth = _splitState == SplitState::Vertical;
|
|
|
|
const Size actualSize{ gsl::narrow_cast<float>(_root.ActualWidth()),
|
|
gsl::narrow_cast<float>(_root.ActualHeight()) };
|
|
// actualDimension is the size in DIPs of this pane in the direction we're
|
|
// resizing.
|
|
const auto actualDimension = changeWidth ? actualSize.Width : actualSize.Height;
|
|
|
|
_desiredSplitPosition = _ClampSplitPosition(changeWidth, _desiredSplitPosition - amount, actualDimension);
|
|
|
|
// Resize our columns to match the new percentages.
|
|
ResizeContent(actualSize);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Moves the separator between panes, as to resize each child on either size
|
|
// of the separator. Tries to move a separator in the given direction. The
|
|
// separator moved is the separator that's closest depth-wise to the
|
|
// currently focused pane, that's also in the correct direction to be moved.
|
|
// If there isn't such a separator, then this method returns false, as we
|
|
// couldn't handle the resize.
|
|
// Arguments:
|
|
// - direction: The direction to move the separator in.
|
|
// Return Value:
|
|
// - true if we or a child handled this resize request.
|
|
bool Pane::ResizePane(const Direction& direction)
|
|
{
|
|
// If we're a leaf, do nothing. We can't possibly have a descendant with a
|
|
// separator the correct direction.
|
|
if (_IsLeaf())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if either our first or second child is the currently focused leaf.
|
|
// If it is, and the requested resize direction matches our separator, then
|
|
// we're the pane that needs to adjust its separator.
|
|
// If our separator is the wrong direction, then we can't handle it.
|
|
const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive;
|
|
const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive;
|
|
if (firstIsFocused || secondIsFocused)
|
|
{
|
|
return _Resize(direction);
|
|
}
|
|
|
|
// If neither of our children were the focused leaf, then recurse into
|
|
// our children and see if they can handle the resize.
|
|
// For each child, if it has a focused descendant, try having that child
|
|
// handle the resize.
|
|
// If the child wasn't able to handle the resize, it's possible that
|
|
// there were no descendants with a separator the correct direction. If
|
|
// our separator _is_ the correct direction, then we should be the pane
|
|
// to resize. Otherwise, just return false, as we couldn't handle it
|
|
// either.
|
|
if ((!_firstChild->_IsLeaf()) && _firstChild->_HasFocusedChild())
|
|
{
|
|
return _firstChild->ResizePane(direction) || _Resize(direction);
|
|
}
|
|
|
|
if ((!_secondChild->_IsLeaf()) && _secondChild->_HasFocusedChild())
|
|
{
|
|
return _secondChild->ResizePane(direction) || _Resize(direction);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Attempts to handle moving focus to one of our children. If our split
|
|
// direction isn't appropriate for the move direction, then we'll return
|
|
// false, to try and let our parent handle the move. If our child we'd move
|
|
// focus to is already focused, we'll also return false, to again let our
|
|
// parent try and handle the focus movement.
|
|
// Arguments:
|
|
// - direction: The direction to move the focus in.
|
|
// Return Value:
|
|
// - true if we handled this focus move request.
|
|
bool Pane::_NavigateFocus(const Direction& direction)
|
|
{
|
|
if (!DirectionMatchesSplit(direction, _splitState))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const bool focusSecond = (direction == Direction::Right) || (direction == Direction::Down);
|
|
|
|
const auto newlyFocusedChild = focusSecond ? _secondChild : _firstChild;
|
|
|
|
// If the child we want to move focus to is _already_ focused, return false,
|
|
// to try and let our parent figure it out.
|
|
if (newlyFocusedChild->_HasFocusedChild())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Transfer focus to our child, and update the focus of our tree.
|
|
newlyFocusedChild->_FocusFirstChild();
|
|
UpdateVisuals();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Attempts to move focus to one of our children. If we have a focused child,
|
|
// we'll try to move the focus in the direction requested.
|
|
// - If there isn't a pane that exists as a child of this pane in the correct
|
|
// direction, we'll return false. This will indicate to our parent that they
|
|
// should try and move the focus themselves. In this way, the focus can move
|
|
// up and down the tree to the correct pane.
|
|
// - This method is _very_ similar to ResizePane. Both are trying to find the
|
|
// right separator to move (focus) in a direction.
|
|
// Arguments:
|
|
// - direction: The direction to move the focus in.
|
|
// Return Value:
|
|
// - true if we or a child handled this focus move request.
|
|
bool Pane::NavigateFocus(const Direction& direction)
|
|
{
|
|
// If we're a leaf, do nothing. We can't possibly have a descendant with a
|
|
// separator the correct direction.
|
|
if (_IsLeaf())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if either our first or second child is the currently focused leaf.
|
|
// If it is, and the requested move direction matches our separator, then
|
|
// we're the pane that needs to handle this focus move.
|
|
const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive;
|
|
const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive;
|
|
if (firstIsFocused || secondIsFocused)
|
|
{
|
|
return _NavigateFocus(direction);
|
|
}
|
|
|
|
// If neither of our children were the focused leaf, then recurse into
|
|
// our children and see if they can handle the focus move.
|
|
// For each child, if it has a focused descendant, try having that child
|
|
// handle the focus move.
|
|
// If the child wasn't able to handle the focus move, it's possible that
|
|
// there were no descendants with a separator the correct direction. If
|
|
// our separator _is_ the correct direction, then we should be the pane
|
|
// to move focus into our other child. Otherwise, just return false, as
|
|
// we couldn't handle it either.
|
|
if ((!_firstChild->_IsLeaf()) && _firstChild->_HasFocusedChild())
|
|
{
|
|
return _firstChild->NavigateFocus(direction) || _NavigateFocus(direction);
|
|
}
|
|
|
|
if ((!_secondChild->_IsLeaf()) && _secondChild->_HasFocusedChild())
|
|
{
|
|
return _secondChild->NavigateFocus(direction) || _NavigateFocus(direction);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Called when our attached control is closed. Triggers listeners to our close
|
|
// event, if we're a leaf pane.
|
|
// - If this was called, and we became a parent pane (due to work on another
|
|
// thread), this function will do nothing (allowing the control's new parent
|
|
// to handle the event instead).
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_ControlConnectionStateChangedHandler(const TermControl& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/)
|
|
{
|
|
std::unique_lock lock{ _createCloseLock };
|
|
// It's possible that this event handler started being executed, then before
|
|
// we got the lock, another thread created another child. So our control is
|
|
// actually no longer _our_ control, and instead could be a descendant.
|
|
//
|
|
// When the control's new Pane takes ownership of the control, the new
|
|
// parent will register it's own event handler. That event handler will get
|
|
// fired after this handler returns, and will properly cleanup state.
|
|
if (!_IsLeaf())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto newConnectionState = _control.ConnectionState();
|
|
|
|
if (newConnectionState < ConnectionState::Closed)
|
|
{
|
|
// Pane doesn't care if the connection isn't entering a terminal state.
|
|
return;
|
|
}
|
|
|
|
const auto& settings = CascadiaSettings::GetCurrentAppSettings();
|
|
auto paneProfile = settings.FindProfile(_profile.value());
|
|
if (paneProfile)
|
|
{
|
|
auto mode = paneProfile->GetCloseOnExitMode();
|
|
if ((mode == CloseOnExitMode::Always) ||
|
|
(mode == CloseOnExitMode::Graceful && newConnectionState == ConnectionState::Closed))
|
|
{
|
|
_ClosedHandlers(nullptr, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// - <unused>
|
|
// Return Value:
|
|
// - <none>
|
|
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:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::Close()
|
|
{
|
|
// Fire our Closed event to tell our parent that we should be removed.
|
|
_ClosedHandlers(nullptr, nullptr);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Prepare this pane to be removed from the UI hierarchy by closing all controls
|
|
// and connections beneath it.
|
|
void Pane::Shutdown()
|
|
{
|
|
// Lock the create/close lock so that another operation won't concurrently
|
|
// modify our tree
|
|
std::unique_lock lock{ _createCloseLock };
|
|
if (_IsLeaf())
|
|
{
|
|
_control.Close();
|
|
}
|
|
else
|
|
{
|
|
_firstChild->Shutdown();
|
|
_secondChild->Shutdown();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Get the root UIElement of this pane. There may be a single TermControl as a
|
|
// child, or an entire tree of grids and panes as children of this element.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - the Grid acting as the root of this pane.
|
|
Controls::Grid Pane::GetRootElement()
|
|
{
|
|
return _root;
|
|
}
|
|
|
|
// Method Description:
|
|
// - If this is the last focused pane, returns itself. Returns nullptr if this
|
|
// is a leaf and it's not focused. If it's a parent, it returns nullptr if no
|
|
// children of this pane were the last pane to be focused, or the Pane that
|
|
// _was_ the last pane to be focused (if there was one).
|
|
// - This Pane's control might not currently be focused, if the tab itself is
|
|
// not currently focused.
|
|
// Return Value:
|
|
// - nullptr if we're a leaf and unfocused, or no children were marked
|
|
// `_lastActive`, else returns this
|
|
std::shared_ptr<Pane> Pane::GetActivePane()
|
|
{
|
|
if (_IsLeaf())
|
|
{
|
|
return _lastActive ? shared_from_this() : nullptr;
|
|
}
|
|
|
|
auto firstFocused = _firstChild->GetActivePane();
|
|
if (firstFocused != nullptr)
|
|
{
|
|
return firstFocused;
|
|
}
|
|
return _secondChild->GetActivePane();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Gets the TermControl of this pane. If this Pane is not a leaf, this will return nullptr.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - nullptr if this Pane is a parent, otherwise the TermControl of this Pane.
|
|
TermControl Pane::GetTerminalControl()
|
|
{
|
|
return _IsLeaf() ? _control : nullptr;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Recursively remove the "Active" state from this Pane and all it's children.
|
|
// - Updates our visuals to match our new state, including highlighting our borders.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::ClearActive()
|
|
{
|
|
_lastActive = false;
|
|
if (!_IsLeaf())
|
|
{
|
|
_firstChild->ClearActive();
|
|
_secondChild->ClearActive();
|
|
}
|
|
UpdateVisuals();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets the "Active" state on this Pane. Only one Pane in a tree of Panes
|
|
// should be "active", and that pane should be a leaf.
|
|
// - Updates our visuals to match our new state, including highlighting our borders.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::SetActive()
|
|
{
|
|
_lastActive = true;
|
|
UpdateVisuals();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns nullopt if no children of this pane were the last control to be
|
|
// focused, or the GUID of the profile of the last control to be focused (if
|
|
// there was one).
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - nullopt if no children of this pane were the last control to be
|
|
// focused, else the GUID of the profile of the last control to be focused
|
|
std::optional<GUID> Pane::GetFocusedProfile()
|
|
{
|
|
auto lastFocused = GetActivePane();
|
|
return lastFocused ? lastFocused->_profile : std::nullopt;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns true if this pane was the last pane to be focused in a tree of panes.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - true iff we were the last pane focused in this tree of panes.
|
|
bool Pane::WasLastFocused() const noexcept
|
|
{
|
|
return _lastActive;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns true iff this pane has no child panes.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - true iff this pane has no child panes.
|
|
bool Pane::_IsLeaf() const noexcept
|
|
{
|
|
return _splitState == SplitState::None;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns true if this pane is currently focused, or there is a pane which is
|
|
// a child of this pane that is actively focused
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - true if the currently focused pane is either this pane, or one of this
|
|
// pane's descendants
|
|
bool Pane::_HasFocusedChild() const noexcept
|
|
{
|
|
// We're intentionally making this one giant expression, so the compiler
|
|
// will skip the following lookups if one of the lookups before it returns
|
|
// true
|
|
return (_control && _lastActive) ||
|
|
(_firstChild && _firstChild->_HasFocusedChild()) ||
|
|
(_secondChild && _secondChild->_HasFocusedChild());
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update the focus state of this pane. We'll make sure to colorize our
|
|
// borders depending on if we are the active pane or not.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::UpdateVisuals()
|
|
{
|
|
_border.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Focuses this control if we're a leaf, or attempts to focus the first leaf
|
|
// of our first child, recursively.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_FocusFirstChild()
|
|
{
|
|
if (_IsLeaf())
|
|
{
|
|
_control.Focus(FocusState::Programmatic);
|
|
}
|
|
else
|
|
{
|
|
_firstChild->_FocusFirstChild();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Attempts to update the settings of this pane or any children of this pane.
|
|
// * If this pane is a leaf, and our profile guid matches the parameter, then
|
|
// we'll apply the new settings to our control.
|
|
// * If we're not a leaf, we'll recurse on our children.
|
|
// Arguments:
|
|
// - settings: The new TerminalSettings to apply to any matching controls
|
|
// - profile: The GUID of the profile these settings should apply to.
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::UpdateSettings(const TerminalSettings& settings, const GUID& profile)
|
|
{
|
|
if (!_IsLeaf())
|
|
{
|
|
_firstChild->UpdateSettings(settings, profile);
|
|
_secondChild->UpdateSettings(settings, profile);
|
|
}
|
|
else
|
|
{
|
|
if (profile == _profile)
|
|
{
|
|
_control.UpdateSettings(settings);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Closes one of our children. In doing so, takes the control from the other
|
|
// child, and makes this pane a leaf node again.
|
|
// Arguments:
|
|
// - closeFirst: if true, the first child should be closed, and the second
|
|
// should be preserved, and vice-versa for false.
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_CloseChild(const bool closeFirst)
|
|
{
|
|
// Lock the create/close lock so that another operation won't concurrently
|
|
// modify our tree
|
|
std::unique_lock lock{ _createCloseLock };
|
|
|
|
// If we're a leaf, then chances are both our children closed in close
|
|
// succession. We waited on the lock while the other child was closed, so
|
|
// now we don't have a child to close anymore. Return here. When we moved
|
|
// the non-closed child into us, we also set up event handlers that will be
|
|
// triggered when we return from this.
|
|
if (_IsLeaf())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto closedChild = closeFirst ? _firstChild : _secondChild;
|
|
auto remainingChild = closeFirst ? _secondChild : _firstChild;
|
|
|
|
// If the only child left is a leaf, that means we're a leaf now.
|
|
if (remainingChild->_IsLeaf())
|
|
{
|
|
// When the remaining child is a leaf, that means both our children were
|
|
// previously leaves, and the only difference in their borders is the
|
|
// border that we gave them. Take a bitwise AND of those two children to
|
|
// remove that border. Other borders the children might have, they
|
|
// inherited from us, so the flag will be set for both children.
|
|
_borders = _firstChild->_borders & _secondChild->_borders;
|
|
|
|
// take the control and profile of the pane that _wasn't_ closed.
|
|
_control = remainingChild->_control;
|
|
_profile = remainingChild->_profile;
|
|
|
|
// Add our new event handler before revoking the old one.
|
|
_connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler });
|
|
|
|
// Revoke the old event handlers. Remove both the handlers for the panes
|
|
// themselves closing, and remove their handlers for their controls
|
|
// closing. At this point, if the remaining child's control is closed,
|
|
// they'll trigger only our event handler for the control's close.
|
|
_firstChild->Closed(_firstClosedToken);
|
|
_secondChild->Closed(_secondClosedToken);
|
|
closedChild->_control.ConnectionStateChanged(closedChild->_connectionStateChangedToken);
|
|
remainingChild->_control.ConnectionStateChanged(remainingChild->_connectionStateChangedToken);
|
|
|
|
// If either of our children was focused, we want to take that focus from
|
|
// them.
|
|
_lastActive = _firstChild->_lastActive || _secondChild->_lastActive;
|
|
|
|
// Remove all the ui elements of our children. This'll make sure we can
|
|
// re-attach the TermControl to our Grid.
|
|
_firstChild->_root.Children().Clear();
|
|
_secondChild->_root.Children().Clear();
|
|
_firstChild->_border.Child(nullptr);
|
|
_secondChild->_border.Child(nullptr);
|
|
|
|
// Reset our UI:
|
|
_root.Children().Clear();
|
|
_border.Child(nullptr);
|
|
_root.ColumnDefinitions().Clear();
|
|
_root.RowDefinitions().Clear();
|
|
|
|
// Reattach the TermControl to our grid.
|
|
_root.Children().Append(_border);
|
|
_border.Child(_control);
|
|
|
|
// Make sure to set our _splitState before focusing the control. If you
|
|
// fail to do this, when the tab handles the GotFocus event and asks us
|
|
// what our active control is, we won't technically be a "leaf", and
|
|
// GetTerminalControl will return null.
|
|
_splitState = SplitState::None;
|
|
|
|
// re-attach our handler for the control's GotFocus event.
|
|
_gotFocusRevoker = _control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler });
|
|
|
|
// If we're inheriting the "last active" state from one of our children,
|
|
// focus our control now. This should trigger our own GotFocus event.
|
|
if (_lastActive)
|
|
{
|
|
_control.Focus(FocusState::Programmatic);
|
|
}
|
|
|
|
_UpdateBorders();
|
|
|
|
// Release our children.
|
|
_firstChild = nullptr;
|
|
_secondChild = nullptr;
|
|
}
|
|
else
|
|
{
|
|
// Determine which border flag we gave to the child when we first split
|
|
// it, so that we can take just that flag away from them.
|
|
Borders clearBorderFlag = Borders::None;
|
|
if (_splitState == SplitState::Horizontal)
|
|
{
|
|
clearBorderFlag = closeFirst ? Borders::Top : Borders::Bottom;
|
|
}
|
|
else if (_splitState == SplitState::Vertical)
|
|
{
|
|
clearBorderFlag = closeFirst ? Borders::Left : Borders::Right;
|
|
}
|
|
|
|
// First stash away references to the old panes and their tokens
|
|
const auto oldFirstToken = _firstClosedToken;
|
|
const auto oldSecondToken = _secondClosedToken;
|
|
const auto oldFirst = _firstChild;
|
|
const auto oldSecond = _secondChild;
|
|
|
|
// Steal all the state from our child
|
|
_splitState = remainingChild->_splitState;
|
|
_firstChild = remainingChild->_firstChild;
|
|
_secondChild = remainingChild->_secondChild;
|
|
|
|
// Set up new close handlers on the children
|
|
_SetupChildCloseHandlers();
|
|
|
|
// Revoke the old event handlers on our new children
|
|
_firstChild->Closed(remainingChild->_firstClosedToken);
|
|
_secondChild->Closed(remainingChild->_secondClosedToken);
|
|
|
|
// Revoke event handlers on old panes and controls
|
|
oldFirst->Closed(oldFirstToken);
|
|
oldSecond->Closed(oldSecondToken);
|
|
closedChild->_control.ConnectionStateChanged(closedChild->_connectionStateChangedToken);
|
|
|
|
// Reset our UI:
|
|
_root.Children().Clear();
|
|
_border.Child(nullptr);
|
|
_root.ColumnDefinitions().Clear();
|
|
_root.RowDefinitions().Clear();
|
|
|
|
// Copy the old UI over to our grid.
|
|
// Start by copying the row/column definitions. Iterate over the
|
|
// rows/cols, and remove each one from the old grid, and attach it to
|
|
// our grid instead.
|
|
while (remainingChild->_root.ColumnDefinitions().Size() > 0)
|
|
{
|
|
auto col = remainingChild->_root.ColumnDefinitions().GetAt(0);
|
|
remainingChild->_root.ColumnDefinitions().RemoveAt(0);
|
|
_root.ColumnDefinitions().Append(col);
|
|
}
|
|
while (remainingChild->_root.RowDefinitions().Size() > 0)
|
|
{
|
|
auto row = remainingChild->_root.RowDefinitions().GetAt(0);
|
|
remainingChild->_root.RowDefinitions().RemoveAt(0);
|
|
_root.RowDefinitions().Append(row);
|
|
}
|
|
|
|
// Remove the child's UI elements from the child's grid, so we can
|
|
// attach them to us instead.
|
|
remainingChild->_root.Children().Clear();
|
|
remainingChild->_border.Child(nullptr);
|
|
|
|
_root.Children().Append(_firstChild->GetRootElement());
|
|
_root.Children().Append(_secondChild->GetRootElement());
|
|
|
|
// Take the flag away from the children that they inherited from their
|
|
// parent, and update their borders to visually match
|
|
WI_ClearAllFlags(_firstChild->_borders, clearBorderFlag);
|
|
WI_ClearAllFlags(_secondChild->_borders, clearBorderFlag);
|
|
_UpdateBorders();
|
|
_firstChild->_UpdateBorders();
|
|
_secondChild->_UpdateBorders();
|
|
|
|
// If the closed child was focused, transfer the focus to it's first sibling.
|
|
if (closedChild->_lastActive)
|
|
{
|
|
_FocusFirstChild();
|
|
}
|
|
|
|
// Release the pointers that the child was holding.
|
|
remainingChild->_firstChild = nullptr;
|
|
remainingChild->_secondChild = nullptr;
|
|
}
|
|
}
|
|
|
|
winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst)
|
|
{
|
|
auto weakThis{ shared_from_this() };
|
|
|
|
co_await winrt::resume_foreground(_root.Dispatcher());
|
|
|
|
if (auto pane{ weakThis.get() })
|
|
{
|
|
_CloseChild(closeFirst);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Adds event handlers to our children to handle their close events.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_SetupChildCloseHandlers()
|
|
{
|
|
_firstClosedToken = _firstChild->Closed([this](auto&& /*s*/, auto&& /*e*/) {
|
|
_CloseChildRoutine(true);
|
|
});
|
|
|
|
_secondClosedToken = _secondChild->Closed([this](auto&& /*s*/, auto&& /*e*/) {
|
|
_CloseChildRoutine(false);
|
|
});
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets up row/column definitions for this pane. There are three total
|
|
// 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
|
|
// available space, and the percent of the space they respectively consume,
|
|
// 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.)
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_CreateRowColDefinitions(const Size& rootSize)
|
|
{
|
|
if (_splitState == SplitState::Vertical)
|
|
{
|
|
_root.ColumnDefinitions().Clear();
|
|
|
|
// Create two columns in this grid: one for each pane
|
|
const auto paneSizes = _CalcChildrenSizes(rootSize.Width);
|
|
|
|
auto firstColDef = Controls::ColumnDefinition();
|
|
firstColDef.Width(GridLengthHelper::FromPixels(paneSizes.first));
|
|
|
|
auto secondColDef = Controls::ColumnDefinition();
|
|
secondColDef.Width(GridLengthHelper::FromPixels(paneSizes.second));
|
|
|
|
_root.ColumnDefinitions().Append(firstColDef);
|
|
_root.ColumnDefinitions().Append(secondColDef);
|
|
}
|
|
else if (_splitState == SplitState::Horizontal)
|
|
{
|
|
_root.RowDefinitions().Clear();
|
|
|
|
// Create two rows in this grid: one for each pane
|
|
const auto paneSizes = _CalcChildrenSizes(rootSize.Height);
|
|
|
|
auto firstRowDef = Controls::RowDefinition();
|
|
firstRowDef.Height(GridLengthHelper::FromPixels(paneSizes.first));
|
|
|
|
auto secondRowDef = Controls::RowDefinition();
|
|
secondRowDef.Height(GridLengthHelper::FromPixels(paneSizes.second));
|
|
|
|
_root.RowDefinitions().Append(firstRowDef);
|
|
_root.RowDefinitions().Append(secondRowDef);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Initializes our UI for a new split in this pane. Sets up row/column
|
|
// definitions, and initializes the separator grid. Does nothing if our split
|
|
// state is currently set to SplitState::None
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_CreateSplitContent()
|
|
{
|
|
Size actualSize{ gsl::narrow_cast<float>(_root.ActualWidth()),
|
|
gsl::narrow_cast<float>(_root.ActualHeight()) };
|
|
|
|
_CreateRowColDefinitions(actualSize);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets the thickness of each side of our borders to match our _borders state.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_UpdateBorders()
|
|
{
|
|
double top = 0, bottom = 0, left = 0, right = 0;
|
|
|
|
Thickness newBorders{ 0 };
|
|
if (WI_IsFlagSet(_borders, Borders::Top))
|
|
{
|
|
top = PaneBorderSize;
|
|
}
|
|
if (WI_IsFlagSet(_borders, Borders::Bottom))
|
|
{
|
|
bottom = PaneBorderSize;
|
|
}
|
|
if (WI_IsFlagSet(_borders, Borders::Left))
|
|
{
|
|
left = PaneBorderSize;
|
|
}
|
|
if (WI_IsFlagSet(_borders, Borders::Right))
|
|
{
|
|
right = PaneBorderSize;
|
|
}
|
|
_border.BorderThickness(ThicknessHelper::FromLengths(left, top, right, bottom));
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets the row/column of our child UI elements, to match our current split type.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_ApplySplitDefinitions()
|
|
{
|
|
if (_splitState == SplitState::Vertical)
|
|
{
|
|
Controls::Grid::SetColumn(_firstChild->GetRootElement(), 0);
|
|
Controls::Grid::SetColumn(_secondChild->GetRootElement(), 1);
|
|
|
|
_firstChild->_borders = _borders | Borders::Right;
|
|
_secondChild->_borders = _borders | Borders::Left;
|
|
_borders = Borders::None;
|
|
|
|
_UpdateBorders();
|
|
_firstChild->_UpdateBorders();
|
|
_secondChild->_UpdateBorders();
|
|
}
|
|
else if (_splitState == SplitState::Horizontal)
|
|
{
|
|
Controls::Grid::SetRow(_firstChild->GetRootElement(), 0);
|
|
Controls::Grid::SetRow(_secondChild->GetRootElement(), 1);
|
|
|
|
_firstChild->_borders = _borders | Borders::Bottom;
|
|
_secondChild->_borders = _borders | Borders::Top;
|
|
_borders = Borders::None;
|
|
|
|
_UpdateBorders();
|
|
_firstChild->_UpdateBorders();
|
|
_secondChild->_UpdateBorders();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Determines whether the pane can be split
|
|
// Arguments:
|
|
// - splitType: what type of split we want to create.
|
|
// Return Value:
|
|
// - True if the pane can be split. False otherwise.
|
|
bool Pane::CanSplit(SplitState splitType)
|
|
{
|
|
if (_IsLeaf())
|
|
{
|
|
return _CanSplit(splitType);
|
|
}
|
|
|
|
if (_firstChild->_HasFocusedChild())
|
|
{
|
|
return _firstChild->CanSplit(splitType);
|
|
}
|
|
|
|
if (_secondChild->_HasFocusedChild())
|
|
{
|
|
return _secondChild->CanSplit(splitType);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Split the focused pane in our tree of panes, and place the given
|
|
// TermControl into the newly created pane. If we're the focused pane, then
|
|
// we'll create two new children, and place them side-by-side in our Grid.
|
|
// Arguments:
|
|
// - splitType: what type of split we want to create.
|
|
// - profile: The profile GUID to associate with the newly created pane.
|
|
// - control: A TermControl to use in the new pane.
|
|
// Return Value:
|
|
// - The two newly created Panes
|
|
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::Split(SplitState splitType, const GUID& profile, const TermControl& control)
|
|
{
|
|
if (!_IsLeaf())
|
|
{
|
|
if (_firstChild->_HasFocusedChild())
|
|
{
|
|
return _firstChild->Split(splitType, profile, control);
|
|
}
|
|
else if (_secondChild->_HasFocusedChild())
|
|
{
|
|
return _secondChild->Split(splitType, profile, control);
|
|
}
|
|
|
|
return { nullptr, nullptr };
|
|
}
|
|
|
|
return _Split(splitType, profile, control);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Converts an "automatic" split type into either Vertical or Horizontal,
|
|
// based upon the current dimensions of the Pane.
|
|
// - If any of the other SplitState values are passed in, they're returned
|
|
// unmodified.
|
|
// Arguments:
|
|
// - splitType: The SplitState to attempt to convert
|
|
// Return Value:
|
|
// - None if splitType was None, otherwise one of Horizontal or Vertical
|
|
SplitState Pane::_convertAutomaticSplitState(const SplitState& splitType) const
|
|
{
|
|
// Careful here! If the pane doesn't yet have a size, these dimensions will
|
|
// be 0, and we'll always return Vertical.
|
|
|
|
if (splitType == SplitState::Automatic)
|
|
{
|
|
// If the requested split type was "auto", determine which direction to
|
|
// split based on our current dimensions
|
|
const Size actualSize{ gsl::narrow_cast<float>(_root.ActualWidth()),
|
|
gsl::narrow_cast<float>(_root.ActualHeight()) };
|
|
return actualSize.Width >= actualSize.Height ? SplitState::Vertical : SplitState::Horizontal;
|
|
}
|
|
return splitType;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Determines whether the pane can be split.
|
|
// Arguments:
|
|
// - splitType: what type of split we want to create.
|
|
// Return Value:
|
|
// - True if the pane can be split. False otherwise.
|
|
bool Pane::_CanSplit(SplitState splitType)
|
|
{
|
|
const Size actualSize{ gsl::narrow_cast<float>(_root.ActualWidth()),
|
|
gsl::narrow_cast<float>(_root.ActualHeight()) };
|
|
|
|
const Size minSize = _GetMinSize();
|
|
|
|
auto actualSplitType = _convertAutomaticSplitState(splitType);
|
|
|
|
if (actualSplitType == SplitState::None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (actualSplitType == SplitState::Vertical)
|
|
{
|
|
const auto widthMinusSeparator = actualSize.Width - CombinedPaneBorderSize;
|
|
const auto newWidth = widthMinusSeparator * Half;
|
|
|
|
return newWidth > minSize.Width;
|
|
}
|
|
|
|
if (actualSplitType == SplitState::Horizontal)
|
|
{
|
|
const auto heightMinusSeparator = actualSize.Height - CombinedPaneBorderSize;
|
|
const auto newHeight = heightMinusSeparator * Half;
|
|
|
|
return newHeight > minSize.Height;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Does the bulk of the work of creating a new split. Initializes our UI,
|
|
// creates a new Pane to host the control, registers event handlers.
|
|
// Arguments:
|
|
// - splitType: what type of split we should create.
|
|
// - profile: The profile GUID to associate with the newly created pane.
|
|
// - control: A TermControl to use in the new pane.
|
|
// Return Value:
|
|
// - The two newly created Panes
|
|
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& control)
|
|
{
|
|
if (splitType == SplitState::None)
|
|
{
|
|
return { nullptr, nullptr };
|
|
}
|
|
|
|
auto actualSplitType = _convertAutomaticSplitState(splitType);
|
|
|
|
// Lock the create/close lock so that another operation won't concurrently
|
|
// modify our tree
|
|
std::unique_lock lock{ _createCloseLock };
|
|
|
|
// revoke our handler - the child will take care of the control now.
|
|
_control.ConnectionStateChanged(_connectionStateChangedToken);
|
|
_connectionStateChangedToken.value = 0;
|
|
|
|
// Remove our old GotFocus handler from the control. We don't what the
|
|
// control telling us that it's now focused, we want it telling its new
|
|
// parent.
|
|
_gotFocusRevoker.revoke();
|
|
|
|
_splitState = actualSplitType;
|
|
_desiredSplitPosition = Half;
|
|
|
|
// Remove any children we currently have. We can't add the existing
|
|
// TermControl to a new grid until we do this.
|
|
_root.Children().Clear();
|
|
_border.Child(nullptr);
|
|
|
|
// Create two new Panes
|
|
// Move our control, guid into the first one.
|
|
// Move the new guid, control into the second.
|
|
_firstChild = std::make_shared<Pane>(_profile.value(), _control);
|
|
_profile = std::nullopt;
|
|
_control = { nullptr };
|
|
_secondChild = std::make_shared<Pane>(profile, control);
|
|
|
|
_CreateSplitContent();
|
|
|
|
_root.Children().Append(_firstChild->GetRootElement());
|
|
_root.Children().Append(_secondChild->GetRootElement());
|
|
|
|
_ApplySplitDefinitions();
|
|
|
|
// Register event handlers on our children to handle their Close events
|
|
_SetupChildCloseHandlers();
|
|
|
|
_lastActive = false;
|
|
|
|
return { _firstChild, _secondChild };
|
|
}
|
|
|
|
// 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. 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. 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<float, float> 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);
|
|
}
|
|
|
|
// 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);
|
|
|
|
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:
|
|
// - <none>
|
|
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<LayoutSizeNode>(*sizeNode.firstChild);
|
|
_firstChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextFirstChild);
|
|
}
|
|
if (sizeNode.nextSecondChild == nullptr)
|
|
{
|
|
sizeNode.nextSecondChild = std::make_unique<LayoutSizeNode>(*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:
|
|
// - Get the absolute minimum size that this pane can be resized to and still
|
|
// have 1x1 character visible, in each of its children. If we're a leaf, we'll
|
|
// include the space needed for borders _within_ us.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - The minimum size that this pane can be resized to and still have a visible
|
|
// character.
|
|
Size Pane::_GetMinSize() const
|
|
{
|
|
if (_IsLeaf())
|
|
{
|
|
auto controlSize = _control.MinimumSize();
|
|
auto newWidth = controlSize.Width;
|
|
auto newHeight = controlSize.Height;
|
|
|
|
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 };
|
|
}
|
|
else
|
|
{
|
|
const auto firstSize = _firstChild->_GetMinSize();
|
|
const auto secondSize = _secondChild->_GetMinSize();
|
|
|
|
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 { minWidth, minHeight };
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// - Root node of built tree that matches this pane.
|
|
Pane::LayoutSizeNode Pane::_CreateMinSizeTree(const bool widthOrHeight) const
|
|
{
|
|
const auto size = _GetMinSize();
|
|
LayoutSizeNode node(widthOrHeight ? size.Width : size.Height);
|
|
if (!_IsLeaf())
|
|
{
|
|
node.firstChild = std::make_unique<LayoutSizeNode>(_firstChild->_CreateMinSizeTree(widthOrHeight));
|
|
node.secondChild = std::make_unique<LayoutSizeNode>(_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:
|
|
// - Attempts to load some XAML resources that the Pane will need. This includes:
|
|
// * The Color we'll use for active Panes's borders - SystemAccentColor
|
|
// * The Brush we'll use for inactive Panes - TabViewBackground (to match the
|
|
// color of the titlebar)
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Pane::_SetupResources()
|
|
{
|
|
const auto res = Application::Current().Resources();
|
|
const auto accentColorKey = winrt::box_value(L"SystemAccentColor");
|
|
if (res.HasKey(accentColorKey))
|
|
{
|
|
const auto colorFromResources = res.Lookup(accentColorKey);
|
|
// If SystemAccentColor is _not_ a Color for some reason, use
|
|
// Transparent as the color, so we don't do this process again on
|
|
// the next pane (by leaving s_focusedBorderBrush nullptr)
|
|
auto actualColor = winrt::unbox_value_or<Color>(colorFromResources, Colors::Black());
|
|
s_focusedBorderBrush = SolidColorBrush(actualColor);
|
|
}
|
|
else
|
|
{
|
|
// DON'T use Transparent here - if it's "Transparent", then it won't
|
|
// be able to hittest for clicks, and then clicking on the border
|
|
// will eat focus.
|
|
s_focusedBorderBrush = SolidColorBrush{ Colors::Black() };
|
|
}
|
|
|
|
const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground");
|
|
if (res.HasKey(tabViewBackgroundKey))
|
|
{
|
|
winrt::Windows::Foundation::IInspectable obj = res.Lookup(tabViewBackgroundKey);
|
|
s_unfocusedBorderBrush = obj.try_as<winrt::Windows::UI::Xaml::Media::SolidColorBrush>();
|
|
}
|
|
else
|
|
{
|
|
// DON'T use Transparent here - if it's "Transparent", then it won't
|
|
// be able to hittest for clicks, and then clicking on the border
|
|
// will eat focus.
|
|
s_unfocusedBorderBrush = SolidColorBrush{ Colors::Black() };
|
|
}
|
|
}
|
|
|
|
DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
|