From b8a5725a8e8e3bf1b7511f030c334b4ab0a0bb8a Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Tue, 24 Sep 2019 19:35:29 +0200 Subject: [PATCH 01/24] Kinda works, no splitted panes --- src/cascadia/TerminalApp/App.cpp | 5 ++ src/cascadia/TerminalApp/App.h | 1 + src/cascadia/TerminalApp/App.idl | 1 + src/cascadia/TerminalApp/Pane.cpp | 21 ++++++++ src/cascadia/TerminalApp/Pane.h | 2 + src/cascadia/TerminalApp/Tab.cpp | 5 ++ src/cascadia/TerminalApp/Tab.h | 1 + src/cascadia/TerminalApp/TerminalPage.cpp | 7 +++ src/cascadia/TerminalApp/TerminalPage.h | 2 + src/cascadia/TerminalControl/TermControl.cpp | 29 +++++++++++ src/cascadia/TerminalControl/TermControl.h | 1 + src/cascadia/TerminalControl/TermControl.idl | 1 + src/cascadia/WindowsTerminal/AppHost.cpp | 5 ++ src/cascadia/WindowsTerminal/IslandWindow.cpp | 50 +++++++++++++++++++ src/cascadia/WindowsTerminal/IslandWindow.h | 4 +- 15 files changed, 134 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index 9a99091d9fa..13b144907fe 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -653,6 +653,11 @@ namespace winrt::TerminalApp::implementation } } + int App::SnapDimension(bool widthOrHeight, int value) + { + return _root->SnapDimension(widthOrHeight, value); + } + // Methods that proxy typed event handlers through TerminalPage winrt::event_token App::SetTitleBarContent(Windows::Foundation::TypedEventHandler const& handler) { diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h index 3896c412fc4..78e19140c87 100644 --- a/src/cascadia/TerminalApp/App.h +++ b/src/cascadia/TerminalApp/App.h @@ -39,6 +39,7 @@ namespace winrt::TerminalApp::implementation hstring Title(); void TitlebarClicked(); + int SnapDimension(bool widthOrHeight, int value); void WindowCloseButtonClicked(); diff --git a/src/cascadia/TerminalApp/App.idl b/src/cascadia/TerminalApp/App.idl index c724191ac36..4e87be1a048 100644 --- a/src/cascadia/TerminalApp/App.idl +++ b/src/cascadia/TerminalApp/App.idl @@ -35,6 +35,7 @@ namespace TerminalApp Boolean GetShowTabsInTitlebar(); void TitlebarClicked(); void WindowCloseButtonClicked(); + UInt32 SnapDimension(Boolean widthOrHeight, UInt32 value); event Windows.Foundation.TypedEventHandler SetTitleBarContent; event Windows.Foundation.TypedEventHandler TitleChanged; diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 3096dddb362..4f75c7f190f 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -320,6 +320,27 @@ void Pane::Close() _closedHandlers(); } +std::pair Pane::SnapDimension(bool widthOrHeight, int value) +{ + if (_IsLeaf()) + { + const auto val = _control.SnapDimensionToGrid(widthOrHeight, value); + return std::make_pair(gsl::narrow_cast(val), gsl::narrow_cast(val << 32)); + } + else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) + { + const auto firstResult = _firstChild->SnapDimension(widthOrHeight, value); + const auto secondResult = _secondChild->SnapDimension(widthOrHeight, value); + const int lowerValue = std::max(firstResult.first, secondResult.first); + const int higherValue = std::min(firstResult.second, secondResult.second); + return std::make_pair(lowerValue, higherValue); + } + else + { + throw std::bad_exception(); + } +} + // 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. diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index d526153720e..b67a9d0f7ec 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -53,6 +53,7 @@ class Pane : public std::enable_shared_from_this void Split(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); void Close(); + std::pair SnapDimension(bool widthOrHeight, int value); DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs); @@ -130,4 +131,5 @@ class Pane : public std::enable_shared_from_this } return false; } + }; diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index f0e2efa88c2..e5626dbcc7d 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -230,6 +230,11 @@ void Tab::SplitPane(Pane::SplitState splitType, const GUID& profile, TermControl _rootPane->Split(splitType, profile, control); } +std::pair Tab::SnapDimension(bool widthOrHeight, int value) +{ + return _rootPane->SnapDimension(widthOrHeight, value); +} + // Method Description: // - Update the size of our panes to fill the new given size. This happens when // the window is resized. diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 8af4423e6b8..7a8aa83db5f 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -22,6 +22,7 @@ class Tab bool CanSplitPane(Pane::SplitState splitType); void SplitPane(Pane::SplitState splitType, const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + std::pair SnapDimension(bool widthOrHeight, int value); void UpdateFocus(); void UpdateIcon(const winrt::hstring iconPath); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 4e0a38247e5..dda52b9b87a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1058,6 +1058,13 @@ namespace winrt::TerminalApp::implementation } } + int TerminalPage::SnapDimension(bool widthOrHeight, int value) + { + const auto focusedTabIndex = _GetFocusedTabIndex(); + const auto closestValues = _tabs[focusedTabIndex]->SnapDimension(widthOrHeight, value); + return value - closestValues.first > closestValues.second - value ? closestValues.first : closestValues.second; + } + // 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 dafec743fe6..26c6ce175a6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -36,6 +36,8 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); + int SnapDimension(bool widthOrHeight, int value); + void CloseWindow(); // -------------------------------- WinRT Events --------------------------------- diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index a52040bc481..40634aeb0af 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1643,6 +1643,35 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } + uint64_t TermControl::SnapDimensionToGrid(bool widthOrHeight, int value) + { + const auto fontSize = _actualFont.GetSize(); + const int fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; + + const int padding = gsl::narrow_cast(widthOrHeight ? + _swapChainPanel.Margin().Left + _swapChainPanel.Margin().Right : + _swapChainPanel.Margin().Top + _swapChainPanel.Margin().Bottom); + + int gridSize = value - padding; + if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible) + { + gridSize -= gsl::narrow_cast(_scrollBar.ActualWidth()); + } + + const int cells = gridSize / fontDimension; + const int lowerValue = cells * fontDimension + padding; + if (lowerValue == value) + { + return static_cast(lowerValue) | static_cast(lowerValue) << 32; //std::make_pair(lowerValue, lowerValue); + } + else + { + const int higherWidth = lowerValue + fontDimension; + //return std::make_pair(lowerValue, higherWidth); + return static_cast(lowerValue) | static_cast(higherWidth) << 32; + } + } + // Method Description: // - Create XAML Thickness object based on padding props provided. // Used for controlling the TermControl XAML Grid container's Padding prop. diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index ee3597ba46b..52d322900b3 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -62,6 +62,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation bool ShouldCloseOnExit() const noexcept; Windows::Foundation::Size CharacterDimensions() const; Windows::Foundation::Size MinimumSize() const; + uint64_t SnapDimensionToGrid(bool widthOrHeight, int value); void ScrollViewport(int viewTop); void KeyboardScrollViewport(int viewTop); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 82452d14add..95045c2144f 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -31,6 +31,7 @@ namespace Microsoft.Terminal.TerminalControl event ConnectionClosedEventArgs ConnectionClosed; event Windows.Foundation.TypedEventHandler CopyToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; + UInt64 SnapDimensionToGrid(Boolean widthOrHeight, UInt32 value); String Title { get; }; diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 8985f23202a..8e9f5fa22eb 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -37,6 +37,11 @@ AppHost::AppHost() noexcept : std::placeholders::_3); _window->SetCreateCallback(pfn); + _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::App::SnapDimension, + _app, + std::placeholders::_1, + std::placeholders::_2)); + _window->MakeWindow(); } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index f546cc4dc0d..bd99f723ae6 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -93,6 +93,11 @@ void IslandWindow::SetCreateCallback(std::function pfn) noexcept +{ + _pfnSnapDimensionCallback = pfn; +} + // Method Description: // - Handles a WM_CREATE message. Calls our create callback, if one's been set. // Arguments: @@ -205,6 +210,51 @@ 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: + { + LPRECT r = (LPRECT)lparam; + int width = r->right - r->left; + int height = r->bottom - r->top; + + if (wparam != WMSZ_TOP && wparam != WMSZ_BOTTOM) + { + width = _pfnSnapDimensionCallback(true, width); + } + if (wparam != WMSZ_LEFT && wparam != WMSZ_RIGHT) + { + height = _pfnSnapDimensionCallback(false, height); + } + + switch (wparam) + { + case WMSZ_LEFT: + case WMSZ_TOPLEFT: + case WMSZ_BOTTOMLEFT: + r->left = r->right - width; + break; + case WMSZ_RIGHT: + case WMSZ_TOPRIGHT: + case WMSZ_BOTTOMRIGHT: + r->right = r->left + width; + break; + } + + switch (wparam) + { + case WMSZ_BOTTOM: + case WMSZ_BOTTOMLEFT: + case WMSZ_BOTTOMRIGHT: + r->bottom = r->top + height; + break; + case WMSZ_TOP: + case WMSZ_TOPLEFT: + case WMSZ_TOPRIGHT: + r->top = r->bottom - height; + break; + } + + return true; //ew || eh; + } case WM_CLOSE: { // If the user wants to close the app by clicking 'X' button, diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 374949f0c79..9b835fc47d2 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -32,7 +32,8 @@ class IslandWindow : virtual void Initialize(); - void SetCreateCallback(std::function pfn) noexcept; + void SetCreateCallback(std::function pfn) noexcept; + void SetSnapDimensionCallback(std::function pfn) noexcept; void UpdateTheme(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); @@ -87,6 +88,7 @@ class IslandWindow : winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; std::function _pfnCreateCallback; + std::function _pfnSnapDimensionCallback; void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; }; From 68f613b7c483e65434db5e8f2b61ae77702f95bc Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Thu, 26 Sep 2019 19:37:17 +0200 Subject: [PATCH 02/24] Calc sizes more accurately --- src/cascadia/TerminalApp/Pane.cpp | 10 +- src/cascadia/TerminalApp/TerminalPage.cpp | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 20 +-- src/cascadia/TerminalControl/TermControl.h | 2 +- src/cascadia/TerminalControl/TermControl.idl | 2 +- src/cascadia/WindowsTerminal/AppHost.cpp | 155 +++++++----------- src/cascadia/WindowsTerminal/IslandWindow.cpp | 45 ++++- src/cascadia/WindowsTerminal/IslandWindow.h | 1 + .../WindowsTerminal/NonClientIslandWindow.cpp | 12 ++ .../WindowsTerminal/NonClientIslandWindow.h | 1 + 10 files changed, 127 insertions(+), 123 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 4f75c7f190f..b8f027e9318 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -324,8 +324,14 @@ std::pair Pane::SnapDimension(bool widthOrHeight, int value) { if (_IsLeaf()) { - const auto val = _control.SnapDimensionToGrid(widthOrHeight, value); - return std::make_pair(gsl::narrow_cast(val), gsl::narrow_cast(val << 32)); + const int lower = _control.SnapDimensionToGrid(widthOrHeight, value); + int higher = lower; + if (value != lower) + { + const auto cellSize = _control.CharacterDimensions(); + higher = lower + static_cast(widthOrHeight ? cellSize.Width : cellSize.Height); + } + return std::make_pair(lower, higher); } else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) { diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index dda52b9b87a..492a0f158b8 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1062,7 +1062,7 @@ namespace winrt::TerminalApp::implementation { const auto focusedTabIndex = _GetFocusedTabIndex(); const auto closestValues = _tabs[focusedTabIndex]->SnapDimension(widthOrHeight, value); - return value - closestValues.first > closestValues.second - value ? closestValues.first : closestValues.second; + return value - closestValues.first < closestValues.second - value ? closestValues.first : closestValues.second; } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 40634aeb0af..6bbd584f422 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1643,33 +1643,23 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } - uint64_t TermControl::SnapDimensionToGrid(bool widthOrHeight, int value) + int TermControl::SnapDimensionToGrid(bool widthOrHeight, int value) { const auto fontSize = _actualFont.GetSize(); const int fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; - const int padding = gsl::narrow_cast(widthOrHeight ? + int nonTerminalArea = gsl::narrow_cast(widthOrHeight ? _swapChainPanel.Margin().Left + _swapChainPanel.Margin().Right : _swapChainPanel.Margin().Top + _swapChainPanel.Margin().Bottom); - int gridSize = value - padding; if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible) { - gridSize -= gsl::narrow_cast(_scrollBar.ActualWidth()); + nonTerminalArea += gsl::narrow_cast(_scrollBar.ActualWidth()); } + const int gridSize = value - nonTerminalArea; const int cells = gridSize / fontDimension; - const int lowerValue = cells * fontDimension + padding; - if (lowerValue == value) - { - return static_cast(lowerValue) | static_cast(lowerValue) << 32; //std::make_pair(lowerValue, lowerValue); - } - else - { - const int higherWidth = lowerValue + fontDimension; - //return std::make_pair(lowerValue, higherWidth); - return static_cast(lowerValue) | static_cast(higherWidth) << 32; - } + return cells * fontDimension + nonTerminalArea; } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 52d322900b3..9c71c1be778 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -62,7 +62,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation bool ShouldCloseOnExit() const noexcept; Windows::Foundation::Size CharacterDimensions() const; Windows::Foundation::Size MinimumSize() const; - uint64_t SnapDimensionToGrid(bool widthOrHeight, int value); + int SnapDimensionToGrid(bool widthOrHeight, int value); void ScrollViewport(int viewTop); void KeyboardScrollViewport(int viewTop); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 95045c2144f..92426b8b572 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -31,7 +31,7 @@ namespace Microsoft.Terminal.TerminalControl event ConnectionClosedEventArgs ConnectionClosed; event Windows.Foundation.TypedEventHandler CopyToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; - UInt64 SnapDimensionToGrid(Boolean widthOrHeight, UInt32 value); + UInt32 SnapDimensionToGrid(Boolean widthOrHeight, UInt32 value); String Title { get; }; diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 8e9f5fa22eb..7198db80b18 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -70,7 +70,7 @@ void AppHost::Initialize() if (_useNonClientArea) { - // Register our callbar for when the app's non-client content changes. + // Register our callbar for when the app's non-clientRect content changes. // This has to be done _before_ App::Create, as the app might set the // content in Create. _app.SetTitleBarContent({ this, &AppHost::_UpdateTitleBarContent }); @@ -149,99 +149,66 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter long adjustedHeight = 0; long adjustedWidth = 0; - if (launchMode == winrt::TerminalApp::LaunchMode::DefaultMode) - { - // Find nearest montitor. - HMONITOR hmon = MonitorFromRect(&proposedRect, MONITOR_DEFAULTTONEAREST); - - // Get nearest monitor information - MONITORINFO monitorInfo; - monitorInfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(hmon, &monitorInfo); - - // 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); - - // We need to check if the top left point of the titlebar of the window is within any screen - RECT offScreenTestRect; - offScreenTestRect.left = proposedRect.left; - offScreenTestRect.top = proposedRect.top; - offScreenTestRect.right = offScreenTestRect.left + 1; - offScreenTestRect.bottom = offScreenTestRect.top + 1; - - bool isTitlebarIntersectWithMonitors = false; - EnumDisplayMonitors( - nullptr, &offScreenTestRect, [](HMONITOR, HDC, LPRECT, LPARAM lParam) -> BOOL { - auto intersectWithMonitor = reinterpret_cast(lParam); - *intersectWithMonitor = true; - // Continue the enumeration - return FALSE; - }, - reinterpret_cast(&isTitlebarIntersectWithMonitors)); - - if (!isTitlebarIntersectWithMonitors) - { - // If the title bar is out-of-screen, we set the initial position to - // the top left corner of the nearest monitor - proposedRect.left = monitorInfo.rcWork.left; - proposedRect.top = monitorInfo.rcWork.top; - } - - auto initialSize = _app.GetLaunchDimensions(dpix); - - const short _currentWidth = Utils::ClampToShortMax( - static_cast(ceil(initialSize.X)), 1); - const short _currentHeight = Utils::ClampToShortMax( - static_cast(ceil(initialSize.Y)), 1); - - // Create a RECT from our requested client size - auto nonClient = Viewport::FromDimensions({ _currentWidth, - _currentHeight }) - .ToRect(); - - // Get the size of a window we'd need to host that client rect. This will - // add the titlebar space. - if (_useNonClientArea) - { - // If we're in NC tabs mode, do the math ourselves. Get the margins - // we're using for the window - this will include the size of the - // titlebar content. - const auto pNcWindow = static_cast(_window.get()); - const MARGINS margins = pNcWindow->GetFrameMargins(); - nonClient.left = 0; - nonClient.top = 0; - nonClient.right = margins.cxLeftWidth + nonClient.right + margins.cxRightWidth; - nonClient.bottom = margins.cyTopHeight + nonClient.bottom + margins.cyBottomHeight; - } - else - { - bool succeeded = AdjustWindowRectExForDpi(&nonClient, WS_OVERLAPPEDWINDOW, false, 0, dpix); - if (!succeeded) - { - // 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(); - nonClient = Viewport::FromDimensions({ _currentWidth, - _currentHeight }) - .ToRect(); - } - - // For client island scenario, there is an invisible border of 8 pixels. - // We need to remove this border to guarantee the left edge of the window - // coincides with the screen - const auto pCWindow = static_cast(_window.get()); - const RECT frame = pCWindow->GetFrameBorderMargins(dpix); - proposedRect.left += frame.left; - } - - adjustedHeight = nonClient.bottom - nonClient.top; - adjustedWidth = nonClient.right - nonClient.left; - } + if (launchMode == winrt::TerminalApp::LaunchMode::DefaultMode) + { + // Find nearest montitor. + HMONITOR hmon = MonitorFromRect(&proposedRect, MONITOR_DEFAULTTONEAREST); + + // Get nearest monitor information + MONITORINFO monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(hmon, &monitorInfo); + + // 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); + + // We need to check if the top left point of the titlebar of the window is within any screen + RECT offScreenTestRect; + offScreenTestRect.left = proposedRect.left; + offScreenTestRect.top = proposedRect.top; + offScreenTestRect.right = offScreenTestRect.left + 1; + offScreenTestRect.bottom = offScreenTestRect.top + 1; + + bool isTitlebarIntersectWithMonitors = false; + EnumDisplayMonitors( + nullptr, &offScreenTestRect, [](HMONITOR, HDC, LPRECT, LPARAM lParam) -> BOOL { + auto intersectWithMonitor = reinterpret_cast(lParam); + *intersectWithMonitor = true; + // Continue the enumeration + return FALSE; + }, + reinterpret_cast(&isTitlebarIntersectWithMonitors)); + + if (!isTitlebarIntersectWithMonitors) + { + // If the title bar is out-of-screen, we set the initial position to + // the top left corner of the nearest monitor + proposedRect.left = monitorInfo.rcWork.left; + proposedRect.top = monitorInfo.rcWork.top; + } + + auto initialSize = _app.GetLaunchDimensions(dpix); + + const short _currentWidth = Utils::ClampToShortMax( + static_cast(ceil(initialSize.X)), 1); + const short _currentHeight = Utils::ClampToShortMax( + static_cast(ceil(initialSize.Y)), 1); + + // Create a RECT from our requested clientRect size + const auto clientRect = Viewport::FromDimensions({ _currentWidth, + _currentHeight }) + .ToRect(); + + // Get the size of a window we'd need to host that client rect. This will + // add the titlebar space. + const auto client2win = _window->GetClientToWinSizeDelta(dpix); + adjustedHeight = clientRect.bottom - clientRect.top + client2win.cx; + adjustedWidth = clientRect.right - clientRect.left + client2win.cy; + } const COORD origin{ gsl::narrow(proposedRect.left), gsl::narrow(proposedRect.top) }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index bd99f723ae6..dbb4121e723 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -212,17 +212,29 @@ void IslandWindow::OnSize(const UINT width, const UINT height) } case WM_SIZING: { - LPRECT r = (LPRECT)lparam; - int width = r->right - r->left; - int height = r->bottom - r->top; + LPRECT winRect = (LPRECT)lparam; + + // Find nearest montitor. + 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 client2win = GetClientToWinSizeDelta(dpix); + int clientWidth = winRect->right - winRect->left - client2win.cx; + int clientHeight = winRect->bottom - winRect->top - client2win.cy; if (wparam != WMSZ_TOP && wparam != WMSZ_BOTTOM) { - width = _pfnSnapDimensionCallback(true, width); + clientWidth = _pfnSnapDimensionCallback(true, clientWidth); } if (wparam != WMSZ_LEFT && wparam != WMSZ_RIGHT) { - height = _pfnSnapDimensionCallback(false, height); + clientHeight = _pfnSnapDimensionCallback(false, clientHeight); } switch (wparam) @@ -230,12 +242,12 @@ void IslandWindow::OnSize(const UINT width, const UINT height) case WMSZ_LEFT: case WMSZ_TOPLEFT: case WMSZ_BOTTOMLEFT: - r->left = r->right - width; + winRect->left = winRect->right - (clientWidth + client2win.cx); break; case WMSZ_RIGHT: case WMSZ_TOPRIGHT: case WMSZ_BOTTOMRIGHT: - r->right = r->left + width; + winRect->right = winRect->left + (clientWidth + client2win.cx); break; } @@ -244,12 +256,12 @@ void IslandWindow::OnSize(const UINT width, const UINT height) case WMSZ_BOTTOM: case WMSZ_BOTTOMLEFT: case WMSZ_BOTTOMRIGHT: - r->bottom = r->top + height; + winRect->bottom = winRect->top + (clientHeight + client2win.cy); break; case WMSZ_TOP: case WMSZ_TOPLEFT: case WMSZ_TOPRIGHT: - r->top = r->bottom - height; + winRect->top = winRect->bottom - (clientHeight + client2win.cy); break; } @@ -335,6 +347,21 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) _rootGrid.Children().Append(content); } +SIZE IslandWindow::GetClientToWinSizeDelta(UINT dpix) const noexcept +{ + RECT rect{}; + bool succeeded = AdjustWindowRectExForDpi(&rect, WS_OVERLAPPEDWINDOW, false, 0, dpix); + if (!succeeded) + { + // 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(); + } + + return { rect.right - rect.left, rect.bottom - rect.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 9b835fc47d2..a503dc9145c 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -29,6 +29,7 @@ class IslandWindow : void OnRestore() override; virtual void OnAppInitialized(); virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); + virtual SIZE GetClientToWinSizeDelta(UINT dpix) const noexcept; virtual void Initialize(); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 0908481d67b..ad87d5cd700 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -340,6 +340,18 @@ MARGINS NonClientIslandWindow::GetFrameMargins() const noexcept return margins; } +SIZE NonClientIslandWindow::GetClientToWinSizeDelta(UINT /* dpix */) const noexcept +{ + SIZE offset; + // If we're in NC tabs mode, do the math ourselves. Get the margins + // we're using for the window - this will include the size of the + // titlebar content. + const MARGINS margins = GetFrameMargins(); + offset.cx = margins.cxLeftWidth + margins.cxRightWidth; + offset.cy = margins.cyTopHeight + margins.cyBottomHeight; + return offset; +} + // 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 4c1d5cdc5cc..7f6c4ea0389 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -34,6 +34,7 @@ class NonClientIslandWindow : public IslandWindow [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; MARGINS GetFrameMargins() const noexcept; + virtual SIZE GetClientToWinSizeDelta(UINT dpix) const noexcept override; void Initialize() override; From 5d0f1d3eca3bbfbcb52ddc16929769e47e9b2122 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Sat, 28 Sep 2019 21:49:00 +0200 Subject: [PATCH 03/24] General work, add resolution for children along resize axis --- src/cascadia/TerminalApp/App.cpp | 2 +- src/cascadia/TerminalApp/App.h | 2 +- src/cascadia/TerminalApp/App.idl | 2 +- src/cascadia/TerminalApp/Pane.cpp | 191 ++++++++++++------ src/cascadia/TerminalApp/Pane.h | 9 +- src/cascadia/TerminalApp/Tab.cpp | 4 +- src/cascadia/TerminalApp/Tab.h | 2 +- src/cascadia/TerminalApp/TerminalPage.cpp | 5 +- src/cascadia/TerminalApp/TerminalPage.h | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 12 +- src/cascadia/TerminalControl/TermControl.h | 2 +- src/cascadia/TerminalControl/TermControl.idl | 2 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 6 +- src/cascadia/WindowsTerminal/IslandWindow.h | 4 +- 14 files changed, 156 insertions(+), 89 deletions(-) diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index 13b144907fe..693b5b98346 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -655,7 +655,7 @@ namespace winrt::TerminalApp::implementation int App::SnapDimension(bool widthOrHeight, int value) { - return _root->SnapDimension(widthOrHeight, value); + return _root->SnapDimension(widthOrHeight, dimension); } // Methods that proxy typed event handlers through TerminalPage diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h index 78e19140c87..5fd5cf3fbe4 100644 --- a/src/cascadia/TerminalApp/App.h +++ b/src/cascadia/TerminalApp/App.h @@ -39,7 +39,7 @@ namespace winrt::TerminalApp::implementation hstring Title(); void TitlebarClicked(); - int SnapDimension(bool widthOrHeight, int value); + float SnapDimension(bool widthOrHeight, float dimension); void WindowCloseButtonClicked(); diff --git a/src/cascadia/TerminalApp/App.idl b/src/cascadia/TerminalApp/App.idl index 4e87be1a048..62df43dd535 100644 --- a/src/cascadia/TerminalApp/App.idl +++ b/src/cascadia/TerminalApp/App.idl @@ -35,7 +35,7 @@ namespace TerminalApp Boolean GetShowTabsInTitlebar(); void TitlebarClicked(); void WindowCloseButtonClicked(); - UInt32 SnapDimension(Boolean widthOrHeight, UInt32 value); + Single SnapDimension(Boolean widthOrHeight, Single dimension); event Windows.Foundation.TypedEventHandler SetTitleBarContent; event Windows.Foundation.TypedEventHandler TitleChanged; diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index b8f027e9318..f7e35578937 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -58,7 +58,7 @@ void Pane::ResizeContent(const Size& newSize) if (_splitState == SplitState::Vertical) { - const auto paneSizes = _GetPaneSizes(width); + const auto paneSizes = _GetPaneSizes(true, width); const Size firstSize{ paneSizes.first, height }; const Size secondSize{ paneSizes.second, height }; @@ -67,7 +67,7 @@ void Pane::ResizeContent(const Size& newSize) } else if (_splitState == SplitState::Horizontal) { - const auto paneSizes = _GetPaneSizes(height); + const auto paneSizes = _GetPaneSizes(false, height); const Size firstSize{ width, paneSizes.first }; const Size secondSize{ width, paneSizes.second }; @@ -107,26 +107,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; - actualDimension -= PaneSeparatorSize; + 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; - const auto secondMinDimension = changeWidth ? secondMinSize.Width : secondMinSize.Height; - - 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; - - _firstPercent = std::clamp(_firstPercent.value() - amount, firstMinPercent, firstMaxPercent); - // Update the other child to fill the remaining percent - _secondPercent = 1.0f - _firstPercent.value(); + _desiredSplitPosition = _CampSplitPosition(changeWidth, _desiredSplitPosition - amount, actualDimension); // Resize our columns to match the new percentages. ResizeContent(actualSize); @@ -308,6 +291,23 @@ void Pane::_ControlClosedHandler() } } + +// Method Description: +// - Adjusts given size 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 ensures it fits in minimal size of the pane. +// Arguments: +// - widthOrHeight: if true operates on width, otherwise on height +// - dimension: a dimension (width or height) to snap +// Return Value: +// - calculated dimension +float Pane::SnapDimension(const bool widthOrHeight, const float dimension) +{ + const auto lower = _SnapDimension(widthOrHeight, false, dimension); + const auto higher = _SnapDimension(widthOrHeight, true, dimension); + return dimension - lower < higher - dimension ? lower : higher; +} + // Method Description: // - Fire our Closed event to tell our parent that we should be removed. // Arguments: @@ -320,33 +320,6 @@ void Pane::Close() _closedHandlers(); } -std::pair Pane::SnapDimension(bool widthOrHeight, int value) -{ - if (_IsLeaf()) - { - const int lower = _control.SnapDimensionToGrid(widthOrHeight, value); - int higher = lower; - if (value != lower) - { - const auto cellSize = _control.CharacterDimensions(); - higher = lower + static_cast(widthOrHeight ? cellSize.Width : cellSize.Height); - } - return std::make_pair(lower, higher); - } - else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) - { - const auto firstResult = _firstChild->SnapDimension(widthOrHeight, value); - const auto secondResult = _secondChild->SnapDimension(widthOrHeight, value); - const int lowerValue = std::max(firstResult.first, secondResult.first); - const int higherValue = std::min(firstResult.second, secondResult.second); - return std::make_pair(lowerValue, higherValue); - } - else - { - throw std::bad_exception(); - } -} - // 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. @@ -697,7 +670,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.) @@ -713,7 +686,7 @@ void Pane::_CreateRowColDefinitions(const Size& rootSize) auto separatorColDef = Controls::ColumnDefinition(); separatorColDef.Width(GridLengthHelper::Auto()); - const auto paneSizes = _GetPaneSizes(rootSize.Width); + const auto paneSizes = _GetPaneSizes(true, rootSize.Width); auto firstColDef = Controls::ColumnDefinition(); firstColDef.Width(GridLengthHelper::FromPixels(paneSizes.first)); @@ -733,7 +706,7 @@ void Pane::_CreateRowColDefinitions(const Size& rootSize) auto separatorRowDef = Controls::RowDefinition(); separatorRowDef.Height(GridLengthHelper::Auto()); - const auto paneSizes = _GetPaneSizes(rootSize.Height); + const auto paneSizes = _GetPaneSizes(false, rootSize.Height); auto firstRowDef = Controls::RowDefinition(); firstRowDef.Height(GridLengthHelper::FromPixels(paneSizes.first)); @@ -909,11 +882,7 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& _connectionClosedToken.value = 0; _splitState = splitType; - - _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. @@ -927,6 +896,8 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& _control = { nullptr }; _secondChild = std::make_shared(profile, control); + _CreateSplitContent(); + _root.Children().Append(_firstChild->GetRootElement()); _root.Children().Append(_separatorRoot); _root.Children().Append(_secondChild->GetRootElement()); @@ -941,27 +912,123 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& // Method Description: // - Gets the size in pixels of each of our children, given the full size they -// should fill. Accounts for the size of the separator that should be between -// them as well. +// should fill. If specified size is lower than required then children will be +// of minimum size. Snaps first child to grid but not the second. Accounts for +// the size of the separator that should be between them as well. // 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 +// children and their separator. 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::_GetPaneSizes(const bool widthOrHeight, float fullSize) { if (_IsLeaf()) { THROW_HR(E_FAIL); } + const auto minSize = _GetMinSize(); + const auto minDimension = widthOrHeight ? minSize.Width : minSize.Height; + fullSize = std::max(fullSize, minDimension); + const auto sizeMinusSeparator = fullSize - PaneSeparatorSize; - const auto firstSize = sizeMinusSeparator * _firstPercent.value(); - const auto secondSize = sizeMinusSeparator * _secondPercent.value(); + + const auto proportionalFirstSize = sizeMinusSeparator * _desiredSplitPosition; + auto firstSize = _firstChild->SnapDimension(widthOrHeight, proportionalFirstSize); + + auto secondSize = sizeMinusSeparator - firstSize; + const auto secondMinSize = _secondChild->_GetMinSize(); + const auto secondMinDimension = widthOrHeight ? secondMinSize.Width : secondMinSize.Height; + secondSize = std::max(secondSize, secondMinDimension); + + firstSize = _firstChild->SnapDimension(widthOrHeight, sizeMinusSeparator - secondSize); + return { firstSize, secondSize }; } +// 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::_CampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) +{ + 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 - PaneSeparatorSize); + const auto maxSplitPosition = 1.0f - (secondMinDimension / (totalSize - PaneSeparatorSize)); + + return std::clamp(requestedValue, minSplitPosition, maxSplitPosition); +} + +// Method Description: +// - Adjusts given size dimension (width or height) so that all descendant terminals +// align with their character grids as close as possible. Also ensures it fits in +// minimal size of the pane. +// Arguments: +// - widthOrHeight: if true operates on width, otherwise on height +// - toLargerOrSmaller: if true snaps upward (to greater size), if false snap downward +// - dimension: a dimension (width or height) to be snapped +// Return Value: +// - calculated dimension +float Pane::_SnapDimension(const bool widthOrHeight, const bool toLargerOrSmaller, const float dimension) +{ + if (_IsLeaf()) + { + const auto minSize = _GetMinSize(); + const auto minDimension = widthOrHeight ? minSize.Width : minSize.Height; + + if (dimension <= minDimension) + { + return minDimension; + } + + const float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension); + float result; + if (!toLargerOrSmaller || lower == dimension) + { + result = lower; + } + else + { + const auto cellSize = _control.CharacterDimensions(); + result = lower + (widthOrHeight ? cellSize.Width : cellSize.Height); + } + + return std::max(result, minDimension); + } + else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) + { + const auto firstSnapped = _firstChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, dimension); + const auto secondSnapped = _secondChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, dimension); + return toLargerOrSmaller ? + std::min(firstSnapped, secondSnapped) : + std::max(firstSnapped, secondSnapped); + } + else + { + const auto sizes = _GetPaneSizes(widthOrHeight, dimension); + auto firstSize = sizes.first; + + auto secondSize = _secondChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, sizes.second); + const auto secondMinSize = _secondChild->_GetMinSize(); + const auto secondMinDimension = widthOrHeight ? secondMinSize.Width : secondMinSize.Height; + secondSize = std::max(secondSize, secondMinDimension); + + return firstSize + PaneSeparatorSize + secondSize; + } +} + // 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. This includes the diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index b67a9d0f7ec..1dba1e2b065 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -51,9 +51,9 @@ class Pane : public std::enable_shared_from_this bool CanSplit(SplitState splitType); void Split(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); + float SnapDimension(const bool widthOrHeight, const float dimension); void Close(); - std::pair SnapDimension(bool widthOrHeight, int value); DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs); @@ -65,8 +65,7 @@ class Pane : public std::enable_shared_from_this std::shared_ptr _firstChild{ nullptr }; std::shared_ptr _secondChild{ nullptr }; SplitState _splitState{ SplitState::None }; - std::optional _firstPercent{ std::nullopt }; - std::optional _secondPercent{ std::nullopt }; + float _desiredSplitPosition; bool _lastFocused{ false }; std::optional _profile{ std::nullopt }; @@ -94,7 +93,9 @@ class Pane : public std::enable_shared_from_this void _FocusFirstChild(); void _ControlClosedHandler(); - std::pair _GetPaneSizes(const float& fullSize); + std::pair _GetPaneSizes(const bool widthOrHeight, float fullSize); + float _CampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize); + float _SnapDimension(const bool widthOrHeight, const bool toLargerOrSmaller, const float dimension); winrt::Windows::Foundation::Size _GetMinSize() const; diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index e5626dbcc7d..94f5ab3124d 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -230,9 +230,9 @@ void Tab::SplitPane(Pane::SplitState splitType, const GUID& profile, TermControl _rootPane->Split(splitType, profile, control); } -std::pair Tab::SnapDimension(bool widthOrHeight, int value) +float Tab::SnapDimension(bool widthOrHeight, float dimension) { - return _rootPane->SnapDimension(widthOrHeight, value); + return _rootPane->SnapDimension(widthOrHeight, dimension); } // Method Description: diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 7a8aa83db5f..9c4b4141fde 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -22,7 +22,7 @@ class Tab bool CanSplitPane(Pane::SplitState splitType); void SplitPane(Pane::SplitState splitType, const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control); - std::pair SnapDimension(bool widthOrHeight, int value); + float SnapDimension(bool widthOrHeight, float dimension); void UpdateFocus(); void UpdateIcon(const winrt::hstring iconPath); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 492a0f158b8..2f575b89f98 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1058,11 +1058,10 @@ namespace winrt::TerminalApp::implementation } } - int TerminalPage::SnapDimension(bool widthOrHeight, int value) + float TerminalPage::SnapDimension(bool widthOrHeight, float dimension) { const auto focusedTabIndex = _GetFocusedTabIndex(); - const auto closestValues = _tabs[focusedTabIndex]->SnapDimension(widthOrHeight, value); - return value - closestValues.first < closestValues.second - value ? closestValues.first : closestValues.second; + return _tabs[focusedTabIndex]->SnapDimension(widthOrHeight, dimension); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 26c6ce175a6..677d42d1e98 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -36,7 +36,7 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); - int SnapDimension(bool widthOrHeight, int value); + float SnapDimension(bool widthOrHeight, float dimension); void CloseWindow(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 6bbd584f422..77ac6d84bed 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1643,22 +1643,22 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } - int TermControl::SnapDimensionToGrid(bool widthOrHeight, int value) + float TermControl::SnapDimensionToGrid(bool widthOrHeight, float dimension) { const auto fontSize = _actualFont.GetSize(); - const int fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; + const auto fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; - int nonTerminalArea = gsl::narrow_cast(widthOrHeight ? + auto nonTerminalArea = gsl::narrow_cast(widthOrHeight ? _swapChainPanel.Margin().Left + _swapChainPanel.Margin().Right : _swapChainPanel.Margin().Top + _swapChainPanel.Margin().Bottom); if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible) { - nonTerminalArea += gsl::narrow_cast(_scrollBar.ActualWidth()); + nonTerminalArea += gsl::narrow_cast(_scrollBar.ActualWidth()); } - const int gridSize = value - nonTerminalArea; - const int cells = gridSize / fontDimension; + const auto gridSize = dimension - nonTerminalArea; + const int cells = static_cast(gridSize / fontDimension); return cells * fontDimension + nonTerminalArea; } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 9c71c1be778..04064fb7348 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -62,7 +62,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation bool ShouldCloseOnExit() const noexcept; Windows::Foundation::Size CharacterDimensions() const; Windows::Foundation::Size MinimumSize() const; - int SnapDimensionToGrid(bool widthOrHeight, int value); + float SnapDimensionToGrid(bool widthOrHeight, float dimension); void ScrollViewport(int viewTop); void KeyboardScrollViewport(int viewTop); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 92426b8b572..b35a5c0b857 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -31,7 +31,7 @@ namespace Microsoft.Terminal.TerminalControl event ConnectionClosedEventArgs ConnectionClosed; event Windows.Foundation.TypedEventHandler CopyToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; - UInt32 SnapDimensionToGrid(Boolean widthOrHeight, UInt32 value); + Single SnapDimensionToGrid(Boolean widthOrHeight, Single dimension); String Title { get; }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index dbb4121e723..f7ff3dca090 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -93,7 +93,7 @@ void IslandWindow::SetCreateCallback(std::function pfn) noexcept +void IslandWindow::SetSnapDimensionCallback(std::function pfn) noexcept { _pfnSnapDimensionCallback = pfn; } @@ -230,11 +230,11 @@ void IslandWindow::OnSize(const UINT width, const UINT height) if (wparam != WMSZ_TOP && wparam != WMSZ_BOTTOM) { - clientWidth = _pfnSnapDimensionCallback(true, clientWidth); + clientWidth = (int)_pfnSnapDimensionCallback(true, (float)clientWidth); } if (wparam != WMSZ_LEFT && wparam != WMSZ_RIGHT) { - clientHeight = _pfnSnapDimensionCallback(false, clientHeight); + clientHeight = (int)_pfnSnapDimensionCallback(false, (float)clientHeight); } switch (wparam) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index a503dc9145c..989014f406f 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -34,7 +34,7 @@ class IslandWindow : virtual void Initialize(); void SetCreateCallback(std::function pfn) noexcept; - void SetSnapDimensionCallback(std::function pfn) noexcept; + void SetSnapDimensionCallback(std::function pfn) noexcept; void UpdateTheme(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); @@ -89,7 +89,7 @@ class IslandWindow : winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; std::function _pfnCreateCallback; - std::function _pfnSnapDimensionCallback; + std::function _pfnSnapDimensionCallback; void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; }; From 90649b2bdf18eadef506c28cfec235227128ece3 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Sun, 29 Sep 2019 18:33:57 +0200 Subject: [PATCH 04/24] Partially fix child displacement --- src/cascadia/TerminalApp/Pane.cpp | 51 ++++++++++++++------ src/cascadia/TerminalApp/Pane.h | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 6 ++- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index f7e35578937..a0ce42de13e 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -109,7 +109,7 @@ bool Pane::_Resize(const Direction& direction) // resizing. const auto actualDimension = changeWidth ? actualSize.Width : actualSize.Height; - _desiredSplitPosition = _CampSplitPosition(changeWidth, _desiredSplitPosition - amount, actualDimension); + _desiredSplitPosition = _ClampSplitPosition(changeWidth, _desiredSplitPosition - amount, actualDimension); // Resize our columns to match the new percentages. ResizeContent(actualSize); @@ -291,7 +291,6 @@ void Pane::_ControlClosedHandler() } } - // Method Description: // - Adjusts given size dimension (width or height) so that all descendant terminals // align with their character grids as close as possible. Snaps to closes match @@ -304,6 +303,11 @@ void Pane::_ControlClosedHandler() float Pane::SnapDimension(const bool widthOrHeight, const float dimension) { const auto lower = _SnapDimension(widthOrHeight, false, dimension); + if (lower > dimension) + { + return lower; + } + const auto higher = _SnapDimension(widthOrHeight, true, dimension); return dimension - lower < higher - dimension ? lower : higher; } @@ -937,14 +941,18 @@ std::pair Pane::_GetPaneSizes(const bool widthOrHeight, float full const auto proportionalFirstSize = sizeMinusSeparator * _desiredSplitPosition; auto firstSize = _firstChild->SnapDimension(widthOrHeight, proportionalFirstSize); - auto secondSize = sizeMinusSeparator - firstSize; + const auto secondMinSize = _secondChild->_GetMinSize(); const auto secondMinDimension = widthOrHeight ? secondMinSize.Width : secondMinSize.Height; - secondSize = std::max(secondSize, secondMinDimension); - - firstSize = _firstChild->SnapDimension(widthOrHeight, sizeMinusSeparator - secondSize); + if (secondSize < secondMinDimension) + { + secondSize = secondMinDimension; + firstSize = _firstChild->_SnapDimension(widthOrHeight, false, sizeMinusSeparator - secondSize); + secondSize = sizeMinusSeparator - firstSize; + } + assert(firstSize + PaneSeparatorSize + secondSize == fullSize); return { firstSize, secondSize }; } @@ -957,7 +965,7 @@ std::pair Pane::_GetPaneSizes(const bool widthOrHeight, float full // - totalSize: size (width or height) of the parent pane // Return Value: // - split position (value in range <0.0, 1.0>) -float Pane::_CampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) +float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) { const auto firstMinSize = _firstChild->_GetMinSize(); const auto secondMinSize = _secondChild->_GetMinSize(); @@ -1017,15 +1025,26 @@ float Pane::_SnapDimension(const bool widthOrHeight, const bool toLargerOrSmalle } else { - const auto sizes = _GetPaneSizes(widthOrHeight, dimension); - auto firstSize = sizes.first; - - auto secondSize = _secondChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, sizes.second); - const auto secondMinSize = _secondChild->_GetMinSize(); - const auto secondMinDimension = widthOrHeight ? secondMinSize.Width : secondMinSize.Height; - secondSize = std::max(secondSize, secondMinDimension); - - return firstSize + PaneSeparatorSize + secondSize; + for (float resultSize = dimension;;) + { + const auto sizes = _GetPaneSizes(widthOrHeight, resultSize); + const auto firstSize = sizes.first; + const auto secondSize = _secondChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, sizes.second); + const float newSize = firstSize + PaneSeparatorSize + secondSize; + + const auto minDim = widthOrHeight ? _GetMinSize().Width : _GetMinSize().Height; + if (toLargerOrSmaller) + assert(newSize >= resultSize); + else + assert(newSize <= resultSize || newSize == minDim); + + if (newSize == resultSize) + { + return resultSize; + } + + resultSize = newSize; + } } } diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 1dba1e2b065..78102e1bb14 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -94,7 +94,7 @@ class Pane : public std::enable_shared_from_this void _ControlClosedHandler(); std::pair _GetPaneSizes(const bool widthOrHeight, float fullSize); - float _CampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize); + float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize); float _SnapDimension(const bool widthOrHeight, const bool toLargerOrSmaller, const float dimension); winrt::Windows::Foundation::Size _GetMinSize() const; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 77ac6d84bed..696087c5f2e 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1636,9 +1636,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation } // Account for the size of any padding - auto thickness = _ParseThicknessFromPadding(_settings.Padding()); + /*auto thickness = _ParseThicknessFromPadding(_settings.Padding()); width += thickness.Left + thickness.Right; - height += thickness.Top + thickness.Bottom; + height += thickness.Top + thickness.Bottom;*/ + width += _swapChainPanel.Margin().Left + _swapChainPanel.Margin().Right; + height += _swapChainPanel.Margin().Top + _swapChainPanel.Margin().Bottom; return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } From cbda9118dab9ca1e31ed8ede7d17f3a4dde23104 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Sat, 5 Oct 2019 17:18:40 +0200 Subject: [PATCH 05/24] Change child layout algorithm, now 100% correct --- src/cascadia/TerminalApp/Pane.cpp | 160 ++++++++++++++++++++---------- src/cascadia/TerminalApp/Pane.h | 6 +- 2 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index a0ce42de13e..71cb005ebb5 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -58,7 +58,7 @@ void Pane::ResizeContent(const Size& newSize) if (_splitState == SplitState::Vertical) { - const auto paneSizes = _GetPaneSizes(true, width); + const auto paneSizes = _GetPaneSizes(width); const Size firstSize{ paneSizes.first, height }; const Size secondSize{ paneSizes.second, height }; @@ -67,7 +67,7 @@ void Pane::ResizeContent(const Size& newSize) } else if (_splitState == SplitState::Horizontal) { - const auto paneSizes = _GetPaneSizes(false, height); + const auto paneSizes = _GetPaneSizes(height); const Size firstSize{ width, paneSizes.first }; const Size secondSize{ width, paneSizes.second }; @@ -690,7 +690,7 @@ void Pane::_CreateRowColDefinitions(const Size& rootSize) auto separatorColDef = Controls::ColumnDefinition(); separatorColDef.Width(GridLengthHelper::Auto()); - const auto paneSizes = _GetPaneSizes(true, rootSize.Width); + const auto paneSizes = _GetPaneSizes(rootSize.Width); auto firstColDef = Controls::ColumnDefinition(); firstColDef.Width(GridLengthHelper::FromPixels(paneSizes.first)); @@ -710,7 +710,7 @@ void Pane::_CreateRowColDefinitions(const Size& rootSize) auto separatorRowDef = Controls::RowDefinition(); separatorRowDef.Height(GridLengthHelper::Auto()); - const auto paneSizes = _GetPaneSizes(false, rootSize.Height); + const auto paneSizes = _GetPaneSizes(rootSize.Height); auto firstRowDef = Controls::RowDefinition(); firstRowDef.Height(GridLengthHelper::FromPixels(paneSizes.first)); @@ -920,63 +920,72 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& // of minimum size. Snaps first child to grid but not the second. Accounts for // the size of the separator that should be between them as well. // 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 pair with the size of our first child and the size of our second child, // respectively. -std::pair Pane::_GetPaneSizes(const bool widthOrHeight, float fullSize) +std::pair Pane::_GetPaneSizes(const float fullSize) { if (_IsLeaf()) { THROW_HR(E_FAIL); } - const auto minSize = _GetMinSize(); - const auto minDimension = widthOrHeight ? minSize.Width : minSize.Height; - fullSize = std::max(fullSize, minDimension); - - const auto sizeMinusSeparator = fullSize - PaneSeparatorSize; - - const auto proportionalFirstSize = sizeMinusSeparator * _desiredSplitPosition; - auto firstSize = _firstChild->SnapDimension(widthOrHeight, proportionalFirstSize); - auto secondSize = sizeMinusSeparator - firstSize; - - const auto secondMinSize = _secondChild->_GetMinSize(); - const auto secondMinDimension = widthOrHeight ? secondMinSize.Width : secondMinSize.Height; - if (secondSize < secondMinDimension) - { - secondSize = secondMinDimension; - firstSize = _firstChild->_SnapDimension(widthOrHeight, false, sizeMinusSeparator - secondSize); - secondSize = sizeMinusSeparator - firstSize; - } - - assert(firstSize + PaneSeparatorSize + secondSize == fullSize); - return { firstSize, secondSize }; + const auto widthOrHeight = _splitState == SplitState::Vertical; + const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize); + return { snappedSizes.first, fullSize - snappedSizes.first }; } // Method Description: -// - Adjusts split position so that no child pane is smaller then its -// minimum size +// - Gets the size in pixels of each of our children, given the full size they +// should fill. Sizes are snapped to grid, so their sum might (and usually is) +// lower than the specified full size. If called multiple times with fullSize +// argument growing, then both returned sizes are guaranteed to be non-decreasing. // 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 +// - fullSize: the amount of space in pixels that should be filled by our +// children and their separator. Can be arbitrarily low. // Return Value: -// - split position (value in range <0.0, 1.0>) -float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) +// - a pair with the size of our first child and the size of our second child, +// respectively. +std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) { - const auto firstMinSize = _firstChild->_GetMinSize(); - const auto secondMinSize = _secondChild->_GetMinSize(); + const auto sizeMinusSeparator = fullSize - PaneSeparatorSize; - const auto firstMinDimension = widthOrHeight ? firstMinSize.Width : firstMinSize.Height; - const auto secondMinDimension = widthOrHeight ? secondMinSize.Width : secondMinSize.Height; + auto firstSize = widthOrHeight ? _firstChild->_GetMinSize().Width : _firstChild->_GetMinSize().Height; + auto secondSize = widthOrHeight ? _secondChild->_GetMinSize().Width : _secondChild->_GetMinSize().Height; + if (firstSize + secondSize < sizeMinusSeparator) + { + auto nextFirstSize = _firstChild->_SnapDimension(widthOrHeight, true, firstSize + 1); + auto nextSecondSize = _secondChild->_SnapDimension(widthOrHeight, true, secondSize + 1); - const auto minSplitPosition = firstMinDimension / (totalSize - PaneSeparatorSize); - const auto maxSplitPosition = 1.0f - (secondMinDimension / (totalSize - PaneSeparatorSize)); + while (firstSize + secondSize < sizeMinusSeparator) + { + if (_WhichChildToExtend(firstSize, secondSize, nextFirstSize, nextSecondSize)) + { + if (nextFirstSize + secondSize > sizeMinusSeparator) + { + break; + } - return std::clamp(requestedValue, minSplitPosition, maxSplitPosition); + firstSize = nextFirstSize; + nextFirstSize = _firstChild->_SnapDimension(widthOrHeight, true, firstSize + 1); + } + else + { + if (firstSize + nextSecondSize > sizeMinusSeparator) + { + break; + } + + secondSize = nextSecondSize; + nextSecondSize = _secondChild->_SnapDimension(widthOrHeight, true, secondSize + 1); + } + } + } + + return { firstSize, secondSize }; } // Method Description: @@ -1025,29 +1034,47 @@ float Pane::_SnapDimension(const bool widthOrHeight, const bool toLargerOrSmalle } else { - for (float resultSize = dimension;;) + const auto sizes = _CalcSnappedPaneDimensions(widthOrHeight, dimension); + + if (toLargerOrSmaller || sizes.first + PaneSeparatorSize + sizes.second == dimension) { - const auto sizes = _GetPaneSizes(widthOrHeight, resultSize); - const auto firstSize = sizes.first; - const auto secondSize = _secondChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, sizes.second); - const float newSize = firstSize + PaneSeparatorSize + secondSize; - - const auto minDim = widthOrHeight ? _GetMinSize().Width : _GetMinSize().Height; - if (toLargerOrSmaller) - assert(newSize >= resultSize); - else - assert(newSize <= resultSize || newSize == minDim); + return sizes.first + PaneSeparatorSize + sizes.second; + } + else + { + const auto nextFirstSize = _firstChild->_SnapDimension(widthOrHeight, true, sizes.first + 1); + const auto nextSecondSize = _secondChild->_SnapDimension(widthOrHeight, true, sizes.second + 1); - if (newSize == resultSize) + if (_WhichChildToExtend(sizes.first, sizes.second, nextFirstSize, nextSecondSize)) { - return resultSize; + return nextFirstSize + PaneSeparatorSize + sizes.second; + } + else + { + return sizes.first + PaneSeparatorSize + nextSecondSize; } - - resultSize = newSize; } } } +// Method Description: +// - Given how child panes can grow, decides which one should do so, so that +// their size ratio is closer to the currently requested one (in the field +// _desiredSplitPosition). Used in snapping. +// Arguments: +// - firstSize: original size of first child +// - secondSize: original size of second child +// - nextFirstSize: size of first child that it would grow to +// - nextSecondSize: size of second child that it would grow to +// Return Value: +// - true if the first child should grow, false if the second child should grow +bool Pane::_WhichChildToExtend(const float firstSize, const float secondSize, const float nextFirstSize, const float nextSecondSize) +{ + const auto deviation1 = nextFirstSize - (nextFirstSize + secondSize) * _desiredSplitPosition; + const auto deviation2 = -1 * (firstSize - (firstSize + nextSecondSize) * _desiredSplitPosition); + return deviation1 <= deviation2; +} + // 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. This includes the @@ -1071,4 +1098,27 @@ Size Pane::_GetMinSize() const return { newWidth, newHeight }; } +// 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 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 - PaneSeparatorSize); + const auto maxSplitPosition = 1.0f - (secondMinDimension / (totalSize - PaneSeparatorSize)); + + return std::clamp(requestedValue, minSplitPosition, maxSplitPosition); +} + DEFINE_EVENT(Pane, Closed, _closedHandlers, ConnectionClosedEventArgs); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 78102e1bb14..532e89f70f9 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -93,11 +93,13 @@ class Pane : public std::enable_shared_from_this void _FocusFirstChild(); void _ControlClosedHandler(); - std::pair _GetPaneSizes(const bool widthOrHeight, float fullSize); - float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize); + std::pair _GetPaneSizes(const float fullSize); + std::pair _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize); float _SnapDimension(const bool widthOrHeight, const bool toLargerOrSmaller, const float dimension); + bool _WhichChildToExtend(const float firstSize, const float secondSize, const float nextFirstSize, const float nextSecondSize); winrt::Windows::Foundation::Size _GetMinSize() const; + float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize); // Function Description: // - Returns true if the given direction can be used with the given split From d69f8ed40310a658e3ca281f723d0eaed625c712 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Sat, 5 Oct 2019 19:13:58 +0200 Subject: [PATCH 06/24] Cache padding and scrollbar width, fix infinite loop --- src/cascadia/TerminalApp/Pane.cpp | 4 +- src/cascadia/TerminalControl/TermControl.cpp | 45 ++++++++++---------- src/cascadia/TerminalControl/TermControl.h | 4 ++ 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 71cb005ebb5..ca04be178cf 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -934,7 +934,7 @@ std::pair Pane::_GetPaneSizes(const float fullSize) const auto widthOrHeight = _splitState == SplitState::Vertical; const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize); - return { snappedSizes.first, fullSize - snappedSizes.first }; + return { snappedSizes.first, fullSize - PaneSeparatorSize - snappedSizes.first }; } // Method Description: @@ -1036,7 +1036,7 @@ float Pane::_SnapDimension(const bool widthOrHeight, const bool toLargerOrSmalle { const auto sizes = _CalcSnappedPaneDimensions(widthOrHeight, dimension); - if (toLargerOrSmaller || sizes.first + PaneSeparatorSize + sizes.second == dimension) + if (!toLargerOrSmaller || sizes.first + PaneSeparatorSize + sizes.second == dimension) { return sizes.first + PaneSeparatorSize + sizes.second; } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 696087c5f2e..d4d154136b5 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -53,6 +53,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _swapChainPanel{ nullptr }, _settings{ settings }, _closing{ false }, + _padding{ 0 }, + _scrollBarWidth{ std::nullopt }, _lastScrollOffset{ std::nullopt }, _autoScrollVelocity{ 0 }, _autoScrollingPointerPoint{ std::nullopt }, @@ -207,20 +209,20 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _BackgroundColorChanged(bg); // Apply padding as swapChainPanel's margin - auto newMargin = _ParseThicknessFromPadding(_settings.Padding()); - auto existingMargin = _swapChainPanel.Margin(); - _swapChainPanel.Margin(newMargin); + _padding = _ParseThicknessFromPadding(_settings.Padding()); + auto existingPadding = _padding; + _swapChainPanel.Margin(_padding); - if (newMargin != existingMargin && newMargin != Thickness{ 0 }) + if (_padding != existingPadding && _padding != Thickness{ 0 }) { TraceLoggingWrite(g_hTerminalControlProvider, "NonzeroPaddingApplied", TraceLoggingDescription("An event emitted when a control has padding applied to it"), TraceLoggingStruct(4, "Padding"), - TraceLoggingFloat64(newMargin.Left, "Left"), - TraceLoggingFloat64(newMargin.Top, "Top"), - TraceLoggingFloat64(newMargin.Right, "Right"), - TraceLoggingFloat64(newMargin.Bottom, "Bottom"), + TraceLoggingFloat64(_padding.Left, "Left"), + TraceLoggingFloat64(_padding.Top, "Top"), + TraceLoggingFloat64(_padding.Right, "Right"), + TraceLoggingFloat64(_padding.Bottom, "Bottom"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } @@ -392,7 +394,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const Windows::UI::Xaml::Thickness TermControl::GetPadding() const { - return _swapChainPanel.Margin(); + return _padding; } void TermControl::SwapChainChanged() @@ -527,6 +529,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); } + _scrollBarWidth = gsl::narrow_cast(_scrollBar.ActualWidth()); + _root.PointerWheelChanged({ this, &TermControl::_MouseWheelHandler }); // These need to be hooked up to the SwapChainPanel because we don't want the scrollbar to respond to pointer events (GitHub #950) @@ -800,8 +804,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto cursorPosition = point.Position(); _SetEndSelectionPointAtCursor(cursorPosition); - const double cursorBelowBottomDist = cursorPosition.Y - _swapChainPanel.Margin().Top - _swapChainPanel.ActualHeight(); - const double cursorAboveTopDist = -1 * cursorPosition.Y + _swapChainPanel.Margin().Top; + const double cursorBelowBottomDist = cursorPosition.Y - _padding.Top - _swapChainPanel.ActualHeight(); + const double cursorAboveTopDist = -1 * cursorPosition.Y + _padding.Top; constexpr double MinAutoScrollDist = 2.0; // Arbitrary value double newAutoScrollVelocity = 0.0; @@ -1632,15 +1636,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Reserve additional space if scrollbar is intended to be visible if (_settings.ScrollState() == ScrollbarState::Visible) { - width += _scrollBar.ActualWidth(); + width += _scrollBarWidth.value_or(0); } // Account for the size of any padding - /*auto thickness = _ParseThicknessFromPadding(_settings.Padding()); - width += thickness.Left + thickness.Right; - height += thickness.Top + thickness.Bottom;*/ - width += _swapChainPanel.Margin().Left + _swapChainPanel.Margin().Right; - height += _swapChainPanel.Margin().Top + _swapChainPanel.Margin().Bottom; + width += _padding.Left + _padding.Right; + height += _padding.Top + _padding.Bottom; return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } @@ -1651,12 +1652,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; auto nonTerminalArea = gsl::narrow_cast(widthOrHeight ? - _swapChainPanel.Margin().Left + _swapChainPanel.Margin().Right : - _swapChainPanel.Margin().Top + _swapChainPanel.Margin().Bottom); + _padding.Left + _padding.Right : + _padding.Top + _padding.Bottom); if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible) { - nonTerminalArea += gsl::narrow_cast(_scrollBar.ActualWidth()); + nonTerminalArea += _scrollBarWidth.value_or(0); } const auto gridSize = dimension - nonTerminalArea; @@ -1792,8 +1793,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation { // Exclude padding from cursor position calculation COORD terminalPosition = { - static_cast(cursorPosition.X - _swapChainPanel.Margin().Left), - static_cast(cursorPosition.Y - _swapChainPanel.Margin().Top) + static_cast(cursorPosition.X - _padding.Left), + static_cast(cursorPosition.Y - _padding.Top) }; const auto fontSize = _actualFont.GetSize(); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 04064fb7348..b38ade6d396 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -112,6 +112,10 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation FontInfoDesired _desiredFont; FontInfo _actualFont; + Windows::UI::Xaml::Thickness _padding; + + // Cached since _scrollBar.ActualWidth() became bottle-neck when resizing + std::optional _scrollBarWidth; std::optional _lastScrollOffset; From b732e745b5afa9510a647948633375eb40797289 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Thu, 10 Oct 2019 20:09:54 +0200 Subject: [PATCH 07/24] (drastically) optimize layout algorithm ( O(n^m) -> O(n) ) --- src/cascadia/TerminalApp/Pane.cpp | 280 ++++++++++++++++++++---------- src/cascadia/TerminalApp/Pane.h | 25 ++- 2 files changed, 210 insertions(+), 95 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index ca04be178cf..fe7c53040e1 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -302,13 +302,9 @@ void Pane::_ControlClosedHandler() // - calculated dimension float Pane::SnapDimension(const bool widthOrHeight, const float dimension) { - const auto lower = _SnapDimension(widthOrHeight, false, dimension); - if (lower > dimension) - { - return lower; - } - - const auto higher = _SnapDimension(widthOrHeight, true, dimension); + const auto snapPossibilites = _SnapDimension(widthOrHeight, dimension); + const auto lower = snapPossibilites.first; + const auto higher = snapPossibilites.second; return dimension - lower < higher - dimension ? lower : higher; } @@ -927,65 +923,65 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& // respectively. std::pair Pane::_GetPaneSizes(const float fullSize) { - if (_IsLeaf()) - { - THROW_HR(E_FAIL); - } - const auto widthOrHeight = _splitState == SplitState::Vertical; - const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize); + const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize, nullptr); return { snappedSizes.first, fullSize - PaneSeparatorSize - snappedSizes.first }; } // Method Description: -// - Gets the size in pixels of each of our children, given the full size they -// should fill. Sizes are snapped to grid, so their sum might (and usually is) -// lower than the specified full size. If called multiple times with fullSize +// - Gets the size in pixels of each of our children, given the full size they should +// fill. Each is snapped to char grid. If called multiple times with fullSize // argument growing, then both returned sizes are guaranteed to be non-decreasing. // 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. +// - fullSize: the amount of space in pixels that should be filled by our children and +// their separator. Can be arbitrarily low. +// - next: if not null, it will be assigned the next possible snapped sizes (see +// 'Return value' below), unless the children fit fullSize without any remaining space, +// in which case it is equal to returned value. // Return Value: // - a pair with the size of our first child and the size of our second child, -// respectively. -std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) +// respectively. Since they are snapped to grid, their sum might be (and usually is) +// lower than the specified full size. +std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next) { - const auto sizeMinusSeparator = fullSize - PaneSeparatorSize; + if (_IsLeaf()) + { + THROW_HR(E_FAIL); + } + + auto sizeTree = _GetMinSizeTree(widthOrHeight); + std::optional lastSizeTree{ sizeTree }; - auto firstSize = widthOrHeight ? _firstChild->_GetMinSize().Width : _firstChild->_GetMinSize().Height; - auto secondSize = widthOrHeight ? _secondChild->_GetMinSize().Width : _secondChild->_GetMinSize().Height; - if (firstSize + secondSize < sizeMinusSeparator) + while (sizeTree.size < fullSize) { - auto nextFirstSize = _firstChild->_SnapDimension(widthOrHeight, true, firstSize + 1); - auto nextSecondSize = _secondChild->_SnapDimension(widthOrHeight, true, secondSize + 1); + lastSizeTree = sizeTree; + _AdvanceSnappedDimension(widthOrHeight, sizeTree); - while (firstSize + secondSize < sizeMinusSeparator) + if (sizeTree.size == fullSize) { - if (_WhichChildToExtend(firstSize, secondSize, nextFirstSize, nextSecondSize)) + if (next) { - if (nextFirstSize + secondSize > sizeMinusSeparator) - { - break; - } - - firstSize = nextFirstSize; - nextFirstSize = _firstChild->_SnapDimension(widthOrHeight, true, firstSize + 1); + *next = { sizeTree.firstChild->size, sizeTree.secondChild->size }; } - else - { - if (firstSize + nextSecondSize > sizeMinusSeparator) - { - break; - } - secondSize = nextSecondSize; - nextSecondSize = _secondChild->_SnapDimension(widthOrHeight, true, secondSize + 1); - } + return { sizeTree.firstChild->size, sizeTree.secondChild->size }; } } - return { firstSize, secondSize }; + if (next) + { + *next = { sizeTree.firstChild->size, sizeTree.secondChild->size }; + } + + if (lastSizeTree.has_value()) + { + return { lastSizeTree.value().firstChild->size, lastSizeTree.value().secondChild->size }; + } + else + { + return { sizeTree.firstChild->size, sizeTree.secondChild->size }; + } } // Method Description: @@ -994,11 +990,12 @@ std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeigh // minimal size of the pane. // Arguments: // - widthOrHeight: if true operates on width, otherwise on height -// - toLargerOrSmaller: if true snaps upward (to greater size), if false snap downward // - dimension: a dimension (width or height) to be snapped // Return Value: -// - calculated dimension -float Pane::_SnapDimension(const bool widthOrHeight, const bool toLargerOrSmaller, const float dimension) +// - 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. +std::pair Pane::_SnapDimension(const bool widthOrHeight, const float dimension) { if (_IsLeaf()) { @@ -1007,72 +1004,106 @@ float Pane::_SnapDimension(const bool widthOrHeight, const bool toLargerOrSmalle if (dimension <= minDimension) { - return minDimension; + return { minDimension, minDimension }; } const float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension); - float result; - if (!toLargerOrSmaller || lower == dimension) - { - result = lower; - } - else + float higher = lower; + if (lower != dimension) { const auto cellSize = _control.CharacterDimensions(); - result = lower + (widthOrHeight ? cellSize.Width : cellSize.Height); + higher = lower + (widthOrHeight ? cellSize.Width : cellSize.Height); } - return std::max(result, minDimension); + return { lower, higher }; } else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) { - const auto firstSnapped = _firstChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, dimension); - const auto secondSnapped = _secondChild->_SnapDimension(widthOrHeight, toLargerOrSmaller, dimension); - return toLargerOrSmaller ? - std::min(firstSnapped, secondSnapped) : - std::max(firstSnapped, secondSnapped); + const auto firstSnapped = _firstChild->_SnapDimension(widthOrHeight, dimension); + const auto secondSnapped = _secondChild->_SnapDimension(widthOrHeight, dimension); + return { + std::max(firstSnapped.first, secondSnapped.first), + std::min(firstSnapped.second, secondSnapped.second) + }; } else { - const auto sizes = _CalcSnappedPaneDimensions(widthOrHeight, dimension); + std::pair higher; + const auto lower = _CalcSnappedPaneDimensions(widthOrHeight, dimension, &higher); + return { + lower.first + PaneSeparatorSize + lower.second, + higher.first + PaneSeparatorSize + higher.second + }; + } +} - if (!toLargerOrSmaller || sizes.first + PaneSeparatorSize + sizes.second == dimension) +void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) +{ + if (_IsLeaf()) + { + if (sizeNode.isMinimumSize) { - return sizes.first + PaneSeparatorSize + sizes.second; + sizeNode.size = _SnapDimension(widthOrHeight, sizeNode.size + 1).second; } else { - const auto nextFirstSize = _firstChild->_SnapDimension(widthOrHeight, true, sizes.first + 1); - const auto nextSecondSize = _secondChild->_SnapDimension(widthOrHeight, true, sizes.second + 1); + const auto cellSize = _control.CharacterDimensions(); + sizeNode.size += widthOrHeight ? cellSize.Width : cellSize.Height; + } + } + else + { + if (sizeNode.nextFirstChild == nullptr) + { + sizeNode.nextFirstChild.reset(new LayoutSizeNode(*sizeNode.firstChild)); + _firstChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextFirstChild); + } - if (_WhichChildToExtend(sizes.first, sizes.second, nextFirstSize, nextSecondSize)) - { - return nextFirstSize + PaneSeparatorSize + sizes.second; - } - else - { - return sizes.first + PaneSeparatorSize + nextSecondSize; - } + if (sizeNode.nextSecondChild == nullptr) + { + sizeNode.nextSecondChild.reset(new LayoutSizeNode(*sizeNode.secondChild)); + _secondChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextSecondChild); + } + + const auto nextFirstSize = sizeNode.nextFirstChild->size; + const auto nextSecondSize = sizeNode.nextSecondChild->size; + + bool advanceFirst; + if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) + { + advanceFirst = nextFirstSize < nextSecondSize; + } + else + { + const auto firstSize = sizeNode.firstChild->size; + const auto secondSize = sizeNode.secondChild->size; + const auto deviation1 = nextFirstSize - (nextFirstSize + secondSize) * _desiredSplitPosition; + const auto deviation2 = -1 * (firstSize - (firstSize + nextSecondSize) * _desiredSplitPosition); + advanceFirst = deviation1 <= deviation2; + } + + if (advanceFirst) + { + *sizeNode.firstChild = *sizeNode.nextFirstChild; + _firstChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextFirstChild); + } + else + { + *sizeNode.secondChild = *sizeNode.nextSecondChild; + _secondChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextSecondChild); + } + + if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) + { + sizeNode.size = std::max(sizeNode.firstChild->size, sizeNode.secondChild->size); + } + else + { + sizeNode.size = sizeNode.firstChild->size + PaneSeparatorSize + sizeNode.secondChild->size; } } -} -// Method Description: -// - Given how child panes can grow, decides which one should do so, so that -// their size ratio is closer to the currently requested one (in the field -// _desiredSplitPosition). Used in snapping. -// Arguments: -// - firstSize: original size of first child -// - secondSize: original size of second child -// - nextFirstSize: size of first child that it would grow to -// - nextSecondSize: size of second child that it would grow to -// Return Value: -// - true if the first child should grow, false if the second child should grow -bool Pane::_WhichChildToExtend(const float firstSize, const float secondSize, const float nextFirstSize, const float nextSecondSize) -{ - const auto deviation1 = nextFirstSize - (nextFirstSize + secondSize) * _desiredSplitPosition; - const auto deviation2 = -1 * (firstSize - (firstSize + nextSecondSize) * _desiredSplitPosition); - return deviation1 <= deviation2; + sizeNode.isMinimumSize = false; } // Method Description: @@ -1098,6 +1129,19 @@ Size Pane::_GetMinSize() const return { newWidth, newHeight }; } +Pane::LayoutSizeNode Pane::_GetMinSizeTree(const bool widthOrHeight) const +{ + const auto size = _GetMinSize(); + LayoutSizeNode node(widthOrHeight ? size.Width : size.Height); + if (!_IsLeaf()) + { + node.firstChild.reset(new LayoutSizeNode(_firstChild->_GetMinSizeTree(widthOrHeight))); + node.secondChild.reset(new LayoutSizeNode(_secondChild->_GetMinSizeTree(widthOrHeight))); + } + + return node; +} + // Method Description: // - Adjusts split position so that no child pane is smaller then its // minimum size @@ -1121,4 +1165,56 @@ float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedV return std::clamp(requestedValue, minSplitPosition, maxSplitPosition); } +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 } +{ +} + +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; +} + +void Pane::LayoutSizeNode::_AssignChildNode(std::unique_ptr& nodeField, const LayoutSizeNode* const newNode) +{ + if (newNode) + { + if (nodeField) + { + *nodeField = *newNode; + } + else + { + nodeField.reset(new LayoutSizeNode(*newNode)); + } + } + else + { + nodeField.release(); + } +} + DEFINE_EVENT(Pane, Closed, _closedHandlers, ConnectionClosedEventArgs); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 532e89f70f9..87a09713d62 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -58,6 +58,8 @@ class Pane : public std::enable_shared_from_this DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs); private: + struct LayoutSizeNode; + winrt::Windows::UI::Xaml::Controls::Grid _root{}; winrt::Windows::UI::Xaml::Controls::Grid _separatorRoot{ nullptr }; winrt::Microsoft::Terminal::TerminalControl::TermControl _control{ nullptr }; @@ -94,11 +96,12 @@ class Pane : public std::enable_shared_from_this void _ControlClosedHandler(); std::pair _GetPaneSizes(const float fullSize); - std::pair _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize); - float _SnapDimension(const bool widthOrHeight, const bool toLargerOrSmaller, const float dimension); - bool _WhichChildToExtend(const float firstSize, const float secondSize, const float nextFirstSize, const float nextSecondSize); + std::pair _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next); + std::pair _SnapDimension(const bool widthOrHeight, const float dimension); + void _AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode); winrt::Windows::Foundation::Size _GetMinSize() const; + LayoutSizeNode _GetMinSizeTree(const bool widthOrHeight) const; float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize); // Function Description: @@ -135,4 +138,20 @@ class Pane : public std::enable_shared_from_this return false; } + struct LayoutSizeNode + { + float size; + bool isMinimumSize; + std::unique_ptr firstChild; + std::unique_ptr secondChild; + 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); + }; }; From 0ecbec551c12cc8035545aedec37872912704f52 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Fri, 11 Oct 2019 18:56:43 +0200 Subject: [PATCH 08/24] Fix minimum pane size calculation --- src/cascadia/TerminalApp/Pane.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index fe7c53040e1..f6cb786eb11 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1124,9 +1124,15 @@ Size Pane::_GetMinSize() const const auto firstSize = _firstChild->_GetMinSize(); const auto secondSize = _secondChild->_GetMinSize(); - const auto newWidth = firstSize.Width + secondSize.Width + (_splitState == SplitState::Vertical ? PaneSeparatorSize : 0); - const auto newHeight = firstSize.Height + secondSize.Height + (_splitState == SplitState::Horizontal ? PaneSeparatorSize : 0); - return { newWidth, newHeight }; + + const auto minWidth = _splitState == SplitState::Vertical ? + firstSize.Width + PaneSeparatorSize + secondSize.Width : + std::max(firstSize.Width, secondSize.Width); + const auto minHeight = _splitState == SplitState::Horizontal ? + firstSize.Height + PaneSeparatorSize + secondSize.Height : + std::max(firstSize.Height, secondSize.Height); + + return { minWidth, minHeight }; } Pane::LayoutSizeNode Pane::_GetMinSizeTree(const bool widthOrHeight) const From 3dcd00f2920b4a45e807ea0e2a318578e3a7c4c1 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Sat, 12 Oct 2019 16:37:42 +0200 Subject: [PATCH 09/24] Style, comments, format; fix layout just after split --- src/cascadia/TerminalApp/App.cpp | 12 +- src/cascadia/TerminalApp/App.h | 2 +- src/cascadia/TerminalApp/App.idl | 2 +- src/cascadia/TerminalApp/Pane.cpp | 174 +++++++++++++----- src/cascadia/TerminalApp/Pane.h | 20 +- src/cascadia/TerminalApp/Tab.cpp | 6 +- src/cascadia/TerminalApp/Tab.h | 3 +- src/cascadia/TerminalApp/TerminalPage.cpp | 4 +- src/cascadia/TerminalApp/TerminalPage.h | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 42 ++++- src/cascadia/TerminalControl/TermControl.h | 9 +- src/cascadia/TerminalControl/TermControl.idl | 4 +- src/cascadia/WindowsTerminal/AppHost.cpp | 14 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 31 ++-- src/cascadia/WindowsTerminal/IslandWindow.h | 2 +- .../WindowsTerminal/NonClientIslandWindow.cpp | 16 +- .../WindowsTerminal/NonClientIslandWindow.h | 2 +- 17 files changed, 236 insertions(+), 109 deletions(-) diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index 693b5b98346..424a2066713 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -397,6 +397,13 @@ namespace winrt::TerminalApp::implementation return point; } + // Method Description: + // - See Pane::SnapDimension + float App::SnapDimension(const bool widthOrHeight, const float dimension) const + { + return _root->SnapDimension(widthOrHeight, dimension); + } + bool App::GetShowTabsInTitlebar() { if (!_loadedInitialSettings) @@ -653,11 +660,6 @@ namespace winrt::TerminalApp::implementation } } - int App::SnapDimension(bool widthOrHeight, int value) - { - return _root->SnapDimension(widthOrHeight, dimension); - } - // Methods that proxy typed event handlers through TerminalPage winrt::event_token App::SetTitleBarContent(Windows::Foundation::TypedEventHandler const& handler) { diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h index 5fd5cf3fbe4..069ef0e0d9b 100644 --- a/src/cascadia/TerminalApp/App.h +++ b/src/cascadia/TerminalApp/App.h @@ -33,13 +33,13 @@ namespace winrt::TerminalApp::implementation Windows::Foundation::Point GetLaunchDimensions(uint32_t dpi); winrt::Windows::Foundation::Point GetLaunchInitialPositions(int32_t defaultInitialX, int32_t defaultInitialY); LaunchMode GetLaunchMode(); + float SnapDimension(const bool widthOrHeight, const float dimension) const; bool GetShowTabsInTitlebar(); Windows::UI::Xaml::UIElement GetRoot() noexcept; hstring Title(); void TitlebarClicked(); - float SnapDimension(bool widthOrHeight, float dimension); void WindowCloseButtonClicked(); diff --git a/src/cascadia/TerminalApp/App.idl b/src/cascadia/TerminalApp/App.idl index 62df43dd535..53f183e014a 100644 --- a/src/cascadia/TerminalApp/App.idl +++ b/src/cascadia/TerminalApp/App.idl @@ -32,10 +32,10 @@ namespace TerminalApp Windows.Foundation.Point GetLaunchDimensions(UInt32 dpi); Windows.Foundation.Point GetLaunchInitialPositions(Int32 defaultInitialX, Int32 defaultInitialY); LaunchMode GetLaunchMode(); + Single SnapDimension(Boolean widthOrHeight, Single dimension); Boolean GetShowTabsInTitlebar(); void TitlebarClicked(); void WindowCloseButtonClicked(); - Single SnapDimension(Boolean widthOrHeight, Single dimension); event Windows.Foundation.TypedEventHandler SetTitleBarContent; event Windows.Foundation.TypedEventHandler TitleChanged; diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index f6cb786eb11..d0252b5c444 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -14,13 +14,16 @@ using namespace winrt::TerminalApp; static const int PaneSeparatorSize = 4; static const float Half = 0.50f; -Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocused) : +Pane::Pane(const GUID& profile, const TermControl& control, Pane* const rootPane, const bool lastFocused) : _control{ control }, _lastFocused{ lastFocused }, _profile{ profile } { + _rootPane = rootPane ? rootPane : this; + _root.Children().Append(_control); _connectionClosedToken = _control.ConnectionClosed({ this, &Pane::_ControlClosedHandler }); + _fonstSizeChangedToken = _control.FontSizeChanged({ this, &Pane::_FontSizeChangedHandler }); // Set the background of the pane to match that of the theme's default grid // background. This way, we'll match the small underline under the tabs, and @@ -291,21 +294,12 @@ void Pane::_ControlClosedHandler() } } -// Method Description: -// - Adjusts given size 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 ensures it fits in minimal size of the pane. -// Arguments: -// - widthOrHeight: if true operates on width, otherwise on height -// - dimension: a dimension (width or height) to snap -// Return Value: -// - calculated dimension -float Pane::SnapDimension(const bool widthOrHeight, const float dimension) +void Pane::_FontSizeChangedHandler(const int fontWidth, const int fontHeight, const bool isInitialChange) { - const auto snapPossibilites = _SnapDimension(widthOrHeight, dimension); - const auto lower = snapPossibilites.first; - const auto higher = snapPossibilites.second; - return dimension - lower < higher - dimension ? lower : higher; + if (isInitialChange) + { + _rootPane->ResizeContent(_rootPane->_root.ActualSize()); + } } // Method Description: @@ -316,6 +310,9 @@ float Pane::SnapDimension(const bool widthOrHeight, const float dimension) // - void Pane::Close() { + _control.FontSizeChanged(_fonstSizeChangedToken); + _fonstSizeChangedToken.value = 0; + // Fire our Closed event to tell our parent that we should be removed. _closedHandlers(); } @@ -881,6 +878,9 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& _control.ConnectionClosed(_connectionClosedToken); _connectionClosedToken.value = 0; + _control.FontSizeChanged(_fonstSizeChangedToken); + _fonstSizeChangedToken.value = 0; + _splitState = splitType; _desiredSplitPosition = Half; @@ -891,10 +891,10 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& // Create two new Panes // Move our control, guid into the first one. // Move the new guid, control into the second. - _firstChild = std::make_shared(_profile.value(), _control); + _firstChild = std::make_shared(_profile.value(), _control, _rootPane); _profile = std::nullopt; _control = { nullptr }; - _secondChild = std::make_shared(profile, control); + _secondChild = std::make_shared(profile, control, _rootPane); _CreateSplitContent(); @@ -921,17 +921,24 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& // 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::_GetPaneSizes(const float fullSize) const { const auto widthOrHeight = _splitState == SplitState::Vertical; const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize, nullptr); - return { snappedSizes.first, fullSize - PaneSeparatorSize - snappedSizes.first }; + + // Keep the first pane snapped and give the second pane all remaining size + return { + snappedSizes.first, + fullSize - PaneSeparatorSize - snappedSizes.first + }; } // Method Description: // - Gets the size in pixels of each of our children, given the full size they should // fill. Each is snapped to char grid. If called multiple times with fullSize // argument growing, then both returned sizes are guaranteed to be non-decreasing. +// This is important so that user doesn't get any pane shrinked when they actually +// increase the window/parent pane size. That's 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 @@ -943,7 +950,7 @@ std::pair Pane::_GetPaneSizes(const float fullSize) // - a pair with the size of our first child and the size of our second child, // respectively. Since they are snapped to grid, their sum might be (and usually is) // lower than the specified full size. -std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next) +std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next) const { if (_IsLeaf()) { @@ -951,7 +958,7 @@ std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeigh } auto sizeTree = _GetMinSizeTree(widthOrHeight); - std::optional lastSizeTree{ sizeTree }; + LayoutSizeNode lastSizeTree{ sizeTree }; while (sizeTree.size < fullSize) { @@ -974,20 +981,30 @@ std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeigh *next = { sizeTree.firstChild->size, sizeTree.secondChild->size }; } - if (lastSizeTree.has_value()) - { - return { lastSizeTree.value().firstChild->size, lastSizeTree.value().secondChild->size }; - } - else - { - return { sizeTree.firstChild->size, sizeTree.secondChild->size }; - } + return { lastSizeTree.firstChild->size, lastSizeTree.secondChild->size }; } // Method Description: -// - Adjusts given size dimension (width or height) so that all descendant terminals -// align with their character grids as close as possible. Also ensures it fits in -// minimal size of the pane. +// - 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: +// - calculated dimension +float Pane::SnapDimension(const bool widthOrHeight, const float dimension) const +{ + const auto snapPossibilites = _SnapDimension(widthOrHeight, dimension); + const auto lower = snapPossibilites.first; + const auto higher = snapPossibilites.second; + 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 @@ -995,10 +1012,12 @@ std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeigh // - 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. -std::pair Pane::_SnapDimension(const bool widthOrHeight, const float dimension) +std::pair Pane::_SnapDimension(const bool widthOrHeight, const float dimension) const { if (_IsLeaf()) { + // If we're a leaf pane, alight to the grid of controlling terminal + const auto minSize = _GetMinSize(); const auto minDimension = widthOrHeight ? minSize.Width : minSize.Height; @@ -1008,17 +1027,22 @@ std::pair Pane::_SnapDimension(const bool widthOrHeight, const flo } const float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension); - float higher = lower; - if (lower != dimension) + if (lower == dimension) + { + return { lower, lower }; + } + else { const auto cellSize = _control.CharacterDimensions(); - higher = lower + (widthOrHeight ? cellSize.Width : cellSize.Height); + const auto higher = lower + (widthOrHeight ? cellSize.Width : cellSize.Height); + return { lower, higher }; } - - return { lower, higher }; } else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) { + // If we're resizes along separator axis, snap to the closes possibility + // given by our children panes. + const auto firstSnapped = _firstChild->_SnapDimension(widthOrHeight, dimension); const auto secondSnapped = _secondChild->_SnapDimension(widthOrHeight, dimension); return { @@ -1028,6 +1052,12 @@ std::pair Pane::_SnapDimension(const bool widthOrHeight, const flo } else { + // If we're resizes 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. + std::pair higher; const auto lower = _CalcSnappedPaneDimensions(widthOrHeight, dimension, &higher); return { @@ -1037,12 +1067,25 @@ std::pair Pane::_SnapDimension(const bool widthOrHeight, const flo } } -void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) +// 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()) { if (sizeNode.isMinimumSize) { + // If the node is of its minimum size, this size might not be snapped, + // so snap it upward. It might however be snapped, so add 1 to make + // sure it really increases (not really required but to avoid surprises). sizeNode.size = _SnapDimension(widthOrHeight, sizeNode.size + 1).second; } else @@ -1053,12 +1096,14 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si } else { + // The given node often has next possible (advanced) values already + // cached by the previous advance operation. If we're the first one, + // we need to calculate them now. if (sizeNode.nextFirstChild == nullptr) { sizeNode.nextFirstChild.reset(new LayoutSizeNode(*sizeNode.firstChild)); _firstChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextFirstChild); } - if (sizeNode.nextSecondChild == nullptr) { sizeNode.nextSecondChild.reset(new LayoutSizeNode(*sizeNode.secondChild)); @@ -1068,15 +1113,25 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si const auto nextFirstSize = sizeNode.nextFirstChild->size; const auto nextSecondSize = sizeNode.nextSecondChild->size; - bool advanceFirst; + bool advanceFirst; // Whether to advance first or second child 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. advanceFirst = nextFirstSize < nextSecondSize; } else { + // If we're growing perpendicularly to separator axis, choose + // the child so that their size ratio is closer to the currently + // maintained (so that the relative separator position is closer + // to the _desiredSplitPosition field). + const auto firstSize = sizeNode.firstChild->size; const auto secondSize = sizeNode.secondChild->size; + + // Because we relay on equality check these calculations have to be + // immune to floating point errors. const auto deviation1 = nextFirstSize - (nextFirstSize + secondSize) * _desiredSplitPosition; const auto deviation2 = -1 * (firstSize - (firstSize + nextSecondSize) * _desiredSplitPosition); advanceFirst = deviation1 <= deviation2; @@ -1126,15 +1181,22 @@ Size Pane::_GetMinSize() const const auto secondSize = _secondChild->_GetMinSize(); const auto minWidth = _splitState == SplitState::Vertical ? - firstSize.Width + PaneSeparatorSize + secondSize.Width : - std::max(firstSize.Width, secondSize.Width); + firstSize.Width + PaneSeparatorSize + secondSize.Width : + std::max(firstSize.Width, secondSize.Width); const auto minHeight = _splitState == SplitState::Horizontal ? - firstSize.Height + PaneSeparatorSize + secondSize.Height : - std::max(firstSize.Height, secondSize.Height); + firstSize.Height + PaneSeparatorSize + 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::_GetMinSizeTree(const bool widthOrHeight) const { const auto size = _GetMinSize(); @@ -1157,7 +1219,7 @@ Pane::LayoutSizeNode Pane::_GetMinSizeTree(const bool widthOrHeight) const // - 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) +float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) const { const auto firstMinSize = _firstChild->_GetMinSize(); const auto secondMinSize = _secondChild->_GetMinSize(); @@ -1171,6 +1233,8 @@ float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedV return std::clamp(requestedValue, minSplitPosition, maxSplitPosition); } +DEFINE_EVENT(Pane, Closed, _closedHandlers, ConnectionClosedEventArgs); + Pane::LayoutSizeNode::LayoutSizeNode(const float minSize) : size{ minSize }, isMinimumSize{ true }, @@ -1191,6 +1255,14 @@ Pane::LayoutSizeNode::LayoutSizeNode(const LayoutSizeNode& other) : { } +// 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; @@ -1204,6 +1276,14 @@ Pane::LayoutSizeNode& Pane::LayoutSizeNode::operator=(const LayoutSizeNode& othe 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) @@ -1222,5 +1302,3 @@ void Pane::LayoutSizeNode::_AssignChildNode(std::unique_ptr& nod nodeField.release(); } } - -DEFINE_EVENT(Pane, Closed, _closedHandlers, ConnectionClosedEventArgs); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 87a09713d62..f119cbff725 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -33,7 +33,7 @@ class Pane : public std::enable_shared_from_this Horizontal = 2 }; - Pane(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, const bool lastFocused = false); + Pane(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, Pane* const rootPane, const bool lastFocused = false); std::shared_ptr GetFocusedPane(); winrt::Microsoft::Terminal::TerminalControl::TermControl GetFocusedTerminalControl(); @@ -51,7 +51,7 @@ class Pane : public std::enable_shared_from_this bool CanSplit(SplitState splitType); void Split(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); - float SnapDimension(const bool widthOrHeight, const float dimension); + float SnapDimension(const bool widthOrHeight, const float dimension) const; void Close(); @@ -64,6 +64,7 @@ class Pane : public std::enable_shared_from_this winrt::Windows::UI::Xaml::Controls::Grid _separatorRoot{ nullptr }; winrt::Microsoft::Terminal::TerminalControl::TermControl _control{ nullptr }; + Pane* _rootPane; std::shared_ptr _firstChild{ nullptr }; std::shared_ptr _secondChild{ nullptr }; SplitState _splitState{ SplitState::None }; @@ -74,6 +75,7 @@ class Pane : public std::enable_shared_from_this winrt::event_token _connectionClosedToken{ 0 }; winrt::event_token _firstClosedToken{ 0 }; winrt::event_token _secondClosedToken{ 0 }; + winrt::event_token _fonstSizeChangedToken{ 0 }; std::shared_mutex _createCloseLock{}; @@ -94,15 +96,16 @@ class Pane : public std::enable_shared_from_this void _FocusFirstChild(); void _ControlClosedHandler(); + void _FontSizeChangedHandler(const int fontWidth, const int fontHeight, const bool isInitialChange); - std::pair _GetPaneSizes(const float fullSize); - std::pair _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next); - std::pair _SnapDimension(const bool widthOrHeight, const float dimension); - void _AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode); + std::pair _GetPaneSizes(const float fullSize) const; + std::pair _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next) const; + std::pair _SnapDimension(const bool widthOrHeight, const float dimension) const; + void _AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) const; winrt::Windows::Foundation::Size _GetMinSize() const; LayoutSizeNode _GetMinSizeTree(const bool widthOrHeight) const; - float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize); + float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) const; // Function Description: // - Returns true if the given direction can be used with the given split @@ -138,6 +141,8 @@ class Pane : public std::enable_shared_from_this return false; } + // Helper structure that builds a (roughly) binary tree corresponding + // to the pane tree. Used for layouting panes with snapped sizes. struct LayoutSizeNode { float size; @@ -151,6 +156,7 @@ class Pane : public std::enable_shared_from_this 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 94f5ab3124d..3ed657b9880 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -17,7 +17,7 @@ namespace winrt Tab::Tab(const GUID& profile, const TermControl& control) { - _rootPane = std::make_shared(profile, control, true); + _rootPane = std::make_shared(profile, control, nullptr, true); _rootPane->Closed([=]() { _closedHandlers(); @@ -230,7 +230,9 @@ void Tab::SplitPane(Pane::SplitState splitType, const GUID& profile, TermControl _rootPane->Split(splitType, profile, control); } -float Tab::SnapDimension(bool widthOrHeight, float dimension) +// Method Description: +// - See Pane::SnapDimension +float Tab::SnapDimension(const bool widthOrHeight, const float dimension) const { return _rootPane->SnapDimension(widthOrHeight, dimension); } diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 9c4b4141fde..ae5c32777f8 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -22,7 +22,8 @@ class Tab bool CanSplitPane(Pane::SplitState splitType); void SplitPane(Pane::SplitState splitType, const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control); - float SnapDimension(bool widthOrHeight, float dimension); + + float SnapDimension(const bool widthOrHeight, const float dimension) const; void UpdateFocus(); void UpdateIcon(const winrt::hstring iconPath); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 2f575b89f98..5ceeaaaffa9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1058,7 +1058,9 @@ namespace winrt::TerminalApp::implementation } } - float TerminalPage::SnapDimension(bool widthOrHeight, float dimension) + // Method Description: + // - See Pane::SnapDimension + float TerminalPage::SnapDimension(const bool widthOrHeight, const float dimension) const { const auto focusedTabIndex = _GetFocusedTabIndex(); return _tabs[focusedTabIndex]->SnapDimension(widthOrHeight, dimension); diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 677d42d1e98..ca2866dc3ec 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -36,7 +36,7 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); - float SnapDimension(bool widthOrHeight, float dimension); + float SnapDimension(const bool widthOrHeight, const float dimension) const; void CloseWindow(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index d4d154136b5..aa30e39c526 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -447,10 +447,17 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); + // Set up the renderer to be used to calculate the width of a glyph, + // should we be unable to figure out its width another way. + auto pfn = std::bind(&::Microsoft::Console::Render::Renderer::IsGlyphWideByFont, _renderer.get(), std::placeholders::_1); + SetGlyphWidthFallback(pfn); + + _scrollBarWidth = gsl::narrow_cast(_scrollBar.ActualWidth()); + // 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) }; @@ -529,8 +536,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); } - _scrollBarWidth = gsl::narrow_cast(_scrollBar.ActualWidth()); - _root.PointerWheelChanged({ this, &TermControl::_MouseWheelHandler }); // These need to be hooked up to the SwapChainPanel because we don't want the scrollbar to respond to pointer events (GitHub #950) @@ -1215,15 +1220,23 @@ 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(); + { + auto lock = _terminal->LockForWriting(); + + const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX()); - const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX()); + // 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); + } - // 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 fontSize = _actualFont.GetSize(); + _fontSizeChangedHandlers(fontSize.X, fontSize.Y, initialUpdate); } // Method Description: @@ -1646,7 +1659,15 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } - float TermControl::SnapDimensionToGrid(bool widthOrHeight, float dimension) + // 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; @@ -1852,6 +1873,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, ConnectionClosed, _connectionClosedHandlers, TerminalControl::ConnectionClosedEventArgs); DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index b38ade6d396..5a229fb066b 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -62,7 +62,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation bool ShouldCloseOnExit() const noexcept; Windows::Foundation::Size CharacterDimensions() const; Windows::Foundation::Size MinimumSize() const; - float SnapDimensionToGrid(bool widthOrHeight, float dimension); + float SnapDimensionToGrid(const bool widthOrHeight, const float dimension) const; void ScrollViewport(int viewTop); void KeyboardScrollViewport(int viewTop); @@ -84,6 +84,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(ConnectionClosed, _connectionClosedHandlers, TerminalControl::ConnectionClosedEventArgs); DECLARE_EVENT(ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); @@ -115,7 +116,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation Windows::UI::Xaml::Thickness _padding; // Cached since _scrollBar.ActualWidth() became bottle-neck when resizing - std::optional _scrollBarWidth; + std::optional _scrollBarWidth; std::optional _lastScrollOffset; @@ -155,8 +156,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void _ApplyUISettings(); void _InitializeBackgroundBrush(); void _BackgroundColorChanged(const uint32_t color); - bool _InitializeTerminal(); - void _UpdateFont(); + void _InitializeTerminal(); + void _UpdateFont(const bool initialUpdate = false); 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); diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index b35a5c0b857..296eae251b9 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 ConnectionClosedEventArgs(); delegate void ScrollPositionChangedEventArgs(Int32 viewTop, Int32 viewHeight, Int32 bufferLength); @@ -28,10 +29,10 @@ namespace Microsoft.Terminal.TerminalControl void UpdateSettings(Microsoft.Terminal.Settings.IControlSettings newSettings); event TitleChangedEventArgs TitleChanged; + event FontSizeChangedEventArgs FontSizeChanged; event ConnectionClosedEventArgs ConnectionClosed; event Windows.Foundation.TypedEventHandler CopyToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; - Single SnapDimensionToGrid(Boolean widthOrHeight, Single dimension); String Title { get; }; @@ -41,6 +42,7 @@ namespace Microsoft.Terminal.TerminalControl Boolean ShouldCloseOnExit { get; }; 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 7198db80b18..7710575c764 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -38,9 +38,9 @@ AppHost::AppHost() noexcept : _window->SetCreateCallback(pfn); _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::App::SnapDimension, - _app, - std::placeholders::_1, - std::placeholders::_2)); + _app, + std::placeholders::_1, + std::placeholders::_2)); _window->MakeWindow(); } @@ -70,7 +70,7 @@ void AppHost::Initialize() if (_useNonClientArea) { - // Register our callbar for when the app's non-clientRect content changes. + // Register our callbar for when the app's non-client content changes. // This has to be done _before_ App::Create, as the app might set the // content in Create. _app.SetTitleBarContent({ this, &AppHost::_UpdateTitleBarContent }); @@ -198,10 +198,10 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter const short _currentHeight = Utils::ClampToShortMax( static_cast(ceil(initialSize.Y)), 1); - // Create a RECT from our requested clientRect size + // Create a RECT from our requested client size const auto clientRect = Viewport::FromDimensions({ _currentWidth, - _currentHeight }) - .ToRect(); + _currentHeight }) + .ToRect(); // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index f7ff3dca090..4be8e214a94 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -214,7 +214,7 @@ void IslandWindow::OnSize(const UINT width, const UINT height) { LPRECT winRect = (LPRECT)lparam; - // Find nearest montitor. + // Find nearest monitor. HMONITOR hmon = MonitorFromRect(winRect, MONITOR_DEFAULTTONEAREST); // This API guarantees that dpix and dpiy will be equal, but neither is an @@ -224,17 +224,16 @@ void IslandWindow::OnSize(const UINT width, const UINT height) // If this fails, we'll use the default of 96. GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy); - const auto client2win = GetClientToWinSizeDelta(dpix); - int clientWidth = winRect->right - winRect->left - client2win.cx; - int clientHeight = winRect->bottom - winRect->top - client2win.cy; - + const auto nonClientSize = GetNonClientSize(dpix); + auto clientWidth = winRect->right - winRect->left - nonClientSize.cx; + auto clientHeight = winRect->bottom - winRect->top - nonClientSize.cy; if (wparam != WMSZ_TOP && wparam != WMSZ_BOTTOM) { - clientWidth = (int)_pfnSnapDimensionCallback(true, (float)clientWidth); + clientWidth = static_cast(_pfnSnapDimensionCallback(true, static_cast(clientWidth))); } if (wparam != WMSZ_LEFT && wparam != WMSZ_RIGHT) { - clientHeight = (int)_pfnSnapDimensionCallback(false, (float)clientHeight); + clientHeight = static_cast(_pfnSnapDimensionCallback(false, static_cast(clientHeight))); } switch (wparam) @@ -242,12 +241,12 @@ void IslandWindow::OnSize(const UINT width, const UINT height) case WMSZ_LEFT: case WMSZ_TOPLEFT: case WMSZ_BOTTOMLEFT: - winRect->left = winRect->right - (clientWidth + client2win.cx); + winRect->left = winRect->right - (clientWidth + nonClientSize.cx); break; case WMSZ_RIGHT: case WMSZ_TOPRIGHT: case WMSZ_BOTTOMRIGHT: - winRect->right = winRect->left + (clientWidth + client2win.cx); + winRect->right = winRect->left + (clientWidth + nonClientSize.cx); break; } @@ -256,16 +255,16 @@ void IslandWindow::OnSize(const UINT width, const UINT height) case WMSZ_BOTTOM: case WMSZ_BOTTOMLEFT: case WMSZ_BOTTOMRIGHT: - winRect->bottom = winRect->top + (clientHeight + client2win.cy); + winRect->bottom = winRect->top + (clientHeight + nonClientSize.cy); break; case WMSZ_TOP: case WMSZ_TOPLEFT: case WMSZ_TOPRIGHT: - winRect->top = winRect->bottom - (clientHeight + client2win.cy); + winRect->top = winRect->bottom - (clientHeight + nonClientSize.cy); break; } - return true; //ew || eh; + return TRUE; } case WM_CLOSE: { @@ -347,7 +346,13 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) _rootGrid.Children().Append(content); } -SIZE IslandWindow::GetClientToWinSizeDelta(UINT dpix) const noexcept +// Method Description: +// - Gets the difference between window and client area size. +// Arguments: +// - dpix: dpi of a monitor on which the window is placed +// Return Value +// - The size difference +SIZE IslandWindow::GetNonClientSize(const UINT dpix) const noexcept { RECT rect{}; bool succeeded = AdjustWindowRectExForDpi(&rect, WS_OVERLAPPEDWINDOW, false, 0, dpix); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 989014f406f..7c865b1f095 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -29,7 +29,7 @@ class IslandWindow : void OnRestore() override; virtual void OnAppInitialized(); virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); - virtual SIZE GetClientToWinSizeDelta(UINT dpix) const noexcept; + virtual SIZE GetNonClientSize(const UINT dpix) const noexcept; virtual void Initialize(); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index ad87d5cd700..58587e0286a 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -340,16 +340,22 @@ MARGINS NonClientIslandWindow::GetFrameMargins() const noexcept return margins; } -SIZE NonClientIslandWindow::GetClientToWinSizeDelta(UINT /* dpix */) const noexcept +// Method Description: +// - Gets the difference between window and client area size. +// Arguments: +// - dpix: dpi of a monitor on which the window is placed +// Return Value +// - The size difference +SIZE NonClientIslandWindow::GetNonClientSize(UINT /* dpix */) const noexcept { - SIZE offset; + SIZE result; // If we're in NC tabs mode, do the math ourselves. Get the margins // we're using for the window - this will include the size of the // titlebar content. const MARGINS margins = GetFrameMargins(); - offset.cx = margins.cxLeftWidth + margins.cxRightWidth; - offset.cy = margins.cyTopHeight + margins.cyBottomHeight; - return offset; + result.cx = margins.cxLeftWidth + margins.cxRightWidth; + result.cy = margins.cyTopHeight + margins.cyBottomHeight; + return result; } // Method Description: diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 7f6c4ea0389..af783b8cabd 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -34,7 +34,7 @@ class NonClientIslandWindow : public IslandWindow [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; MARGINS GetFrameMargins() const noexcept; - virtual SIZE GetClientToWinSizeDelta(UINT dpix) const noexcept override; + virtual SIZE GetNonClientSize(UINT dpix) const noexcept override; void Initialize() override; From 583541d976b43546892a3aa912dd3654a2007350 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Wed, 16 Oct 2019 13:52:36 +0200 Subject: [PATCH 10/24] Review changes --- src/cascadia/TerminalApp/Pane.cpp | 24 ++++++++++++++----- src/cascadia/TerminalApp/Pane.h | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 4 ++-- src/cascadia/WindowsTerminal/IslandWindow.cpp | 15 ++++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index d0252b5c444..b1f61df864d 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -23,7 +23,7 @@ Pane::Pane(const GUID& profile, const TermControl& control, Pane* const rootPane _root.Children().Append(_control); _connectionClosedToken = _control.ConnectionClosed({ this, &Pane::_ControlClosedHandler }); - _fonstSizeChangedToken = _control.FontSizeChanged({ this, &Pane::_FontSizeChangedHandler }); + _fontSizeChangedToken = _control.FontSizeChanged({ this, &Pane::_FontSizeChangedHandler }); // Set the background of the pane to match that of the theme's default grid // background. This way, we'll match the small underline under the tabs, and @@ -294,7 +294,19 @@ void Pane::_ControlClosedHandler() } } -void Pane::_FontSizeChangedHandler(const int fontWidth, const int fontHeight, const bool isInitialChange) +// Method Description: +// - Called when our 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. +// Arguments: +// - fontWidth - new font width in pixels +// - fontHeight - new font height in pixels +// - isInitialChange - whether terminal just got its proper font size. +// Return Value: +// - +void Pane::_FontSizeChangedHandler(const int /* fontWidth */, const int /* fontHeight */, const bool isInitialChange) { if (isInitialChange) { @@ -310,8 +322,8 @@ void Pane::_FontSizeChangedHandler(const int fontWidth, const int fontHeight, co // - void Pane::Close() { - _control.FontSizeChanged(_fonstSizeChangedToken); - _fonstSizeChangedToken.value = 0; + _control.FontSizeChanged(_fontSizeChangedToken); + _fontSizeChangedToken.value = 0; // Fire our Closed event to tell our parent that we should be removed. _closedHandlers(); @@ -878,8 +890,8 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& _control.ConnectionClosed(_connectionClosedToken); _connectionClosedToken.value = 0; - _control.FontSizeChanged(_fonstSizeChangedToken); - _fonstSizeChangedToken.value = 0; + _control.FontSizeChanged(_fontSizeChangedToken); + _fontSizeChangedToken.value = 0; _splitState = splitType; _desiredSplitPosition = Half; diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index f119cbff725..003a6695823 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -75,7 +75,7 @@ class Pane : public std::enable_shared_from_this winrt::event_token _connectionClosedToken{ 0 }; winrt::event_token _firstClosedToken{ 0 }; winrt::event_token _secondClosedToken{ 0 }; - winrt::event_token _fonstSizeChangedToken{ 0 }; + winrt::event_token _fontSizeChangedToken{ 0 }; std::shared_mutex _createCloseLock{}; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index aa30e39c526..73d945ed5e0 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -208,9 +208,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation uint32_t bg = _settings.DefaultBackground(); _BackgroundColorChanged(bg); - // Apply padding as swapChainPanel's margin + const auto existingPadding = _padding; _padding = _ParseThicknessFromPadding(_settings.Padding()); - auto existingPadding = _padding; + // Apply padding as swapChainPanel's margin _swapChainPanel.Margin(_padding); if (_padding != existingPadding && _padding != Thickness{ 0 }) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 4be8e214a94..6127fb61a8c 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -93,6 +93,21 @@ void IslandWindow::SetCreateCallback(std::function void IslandWindow::SetSnapDimensionCallback(std::function pfn) noexcept { _pfnSnapDimensionCallback = pfn; From b00fef800eab46800e744a19e6282224ffbaa4f0 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Fri, 18 Oct 2019 19:43:42 +0200 Subject: [PATCH 11/24] Fix rebase --- src/cascadia/TerminalControl/TermControl.h | 2 +- src/cascadia/WindowsTerminal/AppHost.cpp | 120 ++++++++++---------- src/cascadia/WindowsTerminal/IslandWindow.h | 2 +- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 5a229fb066b..0fedb4162ff 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -156,7 +156,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void _ApplyUISettings(); void _InitializeBackgroundBrush(); void _BackgroundColorChanged(const uint32_t color); - void _InitializeTerminal(); + bool _InitializeTerminal(); void _UpdateFont(const bool initialUpdate = false); 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); diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 7710575c764..f2a16dd0ecf 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -149,66 +149,66 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter long adjustedHeight = 0; long adjustedWidth = 0; - if (launchMode == winrt::TerminalApp::LaunchMode::DefaultMode) - { - // Find nearest montitor. - HMONITOR hmon = MonitorFromRect(&proposedRect, MONITOR_DEFAULTTONEAREST); - - // Get nearest monitor information - MONITORINFO monitorInfo; - monitorInfo.cbSize = sizeof(MONITORINFO); - GetMonitorInfo(hmon, &monitorInfo); - - // 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); - - // We need to check if the top left point of the titlebar of the window is within any screen - RECT offScreenTestRect; - offScreenTestRect.left = proposedRect.left; - offScreenTestRect.top = proposedRect.top; - offScreenTestRect.right = offScreenTestRect.left + 1; - offScreenTestRect.bottom = offScreenTestRect.top + 1; - - bool isTitlebarIntersectWithMonitors = false; - EnumDisplayMonitors( - nullptr, &offScreenTestRect, [](HMONITOR, HDC, LPRECT, LPARAM lParam) -> BOOL { - auto intersectWithMonitor = reinterpret_cast(lParam); - *intersectWithMonitor = true; - // Continue the enumeration - return FALSE; - }, - reinterpret_cast(&isTitlebarIntersectWithMonitors)); - - if (!isTitlebarIntersectWithMonitors) - { - // If the title bar is out-of-screen, we set the initial position to - // the top left corner of the nearest monitor - proposedRect.left = monitorInfo.rcWork.left; - proposedRect.top = monitorInfo.rcWork.top; - } - - auto initialSize = _app.GetLaunchDimensions(dpix); - - const short _currentWidth = Utils::ClampToShortMax( - static_cast(ceil(initialSize.X)), 1); - const short _currentHeight = Utils::ClampToShortMax( - static_cast(ceil(initialSize.Y)), 1); - - // Create a RECT from our requested client size - const auto clientRect = Viewport::FromDimensions({ _currentWidth, - _currentHeight }) - .ToRect(); - - // Get the size of a window we'd need to host that client rect. This will - // add the titlebar space. - const auto client2win = _window->GetClientToWinSizeDelta(dpix); - adjustedHeight = clientRect.bottom - clientRect.top + client2win.cx; - adjustedWidth = clientRect.right - clientRect.left + client2win.cy; - } + if (launchMode == winrt::TerminalApp::LaunchMode::DefaultMode) + { + // Find nearest montitor. + HMONITOR hmon = MonitorFromRect(&proposedRect, MONITOR_DEFAULTTONEAREST); + + // Get nearest monitor information + MONITORINFO monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(hmon, &monitorInfo); + + // 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); + + // We need to check if the top left point of the titlebar of the window is within any screen + RECT offScreenTestRect; + offScreenTestRect.left = proposedRect.left; + offScreenTestRect.top = proposedRect.top; + offScreenTestRect.right = offScreenTestRect.left + 1; + offScreenTestRect.bottom = offScreenTestRect.top + 1; + + bool isTitlebarIntersectWithMonitors = false; + EnumDisplayMonitors( + nullptr, &offScreenTestRect, [](HMONITOR, HDC, LPRECT, LPARAM lParam) -> BOOL { + auto intersectWithMonitor = reinterpret_cast(lParam); + *intersectWithMonitor = true; + // Continue the enumeration + return FALSE; + }, + reinterpret_cast(&isTitlebarIntersectWithMonitors)); + + if (!isTitlebarIntersectWithMonitors) + { + // If the title bar is out-of-screen, we set the initial position to + // the top left corner of the nearest monitor + proposedRect.left = monitorInfo.rcWork.left; + proposedRect.top = monitorInfo.rcWork.top; + } + + auto initialSize = _app.GetLaunchDimensions(dpix); + + const short _currentWidth = Utils::ClampToShortMax( + static_cast(ceil(initialSize.X)), 1); + const short _currentHeight = Utils::ClampToShortMax( + static_cast(ceil(initialSize.Y)), 1); + + // Create a RECT from our requested client size + const auto clientRect = Viewport::FromDimensions({ _currentWidth, + _currentHeight }) + .ToRect(); + + // Get the size of a window we'd need to host that client rect. This will + // add the titlebar space. + const auto nonClientSize = _window->GetNonClientSize(dpix); + adjustedHeight = clientRect.bottom - clientRect.top + nonClientSize.cx; + adjustedWidth = clientRect.right - clientRect.left + nonClientSize.cy; + } const COORD origin{ gsl::narrow(proposedRect.left), gsl::narrow(proposedRect.top) }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 7c865b1f095..61a9085cf12 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -33,7 +33,7 @@ class IslandWindow : virtual void Initialize(); - void SetCreateCallback(std::function pfn) noexcept; + void SetCreateCallback(std::function pfn) noexcept; void SetSnapDimensionCallback(std::function pfn) noexcept; void UpdateTheme(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); From 5e9b31c120a4b751757ffa4f9e539ea83ab50dfa Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Tue, 5 Nov 2019 20:27:03 +0100 Subject: [PATCH 12/24] Undo caching of padding and scrollbar size --- src/cascadia/TerminalControl/TermControl.cpp | 44 ++++++++++---------- src/cascadia/TerminalControl/TermControl.h | 3 -- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 73d945ed5e0..3700afec60d 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -53,8 +53,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _swapChainPanel{ nullptr }, _settings{ settings }, _closing{ false }, - _padding{ 0 }, - _scrollBarWidth{ std::nullopt }, _lastScrollOffset{ std::nullopt }, _autoScrollVelocity{ 0 }, _autoScrollingPointerPoint{ std::nullopt }, @@ -208,21 +206,21 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation uint32_t bg = _settings.DefaultBackground(); _BackgroundColorChanged(bg); - const auto existingPadding = _padding; - _padding = _ParseThicknessFromPadding(_settings.Padding()); + auto newMargin = _ParseThicknessFromPadding(_settings.Padding()); + auto existingMargin = _swapChainPanel.Margin(); // Apply padding as swapChainPanel's margin - _swapChainPanel.Margin(_padding); + _swapChainPanel.Margin(newMargin); - if (_padding != existingPadding && _padding != Thickness{ 0 }) + if (newMargin != existingMargin && newMargin != Thickness{ 0 }) { TraceLoggingWrite(g_hTerminalControlProvider, "NonzeroPaddingApplied", TraceLoggingDescription("An event emitted when a control has padding applied to it"), TraceLoggingStruct(4, "Padding"), - TraceLoggingFloat64(_padding.Left, "Left"), - TraceLoggingFloat64(_padding.Top, "Top"), - TraceLoggingFloat64(_padding.Right, "Right"), - TraceLoggingFloat64(_padding.Bottom, "Bottom"), + TraceLoggingFloat64(newMargin.Left, "Left"), + TraceLoggingFloat64(newMargin.Top, "Top"), + TraceLoggingFloat64(newMargin.Right, "Right"), + TraceLoggingFloat64(newMargin.Bottom, "Bottom"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } @@ -394,7 +392,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const Windows::UI::Xaml::Thickness TermControl::GetPadding() const { - return _padding; + return _swapChainPanel.Margin(); } void TermControl::SwapChainChanged() @@ -452,8 +450,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation auto pfn = std::bind(&::Microsoft::Console::Render::Renderer::IsGlyphWideByFont, _renderer.get(), std::placeholders::_1); SetGlyphWidthFallback(pfn); - _scrollBarWidth = gsl::narrow_cast(_scrollBar.ActualWidth()); - // 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. @@ -809,8 +805,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto cursorPosition = point.Position(); _SetEndSelectionPointAtCursor(cursorPosition); - const double cursorBelowBottomDist = cursorPosition.Y - _padding.Top - _swapChainPanel.ActualHeight(); - const double cursorAboveTopDist = -1 * cursorPosition.Y + _padding.Top; + const double cursorBelowBottomDist = cursorPosition.Y - _swapChainPanel.Margin().Top - _swapChainPanel.ActualHeight(); + const double cursorAboveTopDist = -1 * cursorPosition.Y + _swapChainPanel.Margin().Top; constexpr double MinAutoScrollDist = 2.0; // Arbitrary value double newAutoScrollVelocity = 0.0; @@ -1649,12 +1645,13 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Reserve additional space if scrollbar is intended to be visible if (_settings.ScrollState() == ScrollbarState::Visible) { - width += _scrollBarWidth.value_or(0); + width += gsl::narrow_cast(_scrollBar.ActualWidth()); } // Account for the size of any padding - width += _padding.Left + _padding.Right; - height += _padding.Top + _padding.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) }; } @@ -1672,13 +1669,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation 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); + padding.Left + padding.Right : + padding.Top + padding.Bottom); if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible) { - nonTerminalArea += _scrollBarWidth.value_or(0); + nonTerminalArea += gsl::narrow_cast(_scrollBar.ActualWidth()); } const auto gridSize = dimension - nonTerminalArea; @@ -1814,8 +1812,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation { // Exclude padding from cursor position calculation COORD terminalPosition = { - static_cast(cursorPosition.X - _padding.Left), - static_cast(cursorPosition.Y - _padding.Top) + static_cast(cursorPosition.X - _swapChainPanel.Margin().Left), + static_cast(cursorPosition.Y - _swapChainPanel.Margin().Top) }; const auto fontSize = _actualFont.GetSize(); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 0fedb4162ff..1e257b78f71 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -115,9 +115,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation FontInfo _actualFont; Windows::UI::Xaml::Thickness _padding; - // Cached since _scrollBar.ActualWidth() became bottle-neck when resizing - std::optional _scrollBarWidth; - std::optional _lastScrollOffset; // Auto scroll occurs when user, while selecting, drags cursor outside viewport. View is then scrolled to 'follow' the cursor. From bed02c5840ec4fe7b0d3933ec2f9f2b83655da99 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Wed, 6 Nov 2019 19:41:20 +0100 Subject: [PATCH 13/24] Some review changes --- src/cascadia/TerminalApp/Pane.cpp | 140 +++++------------- src/cascadia/TerminalApp/Pane.h | 21 ++- src/cascadia/TerminalApp/Tab.cpp | 2 +- .../TerminalApp/lib/Pane.LayoutSizeNode.cpp | 73 +++++++++ .../TerminalApp/lib/TerminalAppLib.vcxproj | 57 ++----- src/cascadia/TerminalControl/TermControl.cpp | 4 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 2 +- src/cascadia/WindowsTerminal/IslandWindow.h | 2 +- 8 files changed, 146 insertions(+), 155 deletions(-) create mode 100644 src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index b1f61df864d..c2020fcdeb3 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -14,17 +14,22 @@ using namespace winrt::TerminalApp; static const int PaneSeparatorSize = 4; static const float Half = 0.50f; -Pane::Pane(const GUID& profile, const TermControl& control, Pane* const rootPane, const bool lastFocused) : +Pane::Pane(const GUID& profile, const TermControl& control, const bool isRoot, const bool lastFocused) : _control{ control }, _lastFocused{ lastFocused }, _profile{ profile } { - _rootPane = rootPane ? rootPane : this; - _root.Children().Append(_control); _connectionClosedToken = _control.ConnectionClosed({ this, &Pane::_ControlClosedHandler }); _fontSizeChangedToken = _control.FontSizeChanged({ this, &Pane::_FontSizeChangedHandler }); + if (isRoot) + { + ShouldRelayout([=] { + ResizeContent(_root.ActualSize()); + }); + } + // Set the background of the pane to match that of the theme's default grid // background. This way, we'll match the small underline under the tabs, and // the UI will be consistent on bot light and dark modes. @@ -310,7 +315,7 @@ void Pane::_FontSizeChangedHandler(const int /* fontWidth */, const int /* fontH { if (isInitialChange) { - _rootPane->ResizeContent(_rootPane->_root.ActualSize()); + _shouldRelayoutHandlers(); } } @@ -903,10 +908,17 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& // Create two new Panes // Move our control, guid into the first one. // Move the new guid, control into the second. - _firstChild = std::make_shared(_profile.value(), _control, _rootPane); + _firstChild = std::make_shared(_profile.value(), _control, false); _profile = std::nullopt; _control = { nullptr }; - _secondChild = std::make_shared(profile, control, _rootPane); + _secondChild = std::make_shared(profile, control, false); + + _firstChild->ShouldRelayout([=]() { + _shouldRelayoutHandlers(); + }); + _secondChild->ShouldRelayout([=]() { + _shouldRelayoutHandlers(); + }); _CreateSplitContent(); @@ -936,7 +948,7 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& std::pair Pane::_GetPaneSizes(const float fullSize) const { const auto widthOrHeight = _splitState == SplitState::Vertical; - const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize, nullptr); + const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize).lower; // Keep the first pane snapped and give the second pane all remaining size return { @@ -962,7 +974,7 @@ std::pair Pane::_GetPaneSizes(const float fullSize) const // - a pair with the size of our first child and the size of our second child, // respectively. Since they are snapped to grid, their sum might be (and usually is) // lower than the specified full size. -std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next) const +Pane::SnapChildrenSizeResult Pane::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) const { if (_IsLeaf()) { @@ -979,21 +991,13 @@ std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeigh if (sizeTree.size == fullSize) { - if (next) - { - *next = { sizeTree.firstChild->size, sizeTree.secondChild->size }; - } - - return { sizeTree.firstChild->size, sizeTree.secondChild->size }; + return { { sizeTree.firstChild->size, sizeTree.secondChild->size }, + { sizeTree.firstChild->size, sizeTree.secondChild->size } }; } } - if (next) - { - *next = { sizeTree.firstChild->size, sizeTree.secondChild->size }; - } - - return { lastSizeTree.firstChild->size, lastSizeTree.secondChild->size }; + return { { lastSizeTree.firstChild->size, lastSizeTree.secondChild->size }, + { sizeTree.firstChild->size, sizeTree.secondChild->size } }; } // Method Description: @@ -1004,12 +1008,12 @@ std::pair Pane::_CalcSnappedPaneDimensions(const bool widthOrHeigh // - widthOrHeight: if true operates on width, otherwise on height // - dimension: a dimension (width or height) to snap // Return Value: -// - calculated dimension +// - A value corresponding to the next closest snap size for this Pane, either upward or downward float Pane::SnapDimension(const bool widthOrHeight, const float dimension) const { const auto snapPossibilites = _SnapDimension(widthOrHeight, dimension); - const auto lower = snapPossibilites.first; - const auto higher = snapPossibilites.second; + const auto lower = snapPossibilites.lower; + const auto higher = snapPossibilites.higher; return dimension - lower < higher - dimension ? lower : higher; } @@ -1024,7 +1028,7 @@ float Pane::SnapDimension(const bool widthOrHeight, const float dimension) const // - 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. -std::pair Pane::_SnapDimension(const bool widthOrHeight, const float dimension) const +Pane::SnapSizeResult Pane::_SnapDimension(const bool widthOrHeight, const float dimension) const { if (_IsLeaf()) { @@ -1052,29 +1056,28 @@ std::pair Pane::_SnapDimension(const bool widthOrHeight, const flo } else if (_splitState == (widthOrHeight ? SplitState::Horizontal : SplitState::Vertical)) { - // If we're resizes along separator axis, snap to the closes possibility + // If we're resizing along separator axis, snap to the closest possibility // given by our children panes. const auto firstSnapped = _firstChild->_SnapDimension(widthOrHeight, dimension); const auto secondSnapped = _secondChild->_SnapDimension(widthOrHeight, dimension); return { - std::max(firstSnapped.first, secondSnapped.first), - std::min(firstSnapped.second, secondSnapped.second) + std::max(firstSnapped.lower, secondSnapped.lower), + std::min(firstSnapped.higher, secondSnapped.higher) }; } else { - // If we're resizes perpendicularly to separator axis, calculate the sizes + // 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. - std::pair higher; - const auto lower = _CalcSnappedPaneDimensions(widthOrHeight, dimension, &higher); + const auto childSizes = _CalcSnappedPaneDimensions(widthOrHeight, dimension); return { - lower.first + PaneSeparatorSize + lower.second, - higher.first + PaneSeparatorSize + higher.second + childSizes.lower.first + PaneSeparatorSize + childSizes.lower.second, + childSizes.higher.first + PaneSeparatorSize + childSizes.higher.second }; } } @@ -1098,7 +1101,7 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si // If the node is of its minimum size, this size might not be snapped, // so snap it upward. It might however be snapped, so add 1 to make // sure it really increases (not really required but to avoid surprises). - sizeNode.size = _SnapDimension(widthOrHeight, sizeNode.size + 1).second; + sizeNode.size = _SnapDimension(widthOrHeight, sizeNode.size + 1).higher; } else { @@ -1142,7 +1145,7 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si const auto firstSize = sizeNode.firstChild->size; const auto secondSize = sizeNode.secondChild->size; - // Because we relay on equality check these calculations have to be + // Because we rely on equality check, these calculations have to be // immune to floating point errors. const auto deviation1 = nextFirstSize - (nextFirstSize + secondSize) * _desiredSplitPosition; const auto deviation2 = -1 * (firstSize - (firstSize + nextSecondSize) * _desiredSplitPosition); @@ -1246,71 +1249,4 @@ float Pane::_ClampSplitPosition(const bool widthOrHeight, const float requestedV } DEFINE_EVENT(Pane, Closed, _closedHandlers, ConnectionClosedEventArgs); - -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.reset(new LayoutSizeNode(*newNode)); - } - } - else - { - nodeField.release(); - } -} +DEFINE_EVENT(Pane, ShouldRelayout, _shouldRelayoutHandlers, winrt::delegate<>); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 003a6695823..5bac2c7072f 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -33,7 +33,7 @@ class Pane : public std::enable_shared_from_this Horizontal = 2 }; - Pane(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, Pane* const rootPane, const bool lastFocused = false); + Pane(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, const bool isRoot, const bool lastFocused = false); std::shared_ptr GetFocusedPane(); winrt::Microsoft::Terminal::TerminalControl::TermControl GetFocusedTerminalControl(); @@ -56,8 +56,11 @@ class Pane : public std::enable_shared_from_this void Close(); DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs); + DECLARE_EVENT(ShouldRelayout, _shouldRelayoutHandlers, winrt::delegate<>); private: + struct SnapSizeResult; + struct SnapChildrenSizeResult; struct LayoutSizeNode; winrt::Windows::UI::Xaml::Controls::Grid _root{}; @@ -99,8 +102,8 @@ class Pane : public std::enable_shared_from_this void _FontSizeChangedHandler(const int fontWidth, const int fontHeight, const bool isInitialChange); std::pair _GetPaneSizes(const float fullSize) const; - std::pair _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize, std::pair* next) const; - std::pair _SnapDimension(const bool widthOrHeight, const float dimension) const; + SnapChildrenSizeResult _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) const; + SnapSizeResult _SnapDimension(const bool widthOrHeight, const float dimension) const; void _AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& sizeNode) const; winrt::Windows::Foundation::Size _GetMinSize() const; @@ -141,6 +144,18 @@ class Pane : public std::enable_shared_from_this return false; } + 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 diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 3ed657b9880..af7867816d8 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -17,7 +17,7 @@ namespace winrt Tab::Tab(const GUID& profile, const TermControl& control) { - _rootPane = std::make_shared(profile, control, nullptr, true); + _rootPane = std::make_shared(profile, control, true, true); _rootPane->Closed([=]() { _closedHandlers(); diff --git a/src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp b/src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp new file mode 100644 index 00000000000..650d8a101be --- /dev/null +++ b/src/cascadia/TerminalApp/lib/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.reset(new LayoutSizeNode(*newNode)); + } + } + else + { + nodeField.release(); + } +} diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index fdfba54b61e..7a3da895ffe 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -1,8 +1,6 @@ - - StaticLibrary @@ -16,7 +14,6 @@ false - @@ -75,17 +71,16 @@ - + ../ActionArgs.idl - + ../AppKeyBindings.idl - + ../App.xaml - @@ -118,35 +113,34 @@ + Create - + ../AppKeyBindings.idl - + ../ActionArgs.idl - + ../App.xaml - + ../App.xaml - NotUsing - - + ../App.xaml @@ -168,13 +162,11 @@ Code - - - - <_BinRoot Condition="'$(Platform)' != 'Win32'">$(OpenConsoleDir)$(Platform)\$(Configuration)\ <_BinRoot Condition="'$(Platform)' == 'Win32'">$(OpenConsoleDir)$(Configuration)\ - Warning - - $(_BinRoot)TerminalSettings\Microsoft.Terminal.Settings.winmd true false false - $(_BinRoot)TerminalConnection\Microsoft.Terminal.TerminalConnection.winmd true false false - $(_BinRoot)TerminalControl\Microsoft.Terminal.TerminalControl.winmd true false false - - @@ -243,7 +226,6 @@ WindowsApp.lib;shell32.lib;%(AdditionalDependencies) - {CA5CAD1A-9A12-429C-B551-8562EC954746} @@ -253,7 +235,6 @@ TerminalAppLib 10.0.17763.0 - true - - - Microsoft.Toolkit.Win32.UI.XamlHost.dll @@ -294,7 +272,6 @@ - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. @@ -302,8 +279,6 @@ - - - + - + - - + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 3700afec60d..d15a3ba93f8 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -206,9 +206,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation uint32_t bg = _settings.DefaultBackground(); _BackgroundColorChanged(bg); + // Apply padding as swapChainPanel's margin auto newMargin = _ParseThicknessFromPadding(_settings.Padding()); auto existingMargin = _swapChainPanel.Margin(); - // Apply padding as swapChainPanel's margin _swapChainPanel.Margin(newMargin); if (newMargin != existingMargin && newMargin != Thickness{ 0 }) @@ -1645,7 +1645,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Reserve additional space if scrollbar is intended to be visible if (_settings.ScrollState() == ScrollbarState::Visible) { - width += gsl::narrow_cast(_scrollBar.ActualWidth()); + width += _scrollBar.ActualWidth(); } // Account for the size of any padding diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 6127fb61a8c..1129daf8cca 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -108,7 +108,7 @@ void IslandWindow::SetCreateCallback(std::function -void IslandWindow::SetSnapDimensionCallback(std::function pfn) noexcept +void IslandWindow::SetSnapDimensionCallback(std::function pfn) noexcept { _pfnSnapDimensionCallback = pfn; } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 61a9085cf12..fea6ecc595f 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -89,7 +89,7 @@ class IslandWindow : winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; std::function _pfnCreateCallback; - std::function _pfnSnapDimensionCallback; + std::function _pfnSnapDimensionCallback; void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; }; From 9a95e18f71b378c356f10ea4a4da227edb30ee19 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Mon, 11 Nov 2019 18:27:30 +0100 Subject: [PATCH 14/24] Fix height calculation in non client window --- src/cascadia/WindowsTerminal/IslandWindow.cpp | 11 +++++++---- src/cascadia/WindowsTerminal/IslandWindow.h | 2 +- .../WindowsTerminal/NonClientIslandWindow.cpp | 13 +++++++++---- .../WindowsTerminal/NonClientIslandWindow.h | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 6b64d862e0f..4f56fae5c6c 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -355,13 +355,13 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) // Method Description: // - Gets the difference between window and client area size. // Arguments: -// - dpix: dpi of a monitor on which the window is placed +// - dpi: dpi of a monitor on which the window is placed // Return Value // - The size difference -SIZE IslandWindow::GetNonClientSize(const UINT dpix) const noexcept +SIZE IslandWindow::GetNonClientSize(const UINT dpi) const noexcept { RECT islandFrame{}; - bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpix); + bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpi); if (!succeeded) { // If we failed to get the correct window size for whatever reason, log @@ -370,7 +370,10 @@ SIZE IslandWindow::GetNonClientSize(const UINT dpix) const noexcept LOG_LAST_ERROR(); } - return { islandFrame.right - islandFrame.left, islandFrame.bottom - islandFrame.top }; + return { + islandFrame.right - islandFrame.left, + islandFrame.bottom - islandFrame.top + }; } void IslandWindow::OnAppInitialized() diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index bfc841a4c9c..f6ea4703baf 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -29,7 +29,7 @@ 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 GetNonClientSize(const UINT dpix) const noexcept; + virtual SIZE GetNonClientSize(const UINT dpi) const noexcept; virtual void Initialize(); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 78ec9356380..4a953e16307 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -408,13 +408,13 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept // Method Description: // - Gets the difference between window and client area size. // Arguments: -// - dpix: dpi of a monitor on which the window is placed +// - dpi: dpi of a monitor on which the window is placed // Return Value // - The size difference -SIZE NonClientIslandWindow::GetNonClientSize(UINT dpix) const noexcept +SIZE NonClientIslandWindow::GetNonClientSize(UINT dpi) const noexcept { RECT islandFrame{}; - bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpix); + bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpi); if (!succeeded) { // If we failed to get the correct window size for whatever reason, log @@ -425,7 +425,12 @@ SIZE NonClientIslandWindow::GetNonClientSize(UINT dpix) const noexcept islandFrame.top = -topBorderVisibleHeight; - return { islandFrame.right - islandFrame.left, islandFrame.bottom - islandFrame.top }; + const auto titleBarHeight = _titlebar ? static_cast(_titlebar.ActualHeight()) : 0; + + return { + islandFrame.right - islandFrame.left, + islandFrame.bottom - islandFrame.top + titleBarHeight + }; } // Method Description: diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 80bae6cd6c6..ba2d4636fe4 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -36,7 +36,7 @@ class NonClientIslandWindow : public IslandWindow [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; - virtual SIZE GetNonClientSize(UINT dpix) const noexcept override; + virtual SIZE GetNonClientSize(UINT dpi) const noexcept override; void Initialize() override; From 11fcd5130af4610232fd62e7d80dbbe35688515c Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Mon, 25 Nov 2019 19:45:25 +0100 Subject: [PATCH 15/24] Some additional comments I had time for --- src/cascadia/TerminalApp/Pane.cpp | 58 ++++++++++++++----- src/cascadia/TerminalApp/Pane.h | 3 + src/cascadia/WindowsTerminal/IslandWindow.cpp | 21 ++++++- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 288280f08cf..daa930e663a 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1111,7 +1111,7 @@ Pane::SnapSizeResult Pane::_SnapDimension(const bool widthOrHeight, const float { if (_IsLeaf()) { - // If we're a leaf pane, alight to the grid of controlling terminal + // 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; @@ -1124,6 +1124,8 @@ Pane::SnapSizeResult Pane::_SnapDimension(const bool widthOrHeight, const float const float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension); 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 @@ -1175,11 +1177,15 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si { 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, - // so snap it upward. It might however be snapped, so add 1 to make - // sure it really increases (not really required but to avoid surprises). + // 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 = _SnapDimension(widthOrHeight, sizeNode.size + 1).higher; } else @@ -1190,9 +1196,13 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si } else { - // The given node often has next possible (advanced) values already - // cached by the previous advance operation. If we're the first one, - // we need to calculate them now. + // 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. If not, we need to calculate them now. if (sizeNode.nextFirstChild == nullptr) { sizeNode.nextFirstChild.reset(new LayoutSizeNode(*sizeNode.firstChild)); @@ -1207,31 +1217,46 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si const auto nextFirstSize = sizeNode.nextFirstChild->size; const auto nextSecondSize = sizeNode.nextSecondChild->size; - bool advanceFirst; // Whether to advance first or second child + // 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. - advanceFirst = nextFirstSize < nextSecondSize; + // 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 - // the child so that their size ratio is closer to the currently - // maintained (so that the relative separator position is closer + // 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. + // 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); - advanceFirst = deviation1 <= deviation2; + advanceFirstOrSecond = deviation1 <= deviation2; } - if (advanceFirst) + // 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); @@ -1242,6 +1267,7 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si _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); @@ -1252,6 +1278,8 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si } } + // Because we have grown, we're certainly no longer of our + // minimal size (if we've ever been). sizeNode.isMinimumSize = false; } diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 1c053a4f6d5..226d51095bb 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -178,6 +178,9 @@ class Pane : public std::enable_shared_from_this bool isMinimumSize; std::unique_ptr firstChild; std::unique_ptr secondChild; + + // These two fields hold next possible snapped values of firstChild and + // secondChild. They are used as a cache to std::unique_ptr nextFirstChild; std::unique_ptr nextSecondChild; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 4f56fae5c6c..ecf4d09e040 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -227,7 +227,13 @@ void IslandWindow::OnSize(const UINT width, const UINT height) } case WM_SIZING: { - LPRECT winRect = (LPRECT)lparam; + // Here we intercept a window border (or corner) drag action and apply 'snapping' + // i.e. align the terminal's size to its cell grid. We're given the requested + // window rectangle (this is the one that originates form current drag action) in + // lparam, which we then adjust based on the terminal properties (like font size). + // Note that lparam also acts as the return value (it's a ref parameter). + + LPRECT winRect = reinterpret_cast(lparam); // Find nearest monitor. HMONITOR hmon = MonitorFromRect(winRect, MONITOR_DEFAULTTONEAREST); @@ -242,15 +248,27 @@ void IslandWindow::OnSize(const UINT width, const UINT height) const auto nonClientSize = GetNonClientSize(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 calculate + // a new, snapped width. clientWidth = static_cast(_pfnSnapDimensionCallback(true, static_cast(clientWidth))); } if (wparam != WMSZ_LEFT && wparam != WMSZ_RIGHT) { + // Analogous to above, but for height. If user drags a corner, then both width + // and height will be adjusted. clientHeight = static_cast(_pfnSnapDimensionCallback(false, static_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 + // 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. + + // Set width switch (wparam) { case WMSZ_LEFT: @@ -265,6 +283,7 @@ void IslandWindow::OnSize(const UINT width, const UINT height) break; } + // Set height switch (wparam) { case WMSZ_BOTTOM: From 8034bb4871dce4d96b30be2e7cf5eb73087ccb27 Mon Sep 17 00:00:00 2001 From: mcpiroman <38111589+mcpiroman@users.noreply.github.com> Date: Mon, 25 Nov 2019 20:00:56 +0100 Subject: [PATCH 16/24] Finish comment in pane.h --- src/cascadia/TerminalApp/Pane.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 226d51095bb..524c36da2ca 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -180,7 +180,9 @@ class Pane : public std::enable_shared_from_this std::unique_ptr secondChild; // These two fields hold next possible snapped values of firstChild and - // secondChild. They are used as a cache to + // 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. They might be null. std::unique_ptr nextFirstChild; std::unique_ptr nextSecondChild; From eb67b824168576af8c4cf5c18880e9f81921bef1 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Wed, 27 Nov 2019 19:16:23 +0100 Subject: [PATCH 17/24] Things that should've gone with merge --- src/cascadia/TerminalApp/AppLogic.cpp | 7 +++++++ src/cascadia/TerminalApp/AppLogic.h | 1 + src/cascadia/TerminalApp/AppLogic.idl | 1 + src/cascadia/TerminalControl/TermControl.cpp | 12 +++++------- src/cascadia/WindowsTerminal/AppHost.cpp | 4 ++-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 7e1233e53bb..7583a58f0c0 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -428,6 +428,13 @@ namespace winrt::TerminalApp::implementation return _settings->GlobalSettings().GetShowTabsInTitlebar(); } + // Method Description: + // - See Pane::SnapDimension + float AppLogic::SnapDimension(const bool widthOrHeight, const float dimension) const + { + return _root->SnapDimension(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 66e5027d6ed..8ae728a49f8 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -37,6 +37,7 @@ namespace winrt::TerminalApp::implementation winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); bool GetShowTabsInTitlebar(); + float SnapDimension(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 8be0b6eada1..874f4b67810 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -34,6 +34,7 @@ namespace TerminalApp Windows.UI.Xaml.ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); Boolean GetShowTabsInTitlebar(); + Single SnapDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); void WindowCloseButtonClicked(); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 5cde3a4ff98..b754034d623 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1292,14 +1292,16 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // concerned with initialization process. Value forwarded to event handler. void TermControl::_UpdateFont(const bool initialUpdate) { - { - auto lock = _terminal->LockForWriting(); + auto lock = _terminal->LockForWriting(); - const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX()); + const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX()); // 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: @@ -1316,13 +1318,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _actualFont = { fontFace, 0, 10, { 0, newSize }, CP_UTF8, false }; _desiredFont = { _actualFont }; - // Refresh our font with the renderer _UpdateFont(); - const actualNewSize = _actualFont.GetSize(); - _fontSizeChangedHandlers(actualNewSize.X, actualNewSize.Y, initialUpdate); - // Resize the terminal's BUFFER to match the new font size. This does // NOT change the size of the window, because that can lead to more // problems (like what happens when you change the font size while the diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 264acb8857b..cecfb0dbf2d 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -40,8 +40,8 @@ AppHost::AppHost() noexcept : std::placeholders::_3); _window->SetCreateCallback(pfn); - _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::App::SnapDimension, - _app, + _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::AppLogic::SnapDimension, + _logic, std::placeholders::_1, std::placeholders::_2)); From b4febc8cdd65c872be238b92b0986bcbf8598bcc Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Thu, 28 Nov 2019 17:46:42 +0100 Subject: [PATCH 18/24] Cleanup & format --- src/cascadia/TerminalApp/Pane.cpp | 52 +++++++++---------- src/cascadia/TerminalApp/Pane.h | 21 ++++---- src/cascadia/TerminalControl/TermControl.cpp | 6 --- src/cascadia/TerminalControl/TermControl.h | 1 - src/cascadia/WindowsTerminal/IslandWindow.cpp | 2 +- 5 files changed, 37 insertions(+), 45 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 51974f1ebcc..98bd55cf2fe 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -360,9 +360,9 @@ void Pane::_FontSizeChangedHandler(const int /* fontWidth */, const int /* fontH // Return Value: // - void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& /* sender */, - RoutedEventArgs const& /* args */) + RoutedEventArgs const& /* args */) { - _GotFocusHandlers(shared_from_this()); + _GotFocusHandlers(shared_from_this()); } // Method Description: @@ -1061,8 +1061,8 @@ 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. If specified size is lower than -// required then children will be of minimum size. Snaps first child to grid +// 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 @@ -1230,7 +1230,7 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si { // 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 + // however be already snapped, so add 1 to make sure it really increases // (not strictly necessary but to avoid surprises). sizeNode.size = _SnapDimension(widthOrHeight, sizeNode.size + 1).higher; } @@ -1242,7 +1242,7 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si } else { - // We're a parent pane, so we have to advance dimension of our children panes. In + // 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 @@ -1286,22 +1286,22 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si // 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 + // 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. + // 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 + // 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. + // function). The other child's next* size remains unchanged because its size + // haven't changed either. if (advanceFirstOrSecond) { *sizeNode.firstChild = *sizeNode.nextFirstChild; @@ -1353,20 +1353,20 @@ Size Pane::_GetMinSize() const 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 }; - } + 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: diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 7e7ab9d3fcf..da660ffe631 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -43,10 +43,10 @@ class Pane : public std::enable_shared_from_this Horizontal = 2 }; - Pane(const GUID& profile, - const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, - const bool isRoot, - const bool lastFocused = false); + Pane(const GUID& profile, + const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, + const bool isRoot, + const bool lastFocused = false); std::shared_ptr GetActivePane(); winrt::Microsoft::Terminal::TerminalControl::TermControl GetTerminalControl(); @@ -88,7 +88,6 @@ class Pane : public std::enable_shared_from_this static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush; static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_unfocusedBorderBrush; - Pane* _rootPane; std::shared_ptr _firstChild{ nullptr }; std::shared_ptr _secondChild{ nullptr }; SplitState _splitState{ SplitState::None }; @@ -129,8 +128,8 @@ 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 _FontSizeChangedHandler(const int fontWidth, const int fontHeight, const bool isInitialChange); - void _ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, - winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void _ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Windows::UI::Xaml::RoutedEventArgs const& e); std::pair _GetPaneSizes(const float fullSize) const; SnapChildrenSizeResult _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) const; @@ -175,7 +174,7 @@ class Pane : public std::enable_shared_from_this return false; } - static void _SetupResources(); + static void _SetupResources(); struct SnapSizeResult { @@ -198,9 +197,9 @@ class Pane : public std::enable_shared_from_this 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 + // 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. They might be null. std::unique_ptr nextFirstChild; std::unique_ptr nextSecondChild; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index b754034d623..c99f95d911d 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -479,11 +479,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); - // Set up the renderer to be used to calculate the width of a glyph, - // should we be unable to figure out its width another way. - auto pfn = std::bind(&::Microsoft::Console::Render::Renderer::IsGlyphWideByFont, _renderer.get(), std::placeholders::_1); - SetGlyphWidthFallback(pfn); - // 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. @@ -1320,7 +1315,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Refresh our font with the renderer _UpdateFont(); - // Resize the terminal's BUFFER to match the new font size. This does // NOT change the size of the window, because that can lead to more // problems (like what happens when you change the font size while the diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 43da2643e5a..85f95bd205a 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -122,7 +122,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation FontInfoDesired _desiredFont; FontInfo _actualFont; - Windows::UI::Xaml::Thickness _padding; std::optional _lastScrollOffset; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 1ea7fd12383..6151354005a 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -251,7 +251,7 @@ void IslandWindow::OnSize(const UINT width, const UINT height) if (wparam != WMSZ_TOP && wparam != WMSZ_BOTTOM) { - // If user has dragged anything but the top or bottom border (so e.g. left border, + // 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 calculate // a new, snapped width. clientWidth = static_cast(_pfnSnapDimensionCallback(true, static_cast(clientWidth))); From cae396398b8981d3237613be060a37ad7b7d438b Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Thu, 28 Nov 2019 20:26:06 +0100 Subject: [PATCH 19/24] More comments --- src/cascadia/TerminalApp/Pane.cpp | 42 ++++++++++++++----- src/cascadia/TerminalApp/Pane.h | 2 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 12 +++--- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 98bd55cf2fe..2fbd65eef4e 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1084,21 +1084,21 @@ std::pair Pane::_GetPaneSizes(const float fullSize) const // Method Description: // - Gets the size in pixels of each of our children, given the full size they should -// fill. Each is snapped to char grid. If called multiple times with fullSize -// argument growing, then both returned sizes are guaranteed to be non-decreasing. -// This is important so that user doesn't get any pane shrinked when they actually -// increase the window/parent pane size. That's also required by the layout algorithm. +// 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. -// - next: if not null, it will be assigned the next possible snapped sizes (see -// 'Return value' below), unless the children fit fullSize without any remaining space, -// in which case it is equal to returned value. // Return Value: -// - a pair with the size of our first child and the size of our second child, -// respectively. Since they are snapped to grid, their sum might be (and usually is) -// lower than the specified full size. +// - 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 +// complicity. 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::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) const { if (_IsLeaf()) @@ -1106,6 +1106,23 @@ Pane::SnapChildrenSizeResult Pane::_CalcSnappedPaneDimensions(const bool widthOr 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 sneek 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 = _GetMinSizeTree(widthOrHeight); LayoutSizeNode lastSizeTree{ sizeTree }; @@ -1116,11 +1133,16 @@ Pane::SnapChildrenSizeResult Pane::_CalcSnappedPaneDimensions(const bool widthOr 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 } }; } diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index da660ffe631..5d4f1e54483 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -200,7 +200,7 @@ class Pane : public std::enable_shared_from_this // 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. They might be null. + // simple increment. Hence we cache that here. std::unique_ptr nextFirstChild; std::unique_ptr nextSecondChild; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 6151354005a..c53c5185c35 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -252,21 +252,21 @@ void IslandWindow::OnSize(const UINT width, const UINT height) 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 calculate - // a new, snapped width. + // 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 = static_cast(_pfnSnapDimensionCallback(true, static_cast(clientWidth))); } if (wparam != WMSZ_LEFT && wparam != WMSZ_RIGHT) { - // Analogous to above, but for height. If user drags a corner, then both width - // and height will be adjusted. + // Analogous to above, but for height. clientHeight = static_cast(_pfnSnapDimensionCallback(false, static_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 - // 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. + // 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) From 63ed26acd89a8d9d698258418368f1b6a87a2c68 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Thu, 5 Dec 2019 19:02:15 +0100 Subject: [PATCH 20/24] Review refactor, partly fix border size calculations --- src/cascadia/TerminalApp/AppLogic.cpp | 6 +- src/cascadia/TerminalApp/AppLogic.h | 2 +- src/cascadia/TerminalApp/AppLogic.idl | 2 +- src/cascadia/TerminalApp/Pane.cpp | 129 ++++++++---------- src/cascadia/TerminalApp/Pane.h | 15 +- src/cascadia/TerminalApp/Tab.cpp | 22 ++- src/cascadia/TerminalApp/Tab.h | 2 +- src/cascadia/TerminalApp/TerminalPage.cpp | 6 +- src/cascadia/TerminalApp/TerminalPage.h | 2 +- src/cascadia/WindowsTerminal/AppHost.cpp | 4 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 4 +- src/cascadia/WindowsTerminal/IslandWindow.h | 2 +- .../WindowsTerminal/NonClientIslandWindow.cpp | 2 +- .../WindowsTerminal/NonClientIslandWindow.h | 2 +- 14 files changed, 96 insertions(+), 104 deletions(-) diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 7583a58f0c0..78ed3421e24 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -429,10 +429,10 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - See Pane::SnapDimension - float AppLogic::SnapDimension(const bool widthOrHeight, const float dimension) const + // - See Pane::CalcSnappedDimension + float AppLogic::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { - return _root->SnapDimension(widthOrHeight, dimension); + return _root->CalcSnappedDimension(widthOrHeight, dimension); } // Method Description: diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 8ae728a49f8..8a795627387 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -37,7 +37,7 @@ namespace winrt::TerminalApp::implementation winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); bool GetShowTabsInTitlebar(); - float SnapDimension(const bool widthOrHeight, const float dimension) const; + 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 874f4b67810..0a60d6cc54e 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -34,7 +34,7 @@ namespace TerminalApp Windows.UI.Xaml.ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); Boolean GetShowTabsInTitlebar(); - Single SnapDimension(Boolean widthOrHeight, Single dimension); + Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension); void TitlebarClicked(); void WindowCloseButtonClicked(); diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 2fbd65eef4e..c9abf659164 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -26,7 +26,7 @@ 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 isRoot, const bool lastFocused) : +Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocused) : _control{ control }, _lastActive{ lastFocused }, _profile{ profile } @@ -35,14 +35,6 @@ Pane::Pane(const GUID& profile, const TermControl& control, const bool isRoot, c _border.Child(_control); _connectionStateChangedToken = _control.ConnectionStateChanged({ this, &Pane::_ControlConnectionStateChangedHandler }); - _fontSizeChangedToken = _control.FontSizeChanged({ this, &Pane::_FontSizeChangedHandler }); - - if (isRoot) - { - ShouldRelayout([=] { - ResizeContent(_root.ActualSize()); - }); - } // 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 @@ -84,7 +76,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 }; @@ -93,7 +85,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 }; @@ -102,6 +94,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. @@ -331,26 +334,6 @@ void Pane::_ControlConnectionStateChangedHandler(const TermControl& /*sender*/, } } -// Method Description: -// - Called when our 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. -// Arguments: -// - fontWidth - new font width in pixels -// - fontHeight - new font height in pixels -// - isInitialChange - whether terminal just got its proper font size. -// Return Value: -// - -void Pane::_FontSizeChangedHandler(const int /* fontWidth */, const int /* fontHeight */, const bool isInitialChange) -{ - if (isInitialChange) - { - _shouldRelayoutHandlers(); - } -} - // 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 @@ -373,9 +356,6 @@ void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable cons // - void Pane::Close() { - _control.FontSizeChanged(_fontSizeChangedToken); - _fontSizeChangedToken.value = 0; - // Fire our Closed event to tell our parent that we should be removed. _ClosedHandlers(nullptr, nullptr); } @@ -795,7 +775,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)); @@ -811,7 +791,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)); @@ -1017,9 +997,6 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState // parent. _gotFocusRevoker.revoke(); - _control.FontSizeChanged(_fontSizeChangedToken); - _fontSizeChangedToken.value = 0; - _splitState = splitType; _desiredSplitPosition = Half; @@ -1031,17 +1008,10 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState // Create two new Panes // Move our control, guid into the first one. // Move the new guid, control into the second. - _firstChild = std::make_shared(_profile.value(), _control, false); + _firstChild = std::make_shared(_profile.value(), _control); _profile = std::nullopt; _control = { nullptr }; - _secondChild = std::make_shared(profile, control, false); - - _firstChild->ShouldRelayout([=]() { - _shouldRelayoutHandlers(); - }); - _secondChild->ShouldRelayout([=]() { - _shouldRelayoutHandlers(); - }); + _secondChild = std::make_shared(profile, control); _CreateSplitContent(); @@ -1070,10 +1040,10 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState // 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) const +std::pair Pane::_CalcChildrenSizes(const float fullSize) const { const auto widthOrHeight = _splitState == SplitState::Vertical; - const auto snappedSizes = _CalcSnappedPaneDimensions(widthOrHeight, fullSize).lower; + const auto snappedSizes = _CalcSnappedChildrenSizes(widthOrHeight, fullSize).lower; // Keep the first pane snapped and give the second pane all remaining size return { @@ -1085,7 +1055,7 @@ std::pair Pane::_GetPaneSizes(const float fullSize) const // 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 +// 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. @@ -1094,12 +1064,12 @@ std::pair Pane::_GetPaneSizes(const float fullSize) const // - 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 +// - 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 -// complicity. The 'higher' field represents the size of the children if they slightly exceed +// complicity. 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::_CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) const +Pane::SnapChildrenSizeResult Pane::_CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const { if (_IsLeaf()) { @@ -1112,18 +1082,18 @@ Pane::SnapChildrenSizeResult Pane::_CalcSnappedPaneDimensions(const bool widthOr // 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 + // 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 sneek in the available area in + // 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 + // 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 = _GetMinSizeTree(widthOrHeight); + auto sizeTree = _CreateMinSizeTree(widthOrHeight); LayoutSizeNode lastSizeTree{ sizeTree }; while (sizeTree.size < fullSize) @@ -1156,9 +1126,9 @@ Pane::SnapChildrenSizeResult Pane::_CalcSnappedPaneDimensions(const bool widthOr // - 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::SnapDimension(const bool widthOrHeight, const float dimension) const +float Pane::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { - const auto snapPossibilites = _SnapDimension(widthOrHeight, dimension); + const auto snapPossibilites = _CalcSnappedDimension(widthOrHeight, dimension); const auto lower = snapPossibilites.lower; const auto higher = snapPossibilites.higher; return dimension - lower < higher - dimension ? lower : higher; @@ -1175,7 +1145,7 @@ float Pane::SnapDimension(const bool widthOrHeight, const float dimension) const // - 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::_SnapDimension(const bool widthOrHeight, const float dimension) const +Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { if (_IsLeaf()) { @@ -1189,7 +1159,19 @@ Pane::SnapSizeResult Pane::_SnapDimension(const bool widthOrHeight, const float return { minDimension, minDimension }; } - const float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension); + 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 @@ -1208,8 +1190,8 @@ Pane::SnapSizeResult Pane::_SnapDimension(const bool widthOrHeight, const float // If we're resizing along separator axis, snap to the closest possibility // given by our children panes. - const auto firstSnapped = _firstChild->_SnapDimension(widthOrHeight, dimension); - const auto secondSnapped = _secondChild->_SnapDimension(widthOrHeight, dimension); + 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) @@ -1223,7 +1205,7 @@ Pane::SnapSizeResult Pane::_SnapDimension(const bool widthOrHeight, const float // 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 = _CalcSnappedPaneDimensions(widthOrHeight, dimension); + const auto childSizes = _CalcSnappedChildrenSizes(widthOrHeight, dimension); return { childSizes.lower.first + childSizes.lower.second, childSizes.higher.first + childSizes.higher.second @@ -1254,7 +1236,7 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si // 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 = _SnapDimension(widthOrHeight, sizeNode.size + 1).higher; + sizeNode.size = _CalcSnappedDimension(widthOrHeight, sizeNode.size + 1).higher; } else { @@ -1368,10 +1350,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 }; } @@ -1398,14 +1380,14 @@ Size Pane::_GetMinSize() const // - widthOrHeight: if true operates on width, otherwise on height // Return Value: // - Root node of built tree that matches this pane. -Pane::LayoutSizeNode Pane::_GetMinSizeTree(const bool widthOrHeight) const +Pane::LayoutSizeNode Pane::_CreateMinSizeTree(const bool widthOrHeight) const { const auto size = _GetMinSize(); LayoutSizeNode node(widthOrHeight ? size.Width : size.Height); if (!_IsLeaf()) { - node.firstChild.reset(new LayoutSizeNode(_firstChild->_GetMinSizeTree(widthOrHeight))); - node.secondChild.reset(new LayoutSizeNode(_secondChild->_GetMinSizeTree(widthOrHeight))); + node.firstChild.reset(new LayoutSizeNode(_firstChild->_CreateMinSizeTree(widthOrHeight))); + node.secondChild.reset(new LayoutSizeNode(_secondChild->_CreateMinSizeTree(widthOrHeight))); } return node; @@ -1480,4 +1462,3 @@ void Pane::_SetupResources() } DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate>); -DEFINE_EVENT(Pane, ShouldRelayout, _shouldRelayoutHandlers, winrt::delegate<>); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 5d4f1e54483..f71bb268e04 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -45,7 +45,6 @@ class Pane : public std::enable_shared_from_this Pane(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, - const bool isRoot, const bool lastFocused = false); std::shared_ptr GetActivePane(); @@ -62,6 +61,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); @@ -69,13 +69,12 @@ class Pane : public std::enable_shared_from_this std::pair, std::shared_ptr> Split(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control); - float SnapDimension(const bool widthOrHeight, const float dimension) const; + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; void Close(); WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); - DECLARE_EVENT(ShouldRelayout, _shouldRelayoutHandlers, winrt::delegate<>); private: struct SnapSizeResult; @@ -98,7 +97,6 @@ class Pane : public std::enable_shared_from_this winrt::event_token _connectionStateChangedToken{ 0 }; winrt::event_token _firstClosedToken{ 0 }; winrt::event_token _secondClosedToken{ 0 }; - winrt::event_token _fontSizeChangedToken{ 0 }; winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker; @@ -127,17 +125,16 @@ 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 _FontSizeChangedHandler(const int fontWidth, const int fontHeight, const bool isInitialChange); void _ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); - std::pair _GetPaneSizes(const float fullSize) const; - SnapChildrenSizeResult _CalcSnappedPaneDimensions(const bool widthOrHeight, const float fullSize) const; - SnapSizeResult _SnapDimension(const bool widthOrHeight, const float dimension) const; + 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; - LayoutSizeNode _GetMinSizeTree(const bool widthOrHeight) const; + LayoutSizeNode _CreateMinSizeTree(const bool widthOrHeight) const; float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) const; // Function Description: diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 40e1e713c9a..6ad19e2f0c3 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -17,7 +17,7 @@ namespace winrt Tab::Tab(const GUID& profile, const TermControl& control) { - _rootPane = std::make_shared(profile, control, true, true); + _rootPane = std::make_shared(profile, control, true); _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { _ClosedHandlers(nullptr, nullptr); @@ -230,10 +230,10 @@ void Tab::SplitPane(Pane::SplitState splitType, const GUID& profile, TermControl } // Method Description: -// - See Pane::SnapDimension -float Tab::SnapDimension(const bool widthOrHeight, const float dimension) const +// - See Pane::CalcSnappedDimension +float Tab::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { - return _rootPane->SnapDimension(widthOrHeight, dimension); + return _rootPane->CalcSnappedDimension(widthOrHeight, dimension); } // Method Description: @@ -310,6 +310,20 @@ void Tab::_AttachEventHandlersToControl(const TermControl& control) auto newTabTitle = GetActiveTitle(); SetTabText(newTabTitle); }); + + // 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 0292443b357..5339fb6f820 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -23,7 +23,7 @@ class Tab bool CanSplitPane(Pane::SplitState splitType); void SplitPane(Pane::SplitState splitType, const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control); - float SnapDimension(const bool widthOrHeight, const float dimension) const; + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; void UpdateIcon(const winrt::hstring iconPath); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 052784f7939..51c74beb9f6 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1091,11 +1091,11 @@ namespace winrt::TerminalApp::implementation } // Method Description: - // - See Pane::SnapDimension - float TerminalPage::SnapDimension(const bool widthOrHeight, const float dimension) const + // - See Pane::CalcSnappedDimension + float TerminalPage::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { const auto focusedTabIndex = _GetFocusedTabIndex(); - return _tabs[focusedTabIndex]->SnapDimension(widthOrHeight, dimension); + return _tabs[focusedTabIndex]->CalcSnappedDimension(widthOrHeight, dimension); } // Method Description: diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 2e75edb744e..d53ce7fceef 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -34,7 +34,7 @@ namespace winrt::TerminalApp::implementation void TitlebarClicked(); - float SnapDimension(const bool widthOrHeight, const float dimension) const; + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; void CloseWindow(); diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index cecfb0dbf2d..9eb96cdcdfc 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -40,7 +40,7 @@ AppHost::AppHost() noexcept : std::placeholders::_3); _window->SetCreateCallback(pfn); - _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::AppLogic::SnapDimension, + _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::AppLogic::CalcSnappedDimension, _logic, std::placeholders::_1, std::placeholders::_2)); @@ -204,7 +204,7 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. - const auto nonClientSize = _window->GetNonClientSize(dpix); + const auto nonClientSize = _window->GetClient2WindowSizeDelta(dpix); adjustedWidth = islandWidth + nonClientSize.cx; adjustedHeight = islandHeight + nonClientSize.cy; } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index c53c5185c35..ab56b2add97 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -245,7 +245,7 @@ void IslandWindow::OnSize(const UINT width, const UINT height) // If this fails, we'll use the default of 96. GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy); - const auto nonClientSize = GetNonClientSize(dpix); + const auto nonClientSize = GetClient2WindowSizeDelta(dpix); auto clientWidth = winRect->right - winRect->left - nonClientSize.cx; auto clientHeight = winRect->bottom - winRect->top - nonClientSize.cy; @@ -378,7 +378,7 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) // - dpi: dpi of a monitor on which the window is placed // Return Value // - The size difference -SIZE IslandWindow::GetNonClientSize(const UINT dpi) const noexcept +SIZE IslandWindow::GetClient2WindowSizeDelta(const UINT dpi) const noexcept { RECT islandFrame{}; bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpi); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index f6ea4703baf..07d55f6b12f 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -29,7 +29,7 @@ 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 GetNonClientSize(const UINT dpi) const noexcept; + virtual SIZE GetClient2WindowSizeDelta(const UINT dpi) const noexcept; virtual void Initialize(); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 0e361e19fc7..d88a041e4f1 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -411,7 +411,7 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept // - dpi: dpi of a monitor on which the window is placed // Return Value // - The size difference -SIZE NonClientIslandWindow::GetNonClientSize(UINT dpi) const noexcept +SIZE NonClientIslandWindow::GetClient2WindowSizeDelta(UINT dpi) const noexcept { RECT islandFrame{}; bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpi); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index ebecf473a80..7da1e4ee00f 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -36,7 +36,7 @@ class NonClientIslandWindow : public IslandWindow [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; - virtual SIZE GetNonClientSize(UINT dpi) const noexcept override; + virtual SIZE GetClient2WindowSizeDelta(UINT dpi) const noexcept override; void Initialize() override; From b0097d220d8bdfba6d92f32f27f4dd112e5318f7 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Wed, 11 Dec 2019 10:12:32 +0100 Subject: [PATCH 21/24] Small PR changes --- src/cascadia/TerminalApp/Pane.cpp | 25 ++- .../TerminalApp/lib/Pane.LayoutSizeNode.cpp | 2 +- src/cascadia/WindowsTerminal/AppHost.cpp | 2 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 177 ++++++++++-------- src/cascadia/WindowsTerminal/IslandWindow.h | 3 +- .../WindowsTerminal/NonClientIslandWindow.cpp | 15 +- .../WindowsTerminal/NonClientIslandWindow.h | 2 +- 7 files changed, 118 insertions(+), 108 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index c9abf659164..92b7e1d09c1 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1066,7 +1066,7 @@ std::pair Pane::_CalcChildrenSizes(const float fullSize) const // 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 -// complicity. The 'higher' field represents the size of the children if they slightly exceed +// 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 @@ -1128,9 +1128,7 @@ Pane::SnapChildrenSizeResult Pane::_CalcSnappedChildrenSizes(const bool widthOrH // - 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 snapPossibilites = _CalcSnappedDimension(widthOrHeight, dimension); - const auto lower = snapPossibilites.lower; - const auto higher = snapPossibilites.higher; + const auto [lower, higher] = _CalcSnappedDimension(widthOrHeight, dimension); return dimension - lower < higher - dimension ? lower : higher; } @@ -1160,7 +1158,6 @@ Pane::SnapSizeResult Pane::_CalcSnappedDimension(const bool widthOrHeight, const } float lower = _control.SnapDimensionToGrid(widthOrHeight, dimension); - if (widthOrHeight) { lower += WI_IsFlagSet(_borders, Borders::Left) ? PaneBorderSize : 0; @@ -1233,7 +1230,7 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si 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 + // 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; @@ -1251,16 +1248,16 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si // 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. If not, we need to calculate them now. + // 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.reset(new LayoutSizeNode(*sizeNode.firstChild)); + sizeNode.nextFirstChild = std::make_unique(*sizeNode.firstChild); _firstChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextFirstChild); } if (sizeNode.nextSecondChild == nullptr) { - sizeNode.nextSecondChild.reset(new LayoutSizeNode(*sizeNode.secondChild)); + sizeNode.nextSecondChild = std::make_unique(*sizeNode.secondChild); _secondChild->_AdvanceSnappedDimension(widthOrHeight, *sizeNode.nextSecondChild); } @@ -1278,8 +1275,8 @@ void Pane::_AdvanceSnappedDimension(const bool widthOrHeight, LayoutSizeNode& si } else { - // If we're growing perpendicularly to separator axis, choose - // a child so that their size ratio is closer to that we're trying + // 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). @@ -1386,8 +1383,8 @@ Pane::LayoutSizeNode Pane::_CreateMinSizeTree(const bool widthOrHeight) const LayoutSizeNode node(widthOrHeight ? size.Width : size.Height); if (!_IsLeaf()) { - node.firstChild.reset(new LayoutSizeNode(_firstChild->_CreateMinSizeTree(widthOrHeight))); - node.secondChild.reset(new LayoutSizeNode(_secondChild->_CreateMinSizeTree(widthOrHeight))); + node.firstChild = std::make_unique(_firstChild->_CreateMinSizeTree(widthOrHeight)); + node.secondChild = std::make_unique(_secondChild->_CreateMinSizeTree(widthOrHeight)); } return node; diff --git a/src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp b/src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp index 650d8a101be..f3cc8647ff1 100644 --- a/src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp +++ b/src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp @@ -63,7 +63,7 @@ void Pane::LayoutSizeNode::_AssignChildNode(std::unique_ptr& nod } else { - nodeField.reset(new LayoutSizeNode(*newNode)); + nodeField = std::make_unique(*newNode); } } else diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 9eb96cdcdfc..d503f931877 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -204,7 +204,7 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. - const auto nonClientSize = _window->GetClient2WindowSizeDelta(dpix); + const auto nonClientSize = _window->GetTotalNonClientExclusiveSize(dpix); adjustedWidth = islandWidth + nonClientSize.cx; adjustedHeight = islandHeight + nonClientSize.cy; } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index ab56b2add97..0385a0b6517 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -146,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 = static_cast(_pfnSnapDimensionCallback(true, static_cast(clientWidth))); + } + if (wParam != WMSZ_LEFT && wParam != WMSZ_RIGHT) + { + // Analogous to above, but for height. + clientHeight = static_cast(_pfnSnapDimensionCallback(false, static_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); @@ -227,78 +316,7 @@ void IslandWindow::OnSize(const UINT width, const UINT height) } case WM_SIZING: { - // Here we intercept a window border (or corner) drag action and apply 'snapping' - // i.e. align the terminal's size to its cell grid. We're given the requested - // window rectangle (this is the one that originates form current drag action) in - // lparam, which we then adjust based on the terminal properties (like font size). - // Note that lparam also acts as the return value (it's a ref parameter). - - 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 = GetClient2WindowSizeDelta(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 = static_cast(_pfnSnapDimensionCallback(true, static_cast(clientWidth))); - } - if (wparam != WMSZ_LEFT && wparam != WMSZ_RIGHT) - { - // Analogous to above, but for height. - clientHeight = static_cast(_pfnSnapDimensionCallback(false, static_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; + return _OnSizing(wparam, lparam); } case WM_CLOSE: { @@ -378,17 +396,14 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) // - dpi: dpi of a monitor on which the window is placed // Return Value // - The size difference -SIZE IslandWindow::GetClient2WindowSizeDelta(const UINT dpi) const noexcept +SIZE IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept { RECT islandFrame{}; - bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpi); - if (!succeeded) - { - // 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 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, WS_OVERLAPPEDWINDOW, false, 0, dpi)); return { islandFrame.right - islandFrame.left, diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 07d55f6b12f..f6c7f9713b0 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -29,7 +29,7 @@ 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 GetClient2WindowSizeDelta(const UINT dpi) const noexcept; + virtual SIZE GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; virtual void Initialize(); @@ -92,6 +92,7 @@ class IslandWindow : 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 d88a041e4f1..49ee7d5d776 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -411,17 +411,14 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept // - dpi: dpi of a monitor on which the window is placed // Return Value // - The size difference -SIZE NonClientIslandWindow::GetClient2WindowSizeDelta(UINT dpi) const noexcept +SIZE NonClientIslandWindow::GetTotalNonClientExclusiveSize(UINT dpi) const noexcept { RECT islandFrame{}; - bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpi); - if (!succeeded) - { - // 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 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, WS_OVERLAPPEDWINDOW, false, 0, dpi)); islandFrame.top = -topBorderVisibleHeight; diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 7da1e4ee00f..3d7ade69291 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -36,7 +36,7 @@ class NonClientIslandWindow : public IslandWindow [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; - virtual SIZE GetClient2WindowSizeDelta(UINT dpi) const noexcept override; + virtual SIZE GetTotalNonClientExclusiveSize(UINT dpi) const noexcept override; void Initialize() override; From cb0186afd10c0d5a1221e3fefee086d2cd9b3e9b Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Wed, 11 Dec 2019 10:21:51 +0100 Subject: [PATCH 22/24] Move Pane.LayoutSizeNode.cpp out of lib folder --- src/cascadia/TerminalApp/{lib => }/Pane.LayoutSizeNode.cpp | 0 src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/cascadia/TerminalApp/{lib => }/Pane.LayoutSizeNode.cpp (100%) diff --git a/src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp b/src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp similarity index 100% rename from src/cascadia/TerminalApp/lib/Pane.LayoutSizeNode.cpp rename to src/cascadia/TerminalApp/Pane.LayoutSizeNode.cpp diff --git a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj index 1d0269173a5..bb321a356f8 100644 --- a/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/lib/TerminalAppLib.vcxproj @@ -135,7 +135,7 @@ - + Create From b9335283a5458e8ec4ef53db0b5407149a97e975 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Fri, 20 Dec 2019 17:51:19 +0100 Subject: [PATCH 23/24] Github apparently switched files to LF, so revert that --- src/cascadia/TerminalApp/Pane.cpp | 2996 ++++++++++---------- src/cascadia/TerminalControl/TermControl.h | 470 +-- 2 files changed, 1733 insertions(+), 1733 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 870c715487b..2ccb21da1e4 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1,1498 +1,1498 @@ -// 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: -// - -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: -// - -// 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. -// - 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(_root.ActualWidth()), - gsl::narrow_cast(_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: -// - -// Return Value: -// - -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: -// - -// 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: -// - -// Return Value: -// - -void Pane::Close() -{ - // Fire our Closed event to tell our parent that we should be removed. - _ClosedHandlers(nullptr, nullptr); -} - -// 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: -// - -// 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::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: -// - -// 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: -// - -// Return Value: -// - -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: -// - -// Return Value: -// - -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: -// - -// 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 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: -// - -// 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: -// - -// 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: -// - -// 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: -// - -// Return Value: -// - -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: -// - -// Return Value: -// - -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: -// - -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: -// - -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; - } -} - -// Method Description: -// - Adds event handlers to our children to handle their close events. -// Arguments: -// - -// Return Value: -// - -void Pane::_SetupChildCloseHandlers() -{ - _firstClosedToken = _firstChild->Closed([this](auto&& /*s*/, auto&& /*e*/) { - _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=]() { - _CloseChild(true); - }); - }); - - _secondClosedToken = _secondChild->Closed([this](auto&& /*s*/, auto&& /*e*/) { - _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=]() { - _CloseChild(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 -// availiable 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: -// - -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: -// - -// Return Value: -// - -void Pane::_CreateSplitContent() -{ - Size actualSize{ gsl::narrow_cast(_root.ActualWidth()), - gsl::narrow_cast(_root.ActualHeight()) }; - - _CreateRowColDefinitions(actualSize); -} - -// Method Description: -// - Sets the thickness of each side of our borders to match our _borders state. -// Arguments: -// - -// Return Value: -// - -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: -// - -// Return Value: -// - -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::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(_root.ActualWidth()), - gsl::narrow_cast(_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(_root.ActualWidth()), - gsl::narrow_cast(_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::_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(_profile.value(), _control); - _profile = std::nullopt; - _control = { nullptr }; - _secondChild = std::make_shared(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 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: -// - -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: -// - 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: -// - -// 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(_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: -// - 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: -// - -// Return Value: -// - -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(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(); - } - 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>); +// 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: +// - +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: +// - +// 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. +// - 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(_root.ActualWidth()), + gsl::narrow_cast(_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: +// - +// Return Value: +// - +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: +// - +// 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: +// - +// Return Value: +// - +void Pane::Close() +{ + // Fire our Closed event to tell our parent that we should be removed. + _ClosedHandlers(nullptr, nullptr); +} + +// 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: +// - +// 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::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: +// - +// 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: +// - +// Return Value: +// - +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: +// - +// Return Value: +// - +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: +// - +// 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 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: +// - +// 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: +// - +// 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: +// - +// 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: +// - +// Return Value: +// - +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: +// - +// Return Value: +// - +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: +// - +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: +// - +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; + } +} + +// Method Description: +// - Adds event handlers to our children to handle their close events. +// Arguments: +// - +// Return Value: +// - +void Pane::_SetupChildCloseHandlers() +{ + _firstClosedToken = _firstChild->Closed([this](auto&& /*s*/, auto&& /*e*/) { + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=]() { + _CloseChild(true); + }); + }); + + _secondClosedToken = _secondChild->Closed([this](auto&& /*s*/, auto&& /*e*/) { + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=]() { + _CloseChild(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 +// availiable 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: +// - +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: +// - +// Return Value: +// - +void Pane::_CreateSplitContent() +{ + Size actualSize{ gsl::narrow_cast(_root.ActualWidth()), + gsl::narrow_cast(_root.ActualHeight()) }; + + _CreateRowColDefinitions(actualSize); +} + +// Method Description: +// - Sets the thickness of each side of our borders to match our _borders state. +// Arguments: +// - +// Return Value: +// - +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: +// - +// Return Value: +// - +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::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(_root.ActualWidth()), + gsl::narrow_cast(_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(_root.ActualWidth()), + gsl::narrow_cast(_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::_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(_profile.value(), _control); + _profile = std::nullopt; + _control = { nullptr }; + _secondChild = std::make_shared(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 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: +// - +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: +// - 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: +// - +// 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(_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: +// - 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: +// - +// Return Value: +// - +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(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(); + } + 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>); diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 1f45b306c95..42ab95f6747 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -1,235 +1,235 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include "TermControl.g.h" -#include "CopyToClipboardEventArgs.g.h" -#include "PasteFromClipboardEventArgs.g.h" -#include -#include -#include "../../renderer/base/Renderer.hpp" -#include "../../renderer/dx/DxRenderer.hpp" -#include "../../renderer/uia/UiaRenderer.hpp" -#include "../../cascadia/TerminalCore/Terminal.hpp" -#include "../buffer/out/search.h" -#include "cppwinrt_utils.h" -#include "SearchBoxControl.h" - -namespace winrt::Microsoft::Terminal::TerminalControl::implementation -{ - struct CopyToClipboardEventArgs : - public CopyToClipboardEventArgsT - { - public: - CopyToClipboardEventArgs(hstring text, hstring html, hstring rtf) : - _text(text), - _html(html), - _rtf(rtf) {} - - hstring Text() { return _text; }; - hstring Html() { return _html; }; - hstring Rtf() { return _rtf; }; - - private: - hstring _text; - hstring _html; - hstring _rtf; - }; - - struct PasteFromClipboardEventArgs : - public PasteFromClipboardEventArgsT - { - public: - PasteFromClipboardEventArgs(std::function clipboardDataHandler) : - m_clipboardDataHandler(clipboardDataHandler) {} - - void HandleClipboardData(hstring value) - { - m_clipboardDataHandler(static_cast(value)); - }; - - private: - std::function m_clipboardDataHandler; - }; - - struct TermControl : TermControlT - { - TermControl(); - TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection); - - void UpdateSettings(Settings::IControlSettings newSettings); - - hstring Title(); - - bool CopySelectionToClipboard(bool trimTrailingWhitespace); - void PasteTextFromClipboard(); - 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); - int GetScrollOffset(); - int GetViewHeight() const; - - void AdjustFontSize(int fontSizeDelta); - void ResetFontSize(); - - void SwapChainChanged(); - - void CreateSearchBoxControl(); - - ~TermControl(); - - Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); - ::Microsoft::Console::Types::IUiaData* GetUiaData() const; - const FontInfo GetActualFont() const; - const Windows::UI::Xaml::Thickness GetPadding() const; - - TerminalConnection::ConnectionState ConnectionState() const; - - static Windows::Foundation::Point GetProposedDimensions(Microsoft::Terminal::Settings::IControlSettings const& settings, const uint32_t dpi); - - // 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); - DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(CopyToClipboard, _clipboardCopyHandlers, TerminalControl::TermControl, TerminalControl::CopyToClipboardEventArgs); - - TYPED_EVENT(ConnectionStateChanged, TerminalControl::TermControl, IInspectable); - // clang-format on - - private: - TerminalConnection::ITerminalConnection _connection; - bool _initializedTerminal; - - Windows::UI::Xaml::Controls::Grid _root; - Windows::UI::Xaml::Controls::Image _bgImageLayer; - Windows::UI::Xaml::Controls::SwapChainPanel _swapChainPanel; - Windows::UI::Xaml::Controls::Primitives::ScrollBar _scrollBar; - - winrt::com_ptr _searchBox; - - TSFInputControl _tsfInputControl; - - event_token _connectionOutputEventToken; - TermControl::Tapped_revoker _tappedRevoker; - TerminalConnection::ITerminalConnection::StateChanged_revoker _connectionStateChangedRevoker; - - std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal; - - std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer; - std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine; - std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine; - - Settings::IControlSettings _settings; - bool _focused; - std::atomic _closing; - - FontInfoDesired _desiredFont; - FontInfo _actualFont; - - std::optional _lastScrollOffset; - - // Auto scroll occurs when user, while selecting, drags cursor outside viewport. View is then scrolled to 'follow' the cursor. - double _autoScrollVelocity; - std::optional _autoScrollingPointerPoint; - Windows::UI::Xaml::DispatcherTimer _autoScrollTimer; - std::optional _lastAutoScrollUpdateTime; - - // storage location for the leading surrogate of a utf-16 surrogate pair - std::optional _leadingSurrogate; - - std::optional _cursorTimer; - - // If this is set, then we assume we are in the middle of panning the - // viewport via touch input. - std::optional _touchAnchor; - - using Timestamp = uint64_t; - - // imported from WinUser - // Used for PointerPoint.Timestamp Property (https://docs.microsoft.com/en-us/uwp/api/windows.ui.input.pointerpoint.timestamp#Windows_UI_Input_PointerPoint_Timestamp) - Timestamp _multiClickTimer; - Timestamp _lastMouseClick; - unsigned int _multiClickCounter; - std::optional _lastMouseClickPos; - - // Event revokers -- we need to deregister ourselves before we die, - // lest we get callbacks afterwards. - winrt::Windows::UI::Xaml::Controls::Control::SizeChanged_revoker _sizeChangedRevoker; - winrt::Windows::UI::Xaml::Controls::SwapChainPanel::CompositionScaleChanged_revoker _compositionScaleChangedRevoker; - winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; - winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker; - winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker; - - void _Create(); - void _ApplyUISettings(); - void _InitializeBackgroundBrush(); - void _BackgroundColorChanged(const uint32_t color); - bool _InitializeTerminal(); - 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); - void _PointerReleasedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); - void _MouseWheelHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); - void _ScrollbarChangeHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventArgs const& e); - void _GotFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e); - void _LostFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e); - - void _BlinkCursor(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); - void _SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition); - void _SendInputToConnection(const std::wstring& wstr); - void _SendPastedTextToConnection(const std::wstring& wstr); - void _SwapChainSizeChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::SizeChangedEventArgs const& e); - void _SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& args); - void _DoResize(const double newWidth, const double newHeight); - void _TerminalTitleChanged(const std::wstring_view& wstr); - void _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize); - - void _MouseScrollHandler(const double delta, Windows::UI::Input::PointerPoint const& pointerPoint); - void _MouseZoomHandler(const double delta); - void _MouseTransparencyHandler(const double delta); - - bool _CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); - bool _ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); - - void _TryStartAutoScroll(Windows::UI::Input::PointerPoint const& pointerPoint, const double scrollVelocity); - void _TryStopAutoScroll(const uint32_t pointerId); - void _UpdateAutoScroll(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); - - void _ScrollbarUpdater(Windows::UI::Xaml::Controls::Primitives::ScrollBar scrollbar, const int viewTop, const int viewHeight, const int bufferSize); - static Windows::UI::Xaml::Thickness _ParseThicknessFromPadding(const hstring padding); - - ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() const; - bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers); - - const COORD _GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition); - const unsigned int _NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime); - double _GetAutoScrollSpeed(double cursorDistanceFromBorder) const; - - void _Search(const winrt::Windows::Foundation::IInspectable& sender, const winrt::hstring& text); - void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, Windows::UI::Xaml::RoutedEventArgs const& args); - void _CloseSearchBoxControlHelper(); - - // TSFInputControl Handlers - void _CompositionCompleted(winrt::hstring text); - void _CurrentCursorPositionHandler(const IInspectable& sender, const CursorPositionEventArgs& eventArgs); - void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs); - }; -} - -namespace winrt::Microsoft::Terminal::TerminalControl::factory_implementation -{ - struct TermControl : TermControlT - { - }; -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "TermControl.g.h" +#include "CopyToClipboardEventArgs.g.h" +#include "PasteFromClipboardEventArgs.g.h" +#include +#include +#include "../../renderer/base/Renderer.hpp" +#include "../../renderer/dx/DxRenderer.hpp" +#include "../../renderer/uia/UiaRenderer.hpp" +#include "../../cascadia/TerminalCore/Terminal.hpp" +#include "../buffer/out/search.h" +#include "cppwinrt_utils.h" +#include "SearchBoxControl.h" + +namespace winrt::Microsoft::Terminal::TerminalControl::implementation +{ + struct CopyToClipboardEventArgs : + public CopyToClipboardEventArgsT + { + public: + CopyToClipboardEventArgs(hstring text, hstring html, hstring rtf) : + _text(text), + _html(html), + _rtf(rtf) {} + + hstring Text() { return _text; }; + hstring Html() { return _html; }; + hstring Rtf() { return _rtf; }; + + private: + hstring _text; + hstring _html; + hstring _rtf; + }; + + struct PasteFromClipboardEventArgs : + public PasteFromClipboardEventArgsT + { + public: + PasteFromClipboardEventArgs(std::function clipboardDataHandler) : + m_clipboardDataHandler(clipboardDataHandler) {} + + void HandleClipboardData(hstring value) + { + m_clipboardDataHandler(static_cast(value)); + }; + + private: + std::function m_clipboardDataHandler; + }; + + struct TermControl : TermControlT + { + TermControl(); + TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection); + + void UpdateSettings(Settings::IControlSettings newSettings); + + hstring Title(); + + bool CopySelectionToClipboard(bool trimTrailingWhitespace); + void PasteTextFromClipboard(); + 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); + int GetScrollOffset(); + int GetViewHeight() const; + + void AdjustFontSize(int fontSizeDelta); + void ResetFontSize(); + + void SwapChainChanged(); + + void CreateSearchBoxControl(); + + ~TermControl(); + + Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); + ::Microsoft::Console::Types::IUiaData* GetUiaData() const; + const FontInfo GetActualFont() const; + const Windows::UI::Xaml::Thickness GetPadding() const; + + TerminalConnection::ConnectionState ConnectionState() const; + + static Windows::Foundation::Point GetProposedDimensions(Microsoft::Terminal::Settings::IControlSettings const& settings, const uint32_t dpi); + + // 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); + DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(CopyToClipboard, _clipboardCopyHandlers, TerminalControl::TermControl, TerminalControl::CopyToClipboardEventArgs); + + TYPED_EVENT(ConnectionStateChanged, TerminalControl::TermControl, IInspectable); + // clang-format on + + private: + TerminalConnection::ITerminalConnection _connection; + bool _initializedTerminal; + + Windows::UI::Xaml::Controls::Grid _root; + Windows::UI::Xaml::Controls::Image _bgImageLayer; + Windows::UI::Xaml::Controls::SwapChainPanel _swapChainPanel; + Windows::UI::Xaml::Controls::Primitives::ScrollBar _scrollBar; + + winrt::com_ptr _searchBox; + + TSFInputControl _tsfInputControl; + + event_token _connectionOutputEventToken; + TermControl::Tapped_revoker _tappedRevoker; + TerminalConnection::ITerminalConnection::StateChanged_revoker _connectionStateChangedRevoker; + + std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal; + + std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer; + std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine; + std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine; + + Settings::IControlSettings _settings; + bool _focused; + std::atomic _closing; + + FontInfoDesired _desiredFont; + FontInfo _actualFont; + + std::optional _lastScrollOffset; + + // Auto scroll occurs when user, while selecting, drags cursor outside viewport. View is then scrolled to 'follow' the cursor. + double _autoScrollVelocity; + std::optional _autoScrollingPointerPoint; + Windows::UI::Xaml::DispatcherTimer _autoScrollTimer; + std::optional _lastAutoScrollUpdateTime; + + // storage location for the leading surrogate of a utf-16 surrogate pair + std::optional _leadingSurrogate; + + std::optional _cursorTimer; + + // If this is set, then we assume we are in the middle of panning the + // viewport via touch input. + std::optional _touchAnchor; + + using Timestamp = uint64_t; + + // imported from WinUser + // Used for PointerPoint.Timestamp Property (https://docs.microsoft.com/en-us/uwp/api/windows.ui.input.pointerpoint.timestamp#Windows_UI_Input_PointerPoint_Timestamp) + Timestamp _multiClickTimer; + Timestamp _lastMouseClick; + unsigned int _multiClickCounter; + std::optional _lastMouseClickPos; + + // Event revokers -- we need to deregister ourselves before we die, + // lest we get callbacks afterwards. + winrt::Windows::UI::Xaml::Controls::Control::SizeChanged_revoker _sizeChangedRevoker; + winrt::Windows::UI::Xaml::Controls::SwapChainPanel::CompositionScaleChanged_revoker _compositionScaleChangedRevoker; + winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; + winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker; + winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker; + + void _Create(); + void _ApplyUISettings(); + void _InitializeBackgroundBrush(); + void _BackgroundColorChanged(const uint32_t color); + bool _InitializeTerminal(); + 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); + void _PointerReleasedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void _MouseWheelHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void _ScrollbarChangeHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventArgs const& e); + void _GotFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e); + void _LostFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e); + + void _BlinkCursor(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); + void _SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition); + void _SendInputToConnection(const std::wstring& wstr); + void _SendPastedTextToConnection(const std::wstring& wstr); + void _SwapChainSizeChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::SizeChangedEventArgs const& e); + void _SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& args); + void _DoResize(const double newWidth, const double newHeight); + void _TerminalTitleChanged(const std::wstring_view& wstr); + void _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize); + + void _MouseScrollHandler(const double delta, Windows::UI::Input::PointerPoint const& pointerPoint); + void _MouseZoomHandler(const double delta); + void _MouseTransparencyHandler(const double delta); + + bool _CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + bool _ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + + void _TryStartAutoScroll(Windows::UI::Input::PointerPoint const& pointerPoint, const double scrollVelocity); + void _TryStopAutoScroll(const uint32_t pointerId); + void _UpdateAutoScroll(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); + + void _ScrollbarUpdater(Windows::UI::Xaml::Controls::Primitives::ScrollBar scrollbar, const int viewTop, const int viewHeight, const int bufferSize); + static Windows::UI::Xaml::Thickness _ParseThicknessFromPadding(const hstring padding); + + ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() const; + bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers); + + const COORD _GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition); + const unsigned int _NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime); + double _GetAutoScrollSpeed(double cursorDistanceFromBorder) const; + + void _Search(const winrt::Windows::Foundation::IInspectable& sender, const winrt::hstring& text); + void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, Windows::UI::Xaml::RoutedEventArgs const& args); + void _CloseSearchBoxControlHelper(); + + // TSFInputControl Handlers + void _CompositionCompleted(winrt::hstring text); + void _CurrentCursorPositionHandler(const IInspectable& sender, const CursorPositionEventArgs& eventArgs); + void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs); + }; +} + +namespace winrt::Microsoft::Terminal::TerminalControl::factory_implementation +{ + struct TermControl : TermControlT + { + }; +} From 1ebddfcc593bd237c691bba2476a1090dad4c4f6 Mon Sep 17 00:00:00 2001 From: MCpiroman Date: Sat, 4 Jan 2020 10:46:48 +0100 Subject: [PATCH 24/24] Some things that miniksa mentioned in #4068 --- src/cascadia/WindowsTerminal/IslandWindow.cpp | 7 ++++--- src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 3038c2c7d76..d0fc574ab55 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -188,12 +188,12 @@ LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam) // 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 = static_cast(_pfnSnapDimensionCallback(true, static_cast(clientWidth))); + clientWidth = gsl::narrow_cast(_pfnSnapDimensionCallback(true, gsl::narrow_cast(clientWidth))); } if (wParam != WMSZ_LEFT && wParam != WMSZ_RIGHT) { // Analogous to above, but for height. - clientHeight = static_cast(_pfnSnapDimensionCallback(false, static_cast(clientHeight))); + clientHeight = gsl::narrow_cast(_pfnSnapDimensionCallback(false, gsl::narrow_cast(clientHeight))); } // Now make the window rectangle match the calculated client width and height, @@ -398,12 +398,13 @@ void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content) // - 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, WS_OVERLAPPEDWINDOW, false, 0, dpi)); + LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi)); return { islandFrame.right - islandFrame.left, diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 68a7028b6fd..fc85e78f977 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -406,12 +406,13 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept // - 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, WS_OVERLAPPEDWINDOW, false, 0, dpi)); + LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi)); islandFrame.top = -topBorderVisibleHeight;