Compare commits

..

15 Commits

Author SHA1 Message Date
Mike Griese
c67adbdedb need you 2026-04-24 06:31:05 -05:00
Mike Griese
134a2d5d39 Merge remote-tracking branch 'origin/main' into dev/migrie/f/overview-view 2026-04-24 05:35:34 -05:00
Mike Griese
47950d2753 nope don't check that in 2026-04-24 05:33:44 -05:00
Mike Griese
d3c17f9d6f squad: log overview pre-dismiss before DoAction
Coordinator direct edit in TerminalPage::_KeyDownHandler: dismiss
OverviewPane before _actionDispatch->DoAction when a non-ToggleOverview
action resolves. Restores reparented tab Content to its original parent
before any tab-state-mutating action runs.

Requested by: Mike Griese.
2026-04-18 20:19:29 -05:00
Mike Griese
451bf37008 Overview: route unrecognized keys to global action dispatch
Add PreviewKeyDown=_KeyDownHandler to OverviewPaneElement in TerminalPage.xaml so global keybindings continue to work while the overview is visible. Local grid keys (Tab/arrows/Enter/Escape) remain handled by OverviewPane::_OnKeyDown. Matches the CommandPalette/SuggestionsControl pattern.
2026-04-18 20:01:05 -05:00
Mike Griese
5a3ae31453 avoid a _mild_ exception 2026-04-18 19:56:50 -05:00
Mike Griese
3dd5dadb38 squad: log overview external-dismiss fix
Dallas extracted _DismissOverviewVisuals() helper and wired
_OnTabSelectionChanged to release reparented overview content
before _UpdatedSelectedTab mounts the new tab.

Requested-by: Mike Griese
2026-04-18 06:58:18 -05:00
Mike Griese
724c519788 theme aware bg 2026-04-18 06:50:47 -05:00
Mike Griese
3cc213a2c1 mildly cleaner 2026-03-31 21:32:51 -05:00
Mike Griese
0afdc93bef i'd ship this 2026-03-30 19:55:14 -05:00
Mike Griese
6f384c59f4 Awesome, click on the previews anywhere 2026-03-30 15:57:27 -05:00
Mike Griese
66a828bc02 Weird thing: Why do we hit this all the time now? We never did before? 2026-03-30 15:36:18 -05:00
Mike Griese
e19e710e03 yea that's wacky 2026-03-30 14:51:30 -05:00
Mike Griese
54604076b9 what the fuck 2026-03-30 14:10:40 -05:00
Mike Griese
ceef4d46ed no way 2026-03-30 13:37:41 -05:00
16 changed files with 1051 additions and 0 deletions

View File

@@ -604,6 +604,13 @@ namespace winrt::TerminalApp::implementation
args.Handled(true);
}
void TerminalPage::_HandleToggleOverview(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
ToggleOverview();
args.Handled(true);
}
void TerminalPage::_HandleSetFocusMode(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{

View File

@@ -0,0 +1,690 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "OverviewPane.h"
#include "OverviewPane.g.cpp"
using namespace winrt;
using namespace winrt::TerminalApp;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Xaml::Input;
using namespace winrt::Windows::UI::Xaml::Media;
using namespace winrt::Windows::UI::Xaml::Media::Animation;
using namespace winrt::Windows::System;
namespace winrt::TerminalApp::implementation
{
static constexpr double PreviewCellWidth = 320.0;
static constexpr double PreviewCellHeight = 220.0;
static constexpr std::chrono::milliseconds EnterAnimDuration{ 400 };
static constexpr std::chrono::milliseconds ExitAnimDuration{ 100 };
OverviewPane::OverviewPane()
{
InitializeComponent();
// Apply the initial background (opaque by default, since _useMica
// starts false). Without this the XAML-declared brush stays in place
// until someone explicitly calls UseMica().
_UpdateBackgroundForMica();
// Listen for layout passes so we can start the zoom-in animation
// once the WrapGrid has measured and positioned its children.
PreviewGrid().LayoutUpdated([weakThis = get_weak()](auto&&, auto&&) {
if (auto self = weakThis.get())
{
if (!self->_pendingEnterAnimation)
{
return;
}
// Check that at least the first cell is laid out
auto items = self->PreviewGrid().Items();
if (items.Size() == 0)
{
return;
}
auto first = items.GetAt(0).try_as<FrameworkElement>();
if (!first || first.ActualWidth() <= 0)
{
return;
}
self->_pendingEnterAnimation = false;
self->_StartEnterZoomAnimation();
}
});
}
OverviewPane::~OverviewPane()
{
ClearTabContent();
}
void OverviewPane::UpdateTabContent(Windows::Foundation::Collections::IVector<TerminalApp::Tab> tabs, int32_t focusedIndex)
{
// Clear any previous state
ClearTabContent();
if (!tabs || tabs.Size() == 0)
{
return;
}
const auto itemsControl = PreviewGrid();
// Determine a reference size from the currently visible tab's content.
// Inactive tabs have zero ActualWidth/Height since they're not laid out
// in the visual tree, but all tabs share the same content area, so the
// active tab's size is the right reference for all of them.
double referenceWidth = 0;
double referenceHeight = 0;
if (focusedIndex >= 0 && focusedIndex < static_cast<int32_t>(tabs.Size()))
{
auto focusedContent = tabs.GetAt(static_cast<uint32_t>(focusedIndex)).Content();
if (focusedContent)
{
referenceWidth = focusedContent.ActualWidth();
referenceHeight = focusedContent.ActualHeight();
}
}
for (uint32_t i = 0; i < tabs.Size(); i++)
{
const auto& tab = tabs.GetAt(i);
auto cell = _BuildPreviewCell(tab, static_cast<int32_t>(i), referenceWidth, referenceHeight);
itemsControl.Items().Append(cell);
}
_selectedIndex = focusedIndex;
_UpdateSelection();
_PlayEnterAnimation();
// Focus self for keyboard input
Focus(FocusState::Programmatic);
}
void OverviewPane::ClearTabContent()
{
_pendingEnterAnimation = false;
// Stop any running exit animation
if (_exitContentStoryboard)
{
_exitContentStoryboard.Completed(_exitAnimationToken);
_exitAnimationToken = {};
_exitContentStoryboard.Stop();
_exitContentStoryboard = nullptr;
}
// Reset the zoom transform to identity
auto transform = ContentTransform();
transform.ScaleX(1.0);
transform.ScaleY(1.0);
transform.TranslateX(0.0);
transform.TranslateY(0.0);
ContentWrapper().Opacity(1.0);
// Reparent content back to original parents
for (auto& entry : _reparentedContent)
{
if (entry.content)
{
// Restore original Width/Height (NaN = auto-sizing)
entry.content.Width(entry.originalWidth);
entry.content.Height(entry.originalHeight);
// Restore original RenderTransform and origin
entry.content.RenderTransform(entry.originalRenderTransform);
entry.content.RenderTransformOrigin(entry.originalRenderTransformOrigin);
_DetachContent(entry.content);
// Put it back where it came from
if (entry.originalParent)
{
entry.originalParent.Children().Append(entry.content);
}
}
}
_reparentedContent.clear();
const auto itemsControl = PreviewGrid();
itemsControl.Items().Clear();
}
int32_t OverviewPane::SelectedIndex() const
{
return _selectedIndex;
}
void OverviewPane::SelectedIndex(int32_t value)
{
if (_selectedIndex != value)
{
_selectedIndex = value;
_UpdateSelection();
}
}
bool OverviewPane::UseMica() const
{
return _useMica;
}
void OverviewPane::UseMica(bool value)
{
if (_useMica != value)
{
_useMica = value;
_UpdateBackgroundForMica();
}
}
void OverviewPane::_UpdateBackgroundForMica()
{
auto overlay = BackgroundOverlay();
if (_useMica)
{
// Transparent background — let the Mica backdrop show through
overlay.Background(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
}
else
{
// Opaque background when Mica is not active.
// Use the theme-aware SolidBackgroundFillColorBaseBrush so we
// match the correct color for light / dark theme.
auto res = Application::Current().Resources();
auto brush = res.TryLookup(winrt::box_value(L"SolidBackgroundFillColorBaseBrush"));
if (brush)
{
overlay.Background(brush.as<Brush>());
}
else
{
overlay.Background(SolidColorBrush{ winrt::Windows::UI::ColorHelper::FromArgb(255, 32, 32, 32) });
}
}
}
void OverviewPane::_OnPreviewKeyDown(const IInspectable& /*sender*/, const KeyRoutedEventArgs& e)
{
if (e.OriginalKey() != VirtualKey::Tab)
{
return;
}
const auto items = PreviewGrid().Items();
const auto itemCount = static_cast<int32_t>(items.Size());
if (itemCount == 0)
{
return;
}
const auto shiftPressed = (Windows::UI::Core::CoreWindow::GetForCurrentThread().GetKeyState(VirtualKey::Shift) & Windows::UI::Core::CoreVirtualKeyStates::Down) == Windows::UI::Core::CoreVirtualKeyStates::Down;
if (shiftPressed)
{
if (_selectedIndex > 0)
{
_selectedIndex--;
_UpdateSelection();
}
}
else
{
if (_selectedIndex < itemCount - 1)
{
_selectedIndex++;
_UpdateSelection();
}
}
e.Handled(true);
}
void OverviewPane::_OnKeyDown(const IInspectable& /*sender*/, const KeyRoutedEventArgs& e)
{
const auto items = PreviewGrid().Items();
const auto itemCount = static_cast<int32_t>(items.Size());
if (itemCount == 0)
{
return;
}
auto handled = true;
switch (e.OriginalKey())
{
case VirtualKey::Left:
if (_selectedIndex > 0)
{
_selectedIndex--;
_UpdateSelection();
}
break;
case VirtualKey::Right:
if (_selectedIndex < itemCount - 1)
{
_selectedIndex++;
_UpdateSelection();
}
break;
case VirtualKey::Up:
if (_selectedIndex - _columnCount >= 0)
{
_selectedIndex -= _columnCount;
_UpdateSelection();
}
break;
case VirtualKey::Down:
if (_selectedIndex + _columnCount < itemCount)
{
_selectedIndex += _columnCount;
_UpdateSelection();
}
break;
case VirtualKey::Enter:
_OnItemClicked(_selectedIndex);
break;
case VirtualKey::Escape:
_PlayExitAnimation([weakThis = get_weak()]() {
if (auto self = weakThis.get())
{
self->Dismissed.raise(*self, nullptr);
}
});
break;
default:
handled = false;
break;
}
e.Handled(handled);
}
void OverviewPane::_OnItemClicked(int32_t index)
{
_PlayExitAnimation([weakThis = get_weak(), index]() {
if (auto self = weakThis.get())
{
self->TabSelected.raise(*self, winrt::Windows::Foundation::IReference<int32_t>{ index });
}
});
}
void OverviewPane::_UpdateSelection()
{
const auto items = PreviewGrid().Items();
const auto itemCount = static_cast<int32_t>(items.Size());
// Clamp selection
_selectedIndex = std::clamp(_selectedIndex, 0, std::max(0, itemCount - 1));
for (int32_t i = 0; i < itemCount; i++)
{
if (auto cellElement = items.GetAt(i).try_as<FrameworkElement>())
{
if (auto border = cellElement.try_as<Border>())
{
if (i == _selectedIndex)
{
// Accent-colored border for selected item
const auto accentBrush = Application::Current()
.Resources()
.Lookup(winrt::box_value(L"SystemAccentColor"))
.as<winrt::Windows::UI::Color>();
border.BorderBrush(SolidColorBrush{ accentBrush });
border.BorderThickness(ThicknessHelper::FromUniformLength(2));
// Scroll into view if needed
border.StartBringIntoView();
}
else
{
border.BorderBrush(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
border.BorderThickness(ThicknessHelper::FromUniformLength(2));
}
}
}
}
}
void OverviewPane::_PlayEnterAnimation()
{
// Hide the content wrapper until the LayoutUpdated callback fires
// and we can read cell positions to set up the zoom transform.
ContentWrapper().Opacity(0);
_pendingEnterAnimation = true;
}
void OverviewPane::_StartEnterZoomAnimation()
{
// Start the background fade-in together with the zoom so both
// animations are visible at the same moment (avoids opacity flash).
if (auto bgSb = Resources().Lookup(winrt::box_value(L"BackgroundFadeIn")).try_as<Storyboard>())
{
bgSb.Begin();
}
auto wrapper = ContentWrapper();
auto transform = ContentTransform();
auto zoomParams = _GetZoomParamsForCell(_selectedIndex);
if (!zoomParams)
{
wrapper.Opacity(1.0);
return;
}
auto [scale, tx, ty] = *zoomParams;
// Set the initial transform so the focused cell fills the viewport
transform.ScaleX(scale);
transform.ScaleY(scale);
transform.TranslateX(tx);
transform.TranslateY(ty);
wrapper.Opacity(1.0);
// Animate from the zoomed-in state to identity (zoom out to grid)
Storyboard storyboard;
const auto duration = DurationHelper::FromTimeSpan(EnterAnimDuration);
CubicEase easing;
easing.EasingMode(EasingMode::EaseOut);
_AddDoubleAnimation(storyboard, transform, L"ScaleX", scale, 1.0, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"ScaleY", scale, 1.0, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateX", tx, 0.0, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateY", ty, 0.0, duration, easing);
storyboard.Begin();
}
void OverviewPane::_PlayExitAnimation(std::function<void()> onComplete)
{
// Fade out the background overlay
if (auto bgSb = Resources().Lookup(winrt::box_value(L"BackgroundFadeOut")).try_as<Storyboard>())
{
bgSb.Begin();
}
auto zoomParams = _GetZoomParamsForCell(_selectedIndex);
if (!zoomParams)
{
if (onComplete)
{
onComplete();
}
return;
}
auto [scale, tx, ty] = *zoomParams;
// Animate from the current grid view into the selected cell
auto transform = ContentTransform();
Storyboard storyboard;
const auto duration = DurationHelper::FromTimeSpan(ExitAnimDuration);
CubicEase easing;
easing.EasingMode(EasingMode::EaseIn);
_AddDoubleAnimation(storyboard, transform, L"ScaleX", 1.0, scale, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"ScaleY", 1.0, scale, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateX", 0.0, tx, duration, easing);
_AddDoubleAnimation(storyboard, transform, L"TranslateY", 0.0, ty, duration, easing);
// Revoke any previously registered Completed handler
if (_exitContentStoryboard)
{
_exitContentStoryboard.Completed(_exitAnimationToken);
}
_exitContentStoryboard = storyboard;
_exitAnimationToken = storyboard.Completed([weakThis = get_weak(), onComplete](auto&&, auto&&) {
if (auto self = weakThis.get())
{
if (onComplete)
{
onComplete();
}
}
});
storyboard.Begin();
}
std::optional<std::tuple<double, double, double>> OverviewPane::_GetZoomParamsForCell(int32_t index)
{
auto wrapper = ContentWrapper();
const auto vpW = wrapper.ActualWidth();
const auto vpH = wrapper.ActualHeight();
if (vpW <= 0 || vpH <= 0)
{
return std::nullopt;
}
auto items = PreviewGrid().Items();
if (index < 0 || index >= static_cast<int32_t>(items.Size()))
{
return std::nullopt;
}
auto cell = items.GetAt(static_cast<uint32_t>(index)).try_as<FrameworkElement>();
if (!cell || cell.ActualWidth() <= 0 || cell.ActualHeight() <= 0)
{
return std::nullopt;
}
const auto cellW = cell.ActualWidth();
const auto cellH = cell.ActualHeight();
// Cell center relative to ContentWrapper (accounts for scroll offset)
auto cellTransform = cell.TransformToVisual(wrapper);
auto topLeft = cellTransform.TransformPoint({ 0.0f, 0.0f });
const auto cellCX = static_cast<double>(topLeft.X) + cellW / 2.0;
const auto cellCY = static_cast<double>(topLeft.Y) + cellH / 2.0;
// Scale so the cell fits the viewport
const auto scale = std::min(vpW / cellW, vpH / cellH);
// With RenderTransformOrigin={0.5,0.5} the scale origin is the
// center of ContentWrapper. Translate so the cell center lands
// at the viewport center after scaling.
const auto translateX = (vpW / 2.0 - cellCX) * scale;
const auto translateY = (vpH / 2.0 - cellCY) * scale;
return std::tuple{ scale, translateX, translateY };
}
FrameworkElement OverviewPane::_BuildPreviewCell(const TerminalApp::Tab& tab, int32_t index, double referenceWidth, double referenceHeight)
{
// Outer border — serves as the selection indicator
Border outerBorder;
outerBorder.BorderBrush(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
outerBorder.BorderThickness(ThicknessHelper::FromUniformLength(2));
outerBorder.CornerRadius({ 8, 8, 8, 8 });
outerBorder.Padding(ThicknessHelper::FromUniformLength(4));
outerBorder.Margin(ThicknessHelper::FromUniformLength(6));
// Vertical stack: preview box + title
StackPanel cellStack;
cellStack.Orientation(Orientation::Vertical);
cellStack.HorizontalAlignment(HorizontalAlignment::Center);
// Preview container with a dark background
Border previewBorder;
previewBorder.Width(PreviewCellWidth);
previewBorder.Height(PreviewCellHeight);
previewBorder.CornerRadius({ 6, 6, 6, 6 });
auto bgBrush = Application::Current().Resources().TryLookup(winrt::box_value(L"SystemControlBackgroundChromeMediumBrush"));
if (bgBrush)
{
previewBorder.Background(bgBrush.as<Brush>());
}
else
{
previewBorder.Background(SolidColorBrush{ winrt::Windows::UI::ColorHelper::FromArgb(255, 30, 30, 30) });
}
// Get the tab's content and reparent it with a ScaleTransform
auto tabContent = tab.Content();
if (tabContent)
{
// Save the Width/Height *property* values (likely NaN for auto-sized
// elements). These differ from ActualWidth/Height (the rendered size).
// We need the property values to restore auto-sizing on exit.
const auto origWidthProp = tabContent.Width();
const auto origHeightProp = tabContent.Height();
const auto origRenderTransform = tabContent.RenderTransform();
const auto origRenderTransformOrigin = tabContent.RenderTransformOrigin();
// Use ActualWidth/Height if the content is currently laid out (active tab).
// Inactive tabs aren't in the visual tree and report zero — fall back
// to the reference size from the active tab's content area.
auto layoutWidth = tabContent.ActualWidth();
auto layoutHeight = tabContent.ActualHeight();
if (layoutWidth <= 0 || layoutHeight <= 0)
{
layoutWidth = referenceWidth;
layoutHeight = referenceHeight;
}
auto originalParent = VisualTreeHelper::GetParent(tabContent).try_as<Panel>();
// XAML single-parent rule: remove from current parent first
_DetachContent(tabContent);
// Lock the content to the layout size — this prevents
// TermControl from seeing a resize and reflowing its buffer
if (layoutWidth > 0 && layoutHeight > 0)
{
tabContent.Width(layoutWidth);
tabContent.Height(layoutHeight);
// Calculate uniform scale to fit in preview
const double previewWidth = PreviewCellWidth;
const double previewHeight = PreviewCellHeight;
const double scale = std::min(previewWidth / layoutWidth, previewHeight / layoutHeight);
// RenderTransform is applied AFTER layout — the content still
// thinks it's at its original size
ScaleTransform scaleTransform;
scaleTransform.ScaleX(scale);
scaleTransform.ScaleY(scale);
tabContent.RenderTransform(scaleTransform);
tabContent.RenderTransformOrigin({ 0.0f, 0.0f });
// Use a Canvas so the content is not constrained by the preview
// container's layout. Canvas gives children infinite measure
// space and arranges at desired size.
Canvas canvas;
canvas.Width(previewWidth);
canvas.Height(previewHeight);
// Clip to preview bounds so the scaled content doesn't overflow
RectangleGeometry clipGeometry;
clipGeometry.Rect({ 0, 0, static_cast<float>(previewWidth), static_cast<float>(previewHeight) });
canvas.Clip(clipGeometry);
canvas.Children().Append(tabContent);
// Layer the canvas behind a transparent overlay so
// pointer events never reach the TermControl content.
Grid previewGrid;
previewGrid.Children().Append(canvas);
Border inputOverlay;
inputOverlay.Background(SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() });
previewGrid.Children().Append(inputOverlay);
previewBorder.Child(previewGrid);
}
_reparentedContent.push_back({ tabContent, originalParent, origWidthProp, origHeightProp, origRenderTransform, origRenderTransformOrigin });
}
cellStack.Children().Append(previewBorder);
// Tab title text
TextBlock titleBlock;
titleBlock.Text(tab.Title());
titleBlock.FontSize(14);
titleBlock.Foreground(SolidColorBrush{ winrt::Windows::UI::Colors::White() });
titleBlock.HorizontalAlignment(HorizontalAlignment::Center);
titleBlock.TextTrimming(TextTrimming::CharacterEllipsis);
titleBlock.MaxWidth(PreviewCellWidth);
titleBlock.Margin({ 0, 6, 0, 0 });
cellStack.Children().Append(titleBlock);
outerBorder.Child(cellStack);
// Click handler
outerBorder.PointerPressed([weakThis = get_weak(), index](auto&&, auto&&) {
if (auto self = weakThis.get())
{
self->_selectedIndex = index;
self->_UpdateSelection();
self->_OnItemClicked(index);
}
});
// Hover effect
outerBorder.PointerEntered([weakThis = get_weak(), index](auto&&, auto&&) {
if (auto self = weakThis.get())
{
self->_selectedIndex = index;
self->_UpdateSelection();
}
});
return outerBorder;
}
void OverviewPane::_AddDoubleAnimation(
const Storyboard& storyboard,
const CompositeTransform& target,
const hstring& property,
double from,
double to,
const Duration& duration,
const EasingFunctionBase& easing)
{
DoubleAnimation anim;
anim.From(from);
anim.To(to);
anim.Duration(duration);
anim.EasingFunction(easing);
Storyboard::SetTarget(anim, target);
Storyboard::SetTargetProperty(anim, property);
storyboard.Children().Append(anim);
}
void OverviewPane::_DetachContent(const FrameworkElement& content)
{
// Try removing from various XAML container types
if (auto parent = VisualTreeHelper::GetParent(content))
{
if (auto panel = parent.try_as<Panel>())
{
uint32_t idx;
if (panel.Children().IndexOf(content, idx))
{
panel.Children().RemoveAt(idx);
}
}
else if (auto border = parent.try_as<Border>())
{
border.Child(nullptr);
}
else if (auto contentControl = parent.try_as<ContentControl>())
{
contentControl.Content(nullptr);
}
else if (auto viewbox = parent.try_as<Viewbox>())
{
viewbox.Child(nullptr);
}
}
}
}

View File

@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <limits>
#include <optional>
#include "OverviewPane.g.h"
namespace winrt::TerminalApp::implementation
{
struct OverviewPane : OverviewPaneT<OverviewPane>
{
OverviewPane();
~OverviewPane();
void UpdateTabContent(Windows::Foundation::Collections::IVector<TerminalApp::Tab> tabs, int32_t focusedIndex);
void ClearTabContent();
int32_t SelectedIndex() const;
void SelectedIndex(int32_t value);
bool UseMica() const;
void UseMica(bool value);
// Events
til::typed_event<Windows::Foundation::IInspectable, Windows::Foundation::IReference<int32_t>> TabSelected;
til::typed_event<> Dismissed;
private:
friend struct OverviewPaneT<OverviewPane>; // for Xaml to bind events
void _OnKeyDown(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _OnPreviewKeyDown(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void _OnItemClicked(int32_t index);
void _UpdateSelection();
void _PlayEnterAnimation();
void _PlayExitAnimation(std::function<void()> onComplete = nullptr);
void _StartEnterZoomAnimation();
std::optional<std::tuple<double, double, double>> _GetZoomParamsForCell(int32_t index);
Windows::UI::Xaml::FrameworkElement _BuildPreviewCell(const TerminalApp::Tab& tab, int32_t index, double referenceWidth, double referenceHeight);
void _DetachContent(const Windows::UI::Xaml::FrameworkElement& content);
static void _AddDoubleAnimation(
const Windows::UI::Xaml::Media::Animation::Storyboard& storyboard,
const Windows::UI::Xaml::Media::CompositeTransform& target,
const hstring& property,
double from,
double to,
const Windows::UI::Xaml::Duration& duration,
const Windows::UI::Xaml::Media::Animation::EasingFunctionBase& easing);
int32_t _selectedIndex{ 0 };
int32_t _columnCount{ 3 }; // must match WrapGrid MaximumRowsOrColumns in OverviewPane.xaml
bool _pendingEnterAnimation{ false };
bool _useMica{ false };
void _UpdateBackgroundForMica();
winrt::event_token _exitAnimationToken{};
Windows::UI::Xaml::Media::Animation::Storyboard _exitContentStoryboard{ nullptr };
struct ReparentedEntry
{
Windows::UI::Xaml::FrameworkElement content{ nullptr };
Windows::UI::Xaml::Controls::Panel originalParent{ nullptr };
double originalWidth{ std::numeric_limits<double>::quiet_NaN() };
double originalHeight{ std::numeric_limits<double>::quiet_NaN() };
Windows::UI::Xaml::Media::Transform originalRenderTransform{ nullptr };
Windows::Foundation::Point originalRenderTransformOrigin{ 0.0f, 0.0f };
};
std::vector<ReparentedEntry> _reparentedContent;
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(OverviewPane);
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "Tab.idl";
namespace TerminalApp
{
[default_interface] runtimeclass OverviewPane : Windows.UI.Xaml.Controls.UserControl
{
OverviewPane();
void UpdateTabContent(Windows.Foundation.Collections.IVector<Tab> tabs, Int32 focusedIndex);
void ClearTabContent();
Int32 SelectedIndex;
Boolean UseMica;
event Windows.Foundation.TypedEventHandler<Object, Windows.Foundation.IReference<Int32> > TabSelected;
event Windows.Foundation.TypedEventHandler<Object, Object> Dismissed;
}
}

View File

@@ -0,0 +1,88 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
-->
<UserControl x:Class="TerminalApp.OverviewPane"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:TerminalApp"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
AllowFocusOnInteraction="True"
IsTabStop="True"
KeyDown="_OnKeyDown"
PreviewKeyDown="_OnPreviewKeyDown"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<Storyboard x:Key="BackgroundFadeIn">
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetName="BackgroundOverlay"
Storyboard.TargetProperty="Opacity"
From="0.0"
To="1.0">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Key="BackgroundFadeOut">
<DoubleAnimation Duration="0:0:0.15"
Storyboard.TargetName="BackgroundOverlay"
Storyboard.TargetProperty="Opacity"
From="1.0"
To="0.0">
<DoubleAnimation.EasingFunction>
<QuadraticEase EasingMode="EaseIn" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</ResourceDictionary>
</UserControl.Resources>
<Grid x:Name="RootGrid"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<!-- Semi-transparent dark overlay -->
<Border x:Name="BackgroundOverlay"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource SmokeFillColorDefaultBrush}" />
<!-- Zoom-animated wrapper around the scrollable content -->
<Grid x:Name="ContentWrapper"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<CompositeTransform x:Name="ContentTransform" />
</Grid.RenderTransform>
<ScrollViewer x:Name="PreviewScrollViewer"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<!-- WrapGrid-style layout using ItemsControl with WrapGrid panel -->
<ItemsControl x:Name="PreviewGrid"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid HorizontalChildrenAlignment="Left"
ItemHeight="270"
ItemWidth="332"
MaximumRowsOrColumns="3"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
</Grid>
</UserControl>

View File

@@ -1033,6 +1033,13 @@ namespace winrt::TerminalApp::implementation
{
if (!_rearranging && !_removing)
{
// If the user clicked a tab in the tab row while the overview is
// open, the overview is still holding that tab's Content reparented
// into one of its preview cells. Tear down the overview visuals
// first so _UpdatedSelectedTab can mount the content into the
// active content area without hitting the XAML single-parent rule.
_DismissOverviewVisuals();
auto tabView = sender.as<MUX::Controls::TabView>();
auto selectedIndex = tabView.SelectedIndex();
if (selectedIndex >= 0 && selectedIndex < gsl::narrow_cast<int32_t>(_tabs.Size()))

View File

@@ -71,6 +71,9 @@
<Page Include="CommandPalette.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="OverviewPane.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="SuggestionsControl.xaml">
<SubType>Designer</SubType>
</Page>
@@ -132,6 +135,9 @@
<ClInclude Include="CommandPalette.h">
<DependentUpon>CommandPalette.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="OverviewPane.h">
<DependentUpon>OverviewPane.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="FilteredCommand.h" />
<ClInclude Include="Pane.h" />
<ClInclude Include="../fzf/fzf.h" />
@@ -242,6 +248,9 @@
<ClCompile Include="CommandPalette.cpp">
<DependentUpon>CommandPalette.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="OverviewPane.cpp">
<DependentUpon>OverviewPane.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="FilteredCommand.cpp" />
<ClCompile Include="Pane.cpp" />
<ClCompile Include="Pane.LayoutSizeNode.cpp" />
@@ -352,6 +361,10 @@
<DependentUpon>CommandPalette.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="OverviewPane.idl">
<DependentUpon>OverviewPane.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="SuggestionsControl.idl">
<DependentUpon>SuggestionsControl.xaml</DependentUpon>
<SubType>Code</SubType>

View File

@@ -1786,6 +1786,17 @@ namespace winrt::TerminalApp::implementation
return;
}
// If the overview pane is open, most actions mutate tab state whose
// Content() is currently reparented under the overview. Tear down the
// overview's visuals first so the content is back under its original
// parent before the action runs. ToggleOverview is the exception — it
// handles its own exit.
if (_isInOverviewMode &&
cmd.ActionAndArgs().Action() != ShortcutAction::ToggleOverview)
{
_DismissOverviewVisuals();
}
if (!_actionDispatch->DoAction(cmd.ActionAndArgs()))
{
return;
@@ -4146,6 +4157,120 @@ namespace winrt::TerminalApp::implementation
AlwaysOnTopChanged.raise(*this, nullptr);
}
void TerminalPage::ToggleOverview()
{
if (_isInOverviewMode)
{
_ExitOverview(std::nullopt);
}
else
{
_EnterOverview();
}
}
void TerminalPage::_EnterOverview()
{
_isInOverviewMode = true;
// Use FindName to lazily load the OverviewPane (same pattern as CommandPalette)
auto overview = FindName(L"OverviewPaneElement").try_as<OverviewPane>();
if (!overview)
{
return;
}
const auto focusedIdx = _GetFocusedTabIndex();
const auto idx = focusedIdx.has_value() ? static_cast<int32_t>(focusedIdx.value()) : 0;
// Wire up events to handle tab selection and dismissal
_overviewTabSelectedToken = overview.TabSelected([weakThis = get_weak()](auto&&, const auto& args) {
if (auto self = weakThis.get())
{
if (args)
{
self->_ExitOverview(static_cast<uint32_t>(args.Value()));
}
}
});
_overviewDismissedToken = overview.Dismissed([weakThis = get_weak()](auto&&, auto&&) {
if (auto self = weakThis.get())
{
self->_ExitOverview(std::nullopt);
}
});
// _tabs is already IObservableVector<Tab>, which inherits from IVector<Tab>
const auto theme = _settings.GlobalSettings().CurrentTheme();
const auto windowTheme = theme ? theme.Window() : nullptr;
overview.UseMica(windowTheme ? windowTheme.UseMica() : false);
overview.UpdateTabContent(_tabs, idx);
overview.Visibility(WUX::Visibility::Visible);
overview.Focus(WUX::FocusState::Programmatic);
}
void TerminalPage::_ExitOverview(const std::optional<uint32_t>& selectedIndex)
{
auto overview = FindName(L"OverviewPaneElement").try_as<OverviewPane>();
// Determine which tab to switch to. Prefer the explicitly passed index
// (from TabSelected event), but fall back to the overview pane's
// current selection — this covers ToggleOverview and Dismiss paths so
// the user always lands on whichever tab they navigated to.
std::optional<uint32_t> tabToSelect = selectedIndex;
if (!tabToSelect.has_value() && overview)
{
const auto overviewIdx = overview.SelectedIndex();
if (overviewIdx >= 0 && overviewIdx < static_cast<int32_t>(_tabs.Size()))
{
tabToSelect = static_cast<uint32_t>(overviewIdx);
}
}
_DismissOverviewVisuals();
if (tabToSelect.has_value())
{
_SelectTab(tabToSelect.value());
}
_UpdatedSelectedTab(_GetFocusedTab());
}
// Method Description:
// - Tears down the overview's reparented content and hides the overlay,
// without changing tab selection. Safe to call when not in overview mode.
// - Used by both _ExitOverview (which then selects a tab) and by
// _OnTabSelectionChanged (where the TabView has already updated the
// selection and we just need to release the reparented content before
// _UpdatedSelectedTab tries to mount it back into the content area).
void TerminalPage::_DismissOverviewVisuals()
{
if (!_isInOverviewMode)
{
return;
}
if (auto overview = FindName(L"OverviewPaneElement").try_as<OverviewPane>())
{
// Revoke event handlers to avoid stale callbacks
overview.TabSelected(_overviewTabSelectedToken);
overview.Dismissed(_overviewDismissedToken);
_overviewTabSelectedToken = {};
_overviewDismissedToken = {};
overview.ClearTabContent();
overview.Visibility(WUX::Visibility::Collapsed);
}
_isInOverviewMode = false;
}
bool TerminalPage::OverviewMode() const
{
return _isInOverviewMode;
}
// Method Description:
// - Sets the tab split button color when a new tab color is selected
// Arguments:

View File

@@ -128,9 +128,11 @@ namespace winrt::TerminalApp::implementation
void ToggleFocusMode();
void ToggleFullscreen();
void ToggleAlwaysOnTop();
void ToggleOverview();
bool FocusMode() const;
bool Fullscreen() const;
bool AlwaysOnTop() const;
bool OverviewMode() const;
bool ShowTabsFullscreen() const;
void SetShowTabsFullscreen(bool newShowTabsFullscreen);
void SetFullscreen(bool);
@@ -238,6 +240,9 @@ namespace winrt::TerminalApp::implementation
TerminalApp::Tab _settingsTab{ nullptr };
bool _isInFocusMode{ false };
bool _isInOverviewMode{ false };
winrt::event_token _overviewTabSelectedToken{};
winrt::event_token _overviewDismissedToken{};
bool _isFullscreen{ false };
bool _isMaximized{ false };
bool _isAlwaysOnTop{ false };
@@ -364,6 +369,9 @@ namespace winrt::TerminalApp::implementation
void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference<Microsoft::Terminal::Settings::Model::TabSwitcherMode>& customTabSwitcherMode);
bool _SelectTab(uint32_t tabIndex);
void _EnterOverview();
void _ExitOverview(const std::optional<uint32_t>& selectedIndex);
void _DismissOverviewVisuals();
bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction);
bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args);

View File

@@ -61,6 +61,7 @@ namespace TerminalApp
Boolean FocusMode { get; };
Boolean Fullscreen { get; };
Boolean AlwaysOnTop { get; };
Boolean OverviewMode { get; };
WindowProperties WindowProperties { get; };
void IdentifyWindow();

View File

@@ -160,6 +160,12 @@
</TextBlock>
</ContentDialog>
<local:OverviewPane x:Name="OverviewPaneElement"
Grid.Row="2"
x:Load="False"
PreviewKeyDown="_KeyDownHandler"
Visibility="Collapsed" />
<local:CommandPalette x:Name="CommandPaletteElement"
Grid.Row="2"
VerticalAlignment="Stretch"

View File

@@ -2906,6 +2906,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
else
{
// Yes, this is reachable: when a pane is split, the new TermControl
// is added to the XAML tree before its Loaded event fires, so
// _initializedTerminal is still false when layout queries MinimumSize().
// Return a small fallback — the real size will be used once initialized.
return { 10, 10 };
}
}

View File

@@ -57,6 +57,7 @@ static constexpr std::string_view SuggestionsKey{ "showSuggestions" };
static constexpr std::string_view ToggleFocusModeKey{ "toggleFocusMode" };
static constexpr std::string_view SetFocusModeKey{ "setFocusMode" };
static constexpr std::string_view ToggleFullscreenKey{ "toggleFullscreen" };
static constexpr std::string_view ToggleOverviewKey{ "toggleOverview" };
static constexpr std::string_view SetFullScreenKey{ "setFullScreen" };
static constexpr std::string_view SetMaximizedKey{ "setMaximized" };
static constexpr std::string_view TogglePaneZoomKey{ "togglePaneZoom" };

View File

@@ -59,6 +59,7 @@
ON_ALL_ACTIONS(ToggleShaderEffects) \
ON_ALL_ACTIONS(ToggleFocusMode) \
ON_ALL_ACTIONS(ToggleFullscreen) \
ON_ALL_ACTIONS(ToggleOverview) \
ON_ALL_ACTIONS(ToggleAlwaysOnTop) \
ON_ALL_ACTIONS(OpenSettings) \
ON_ALL_ACTIONS(SetFocusMode) \

View File

@@ -466,6 +466,9 @@
<value>Toggle focus mode</value>
<comment>"Focus mode" is a mode with minimal UI elements, for a distraction-free experience</comment>
</data>
<data name="ToggleOverviewCommandKey" xml:space="preserve">
<value>Toggle overview mode</value>
</data>
<data name="EnableFocusModeCommandKey" xml:space="preserve">
<value>Enable focus mode</value>
</data>

View File

@@ -517,6 +517,7 @@
{ "command": "closeWindow", "id": "Terminal.CloseWindow" },
{ "command": "toggleFullscreen", "id": "Terminal.ToggleFullscreen" },
{ "command": "toggleFocusMode", "id": "Terminal.ToggleFocusMode" },
{ "command": "toggleOverview", "id": "Terminal.ToggleOverview" },
{ "command": "toggleAlwaysOnTop", "id": "Terminal.ToggleAlwaysOnTop" },
{ "command": "openNewTabDropdown", "id": "Terminal.OpenNewTabDropdown" },
{ "command": { "action": "openSettings", "target": "settingsUI" }, "id": "Terminal.OpenSettingsUI" },