Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 155 additions & 20 deletions src/cascadia/TerminalApp/Pane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Graphics::Display;
using namespace winrt::Windows::System;
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Core;
Expand All @@ -27,6 +28,17 @@ static const int CombinedPaneBorderSize = 2 * PaneBorderSize;
static const int AnimationDurationInMilliseconds = 200;
static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(AnimationDurationInMilliseconds)));

// Keyboard auto-repeat is ~30Hz. Each applied resize reflows every terminal
// under the splitter. The live gap tracks recent UI cost and stays between
// key-repeat (don't outrun input) and a still-moving floor.
constexpr auto PaneResizeLayoutMinInterval = std::chrono::milliseconds{ 32 };
constexpr auto PaneResizeLayoutMaxInterval = std::chrono::milliseconds{ 250 };

// Two key-repeat steps (5% each). Bounds how far one applied layout may move
// the splitter; anything more means input piled up during a UI stall and
// would land as one huge jump.
constexpr auto PaneResizeMaxSplitDeltaPerLayout = 0.10f;

Pane::Pane(IPaneContent content, const bool lastFocused) :
_lastActive{ lastFocused }
{
Expand Down Expand Up @@ -239,7 +251,8 @@ Pane::BuildStartupState Pane::BuildStartupActions(uint32_t currentId, uint32_t n
// 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%
// - Adjusts the separation amount by 5%. The grid is updated at a capped rate
// so key auto-repeat cannot queue a layout of every pane under the splitter.
// - 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,
Expand Down Expand Up @@ -271,8 +284,16 @@ bool Pane::_Resize(const ResizeDirection& direction)

_desiredSplitPosition = _ClampSplitPosition(changeWidth, _desiredSplitPosition - amount, actualDimension);

// Resize our columns to match the new percentages.
_CreateRowColDefinitions();
// Always accept the new percentage so a held key still progresses. Apply
// the grid at the current interval so auto-repeat cannot queue layouts.
if (std::chrono::steady_clock::now() - _lastSplitLayoutAt < _splitLayoutInterval)
{
_ScheduleSplitLayout();
}
else
{
_ApplySplitLayoutNow();
}

return true;
}
Expand Down Expand Up @@ -1011,6 +1032,12 @@ void Pane::Close()
// and connections beneath it.
void Pane::Shutdown()
{
if (_splitLayoutTimer)
{
_splitLayoutTimer.Stop();
_splitLayoutTimer = nullptr;
}

if (_IsLeaf())
{
_setPaneContent(nullptr);
Expand Down Expand Up @@ -1774,36 +1801,144 @@ void Pane::_CreateRowColDefinitions()
{
const auto first = _desiredSplitPosition * 100.0f;
const auto second = 100.0f - first;
const auto firstLen = GridLengthHelper::FromValueAndType(first, GridUnitType::Star);
const auto secondLen = GridLengthHelper::FromValueAndType(second, GridUnitType::Star);
if (_splitState == SplitState::Vertical)
{
_root.ColumnDefinitions().Clear();

// Create two columns in this grid: one for each pane
const auto columns = _root.ColumnDefinitions();
if (columns.Size() == 2)
{
columns.GetAt(0).Width(firstLen);
columns.GetAt(1).Width(secondLen);
}
else
{
columns.Clear();

auto firstColDef = Controls::ColumnDefinition();
firstColDef.Width(GridLengthHelper::FromValueAndType(first, GridUnitType::Star));
auto firstColDef = Controls::ColumnDefinition();
firstColDef.Width(firstLen);

auto secondColDef = Controls::ColumnDefinition();
secondColDef.Width(GridLengthHelper::FromValueAndType(second, GridUnitType::Star));
auto secondColDef = Controls::ColumnDefinition();
secondColDef.Width(secondLen);

_root.ColumnDefinitions().Append(firstColDef);
_root.ColumnDefinitions().Append(secondColDef);
columns.Append(firstColDef);
columns.Append(secondColDef);
}
}
else if (_splitState == SplitState::Horizontal)
{
_root.RowDefinitions().Clear();
const auto rows = _root.RowDefinitions();
if (rows.Size() == 2)
{
rows.GetAt(0).Height(firstLen);
rows.GetAt(1).Height(secondLen);
}
else
{
rows.Clear();

// Create two rows in this grid: one for each pane
auto firstRowDef = Controls::RowDefinition();
firstRowDef.Height(firstLen);

auto firstRowDef = Controls::RowDefinition();
firstRowDef.Height(GridLengthHelper::FromValueAndType(first, GridUnitType::Star));
auto secondRowDef = Controls::RowDefinition();
secondRowDef.Height(secondLen);

auto secondRowDef = Controls::RowDefinition();
secondRowDef.Height(GridLengthHelper::FromValueAndType(second, GridUnitType::Star));
rows.Append(firstRowDef);
rows.Append(secondRowDef);
}
}

_root.RowDefinitions().Append(firstRowDef);
_root.RowDefinitions().Append(secondRowDef);
_lastAppliedSplitPosition = _desiredSplitPosition;
}

void Pane::_ApplySplitLayoutNow()
{
if (_IsLeaf() || _splitState == SplitState::None)
{
return;
}

// A no-op (position unchanged, e.g. held key at the clamp boundary) costs
// nothing: don't refresh the interval clock, so the next real move applies
// immediately instead of waiting out a stale interval.
if (_lastAppliedSplitPosition == _desiredSplitPosition)
{
return;
}

// Cap each applied move and drop the excess (a held key simply travels a
// little less). Never binds at the normal 1-2 steps per apply.
if (_lastAppliedSplitPosition)
{
_desiredSplitPosition = std::clamp(_desiredSplitPosition,
*_lastAppliedSplitPosition - PaneResizeMaxSplitDeltaPerLayout,
*_lastAppliedSplitPosition + PaneResizeMaxSplitDeltaPerLayout);
}

const auto startedAt = std::chrono::steady_clock::now();
_CreateRowColDefinitions();
_lastSplitLayoutAt = startedAt;

// Sample after this pump (and typical XAML layout). Cost is the real
// machine+tree price.
if (const auto dispatcher = DispatcherQueue::GetForCurrentThread())
{
dispatcher.TryEnqueue(DispatcherQueuePriority::Low, [weak = weak_from_this(), startedAt]() {
if (auto self = weak.lock())
{
self->_AdoptSplitLayoutCost(std::chrono::steady_clock::now() - startedAt);
}
});
}
}

void Pane::_AdoptSplitLayoutCost(const std::chrono::steady_clock::duration cost)
{
using ms = std::chrono::milliseconds;
// 2x headroom so the next apply starts after the UI has recovered.
auto target = std::chrono::duration_cast<ms>(cost) * 2;
if (target < PaneResizeLayoutMinInterval)
{
target = PaneResizeLayoutMinInterval;
}
else if (target > PaneResizeLayoutMaxInterval)
{
target = PaneResizeLayoutMaxInterval;
}
_splitLayoutInterval = (_splitLayoutInterval * 3 + target) / 4;
}

void Pane::_ScheduleSplitLayout()
{
const auto dispatcher = DispatcherQueue::GetForCurrentThread();
if (!dispatcher)
{
_ApplySplitLayoutNow();
return;
}

if (!_splitLayoutTimer)
{
_splitLayoutTimer = dispatcher.CreateTimer();
_splitLayoutTimer.IsRepeating(false);
_splitLayoutTimer.Tick([weak = weak_from_this()](auto&&, auto&&) {
if (auto self = weak.lock())
{
self->_ApplySplitLayoutNow();
}
});
}

if (_splitLayoutTimer.IsRunning())
{
return;
}

const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - _lastSplitLayoutAt);
const auto delay = elapsed >= _splitLayoutInterval ? std::chrono::milliseconds{ 0 } : (_splitLayoutInterval - elapsed);

_splitLayoutTimer.Interval(delay);
_splitLayoutTimer.Start();
}

// Method Description:
Expand Down
14 changes: 14 additions & 0 deletions src/cascadia/TerminalApp/Pane.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

#pragma once

#include <chrono>
#include <optional>

#include "TaskbarState.h"
#include "TerminalPaneContent.h"

Expand Down Expand Up @@ -262,6 +265,14 @@ class Pane : public std::enable_shared_from_this<Pane>
bool _zoomed{ false };
bool _broadcastEnabled{ false };

// Key auto-repeat can fire resizePane faster than XAML + every terminal
// in the tree can reflow. Apply the latest split position at a rate
// that tracks recent layout cost.
std::optional<float> _lastAppliedSplitPosition;
std::chrono::steady_clock::time_point _lastSplitLayoutAt{};
std::chrono::milliseconds _splitLayoutInterval{ 125 };
winrt::Windows::System::DispatcherQueueTimer _splitLayoutTimer{ nullptr };

bool _IsLeaf() const noexcept;
bool _HasFocusedChild() const noexcept;
void _SetupChildCloseHandlers();
Expand All @@ -275,6 +286,9 @@ class Pane : public std::enable_shared_from_this<Pane>
std::shared_ptr<Pane> newPane);

void _CreateRowColDefinitions();
void _ApplySplitLayoutNow();
void _ScheduleSplitLayout();
void _AdoptSplitLayoutCost(std::chrono::steady_clock::duration cost);
void _ApplySplitDefinitions();
void _SetupEntranceAnimation();
void _UpdateBorders();
Expand Down