Compare commits

...

2 Commits

Author SHA1 Message Date
Mike Griese
0c09178d7a what the hell 2026-03-05 16:34:35 -06:00
Mike Griese
056925eb42 jesus 2026-03-05 13:03:17 -06:00
17 changed files with 775 additions and 40 deletions

View File

@@ -85,7 +85,7 @@
<uap3:Name>com.microsoft.windows.terminal.settings</uap3:Name>
</uap3:AppExtensionHost>
</uap3:Extension>
<uap3:Extension Category="windows.appExtension">
<!-- <uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.windows.console.host"
Id="OpenConsole-Dev"
DisplayName="OpenConsole Dev"
@@ -111,7 +111,7 @@
<com:ComInterface>
<com:ProxyStub Id="DEC4804D-56D1-4F73-9FBE-6828E7C85C56" DisplayName="OpenConsoleHandoffProxy" Path="OpenConsoleProxy.dll"/>
<com:Interface Id="E686C757-9A35-4A1C-B3CE-0BCC8B5C69F4" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/>
<com:Interface Id="6F23DA90-15C5-4203-9DB0-64E73F1B1B00" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/> <!-- ITerminalHandoff3 -->
<com:Interface Id="6F23DA90-15C5-4203-9DB0-64E73F1B1B00" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/>
<com:Interface Id="746E6BC0-AB05-4E38-AB14-71E86763141F" ProxyStubClsid="DEC4804D-56D1-4F73-9FBE-6828E7C85C56"/>
</com:ComInterface>
</com:Extension>
@@ -137,7 +137,7 @@
<desktop5:Verb Id="OpenTerminalDev" Clsid="52065414-e077-47ec-a3ac-1cc5455e1b54" />
</desktop5:ItemType>
</desktop4:FileExplorerContextMenus>
</desktop4:Extension>
</desktop4:Extension> -->
</Extensions>

View File

@@ -245,6 +245,7 @@
</ResourceDictionary>
<ResourceDictionary Source="ms-resource:///Files/TerminalApp/HighlightedTextControlStyle.xaml" />
<ResourceDictionary Source="ms-resource:///Files/TerminalApp/VerticalTabViewStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -442,6 +442,10 @@
<data name="NewTabRun.Text" xml:space="preserve">
<value>Open a new tab</value>
</data>
<data name="TabNewButtonText" xml:space="preserve">
<value>New tab</value>
<comment>Label for the new tab button in vertical tab strip mode.</comment>
</data>
<data name="NewPaneRun.Text" xml:space="preserve">
<value>Alt+Click to split the current window</value>
</data>

View File

@@ -255,9 +255,38 @@ namespace winrt::TerminalApp::implementation
}
if (_tabRow)
{
// collapse/show the row that the tabs are in.
// NaN is the special value XAML uses for "Auto" sizing.
_tabRow.Height(isVisible ? NAN : 0);
if (_tabPosition == Settings::Model::TabPosition::Left ||
_tabPosition == Settings::Model::TabPosition::Right)
{
// For left/right positions, collapse the column width instead of row height.
// Also hide the splitter.
_tabRow.Width(isVisible ? std::numeric_limits<double>::quiet_NaN() : 0);
if (_tabStripSplitter)
{
_tabStripSplitter.Visibility(isVisible ? Visibility::Visible : Visibility::Collapsed);
}
// Collapse or restore the tab strip column
auto tabStripColIdx = (_tabPosition == Settings::Model::TabPosition::Left) ? 0u : 2u;
auto root = this->Root();
if (root.ColumnDefinitions().Size() > tabStripColIdx)
{
auto col = root.ColumnDefinitions().GetAt(tabStripColIdx);
if (isVisible)
{
col.Width(WUX::GridLengthHelper::FromPixels(200));
}
else
{
col.Width(WUX::GridLengthHelper::FromPixels(0));
}
}
}
else
{
// Top/Bottom: collapse/show the row that the tabs are in.
// NaN is the special value XAML uses for "Auto" sizing.
_tabRow.Height(isVisible ? NAN : 0);
}
}
}

View File

@@ -65,6 +65,10 @@
<Type>DefaultStyle</Type>
<SubType>Designer</SubType>
</Page>
<Page Include="VerticalTabViewStyle.xaml">
<Type>DefaultStyle</Type>
<SubType>Designer</SubType>
</Page>
<Page Include="ColorPickupFlyout.xaml">
<SubType>Designer</SubType>
</Page>

View File

@@ -315,6 +315,307 @@ namespace winrt::TerminalApp::implementation
return true;
}
// Method Description:
// - Rearranges the TerminalPage's root grid to place the tab strip at the
// position indicated by the current theme's TabPosition property. For Top,
// the existing behavior is preserved (tabs in titlebar or Row 0). For
// Bottom, the tab row goes in the last row. For Left/Right, an outer
// 3-column grid is created with a resizable splitter between the tab strip
// and the content area.
void TerminalPage::_ApplyTabPosition()
{
// Read the tab position from the theme
if (const auto theme = _settings.GlobalSettings().CurrentTheme())
{
if (const auto window = theme.Window())
{
_tabPosition = window.TabPosition();
}
}
auto root = this->Root();
auto infoBarPanel = this->InfoBarPanel();
switch (_tabPosition)
{
case TabPosition::Top:
{
// Default XAML layout: Row 0=TabRow(Auto), Row 1=InfoBars(Auto), Row 2=TabContent(*)
// If ShowTabsInTitlebar, remove the tab row from the grid and raise SetTitleBarContent
if (_settings.GlobalSettings().ShowTabsInTitlebar())
{
uint32_t index = 0;
if (root.Children().IndexOf(_tabRow, index))
{
root.Children().RemoveAt(index);
}
SetTitleBarContent.raise(*this, _tabRow);
const auto transparent = Media::SolidColorBrush();
transparent.Color(Windows::UI::Colors::Transparent());
_tabRow.Background(transparent);
}
break;
}
case TabPosition::Bottom:
{
// Rearrange to: Row 0=InfoBars(Auto), Row 1=TabContent(*), Row 2=TabRow(Auto)
// Remove only the three layout elements; leave deferred-load stubs
// (CommandPalette, SuggestionsControl, dialogs, etc.) in the tree
// so that FindName() can still locate them later.
uint32_t idx;
if (root.Children().IndexOf(_tabRow, idx))
root.Children().RemoveAt(idx);
if (root.Children().IndexOf(infoBarPanel, idx))
root.Children().RemoveAt(idx);
if (root.Children().IndexOf(_tabContent, idx))
root.Children().RemoveAt(idx);
root.RowDefinitions().Clear();
WUX::Controls::RowDefinition row0;
row0.Height(WUX::GridLengthHelper::FromValueAndType(1, WUX::GridUnitType::Auto));
WUX::Controls::RowDefinition row1;
row1.Height(WUX::GridLengthHelper::FromValueAndType(1, WUX::GridUnitType::Star));
WUX::Controls::RowDefinition row2;
row2.Height(WUX::GridLengthHelper::FromValueAndType(1, WUX::GridUnitType::Auto));
root.RowDefinitions().Append(row0);
root.RowDefinitions().Append(row1);
root.RowDefinitions().Append(row2);
WUX::Controls::Grid::SetRow(infoBarPanel, 0);
WUX::Controls::Grid::SetRow(_tabContent, 1);
WUX::Controls::Grid::SetRow(_tabRow, 2);
// Insert layout elements at the front so overlay elements
// (command palette, dialogs) remain on top in z-order.
root.Children().InsertAt(0, infoBarPanel);
root.Children().InsertAt(1, _tabContent);
root.Children().InsertAt(2, _tabRow);
// Update overlay elements: the ones that had Grid.Row="2" in XAML
// should now target Row 1 (the content area) instead of Row 2
// (which is the tab row in this layout). Iterate the remaining
// children (index 3+) and reassign any that were on row 2.
for (uint32_t i = 3; i < root.Children().Size(); ++i)
{
auto child = root.Children().GetAt(i);
if (const auto& fwe { child.try_as<WUX::FrameworkElement>() })
{
if (WUX::Controls::Grid::GetRow(fwe) == 2)
{
WUX::Controls::Grid::SetRow(fwe, 1);
}
}
}
break;
}
case TabPosition::Left:
case TabPosition::Right:
{
// Build a 3-column layout: [tabstrip | splitter | content] or reversed.
// Remove only the three layout elements; leave deferred-load stubs
// (CommandPalette, SuggestionsControl, dialogs, etc.) in the tree
// so that FindName() can still locate them later.
uint32_t removeIdx;
if (root.Children().IndexOf(_tabRow, removeIdx))
root.Children().RemoveAt(removeIdx);
if (root.Children().IndexOf(infoBarPanel, removeIdx))
root.Children().RemoveAt(removeIdx);
if (root.Children().IndexOf(_tabContent, removeIdx))
root.Children().RemoveAt(removeIdx);
root.RowDefinitions().Clear();
// Create column definitions
WUX::Controls::ColumnDefinition tabStripCol;
tabStripCol.Width(WUX::GridLengthHelper::FromPixels(200));
tabStripCol.MinWidth(100);
tabStripCol.MaxWidth(400);
WUX::Controls::ColumnDefinition splitterCol;
splitterCol.Width(WUX::GridLengthHelper::FromValueAndType(1, WUX::GridUnitType::Auto));
WUX::Controls::ColumnDefinition contentCol;
contentCol.Width(WUX::GridLengthHelper::FromValueAndType(1, WUX::GridUnitType::Star));
// Create internal content grid (infobars + tab content stacked vertically)
WUX::Controls::Grid contentGrid;
WUX::Controls::RowDefinition infoRow;
infoRow.Height(WUX::GridLengthHelper::FromValueAndType(1, WUX::GridUnitType::Auto));
WUX::Controls::RowDefinition mainRow;
mainRow.Height(WUX::GridLengthHelper::FromValueAndType(1, WUX::GridUnitType::Star));
contentGrid.RowDefinitions().Append(infoRow);
contentGrid.RowDefinitions().Append(mainRow);
WUX::Controls::Grid::SetRow(infoBarPanel, 0);
WUX::Controls::Grid::SetRow(_tabContent, 1);
contentGrid.Children().Append(infoBarPanel);
contentGrid.Children().Append(_tabContent);
// Create the splitter border
_tabStripSplitter = WUX::Controls::Border();
_tabStripSplitter.Width(4);
// the BG color will get set in _updatePaneResources
// Use a custom cursor via InputSystemCursorShape
_tabStripSplitter.ManipulationMode(WUX::Input::ManipulationModes::None);
// Wire up pointer events for the splitter
_tabStripSplitter.PointerEntered([](const auto& /*sender*/, const auto&) {
if (const auto coreWindow = Windows::UI::Core::CoreWindow::GetForCurrentThread())
{
coreWindow.PointerCursor(
Windows::UI::Core::CoreCursor{ Windows::UI::Core::CoreCursorType::SizeWestEast, 0 });
}
});
_tabStripSplitter.PointerExited([](const auto& /*sender*/, const auto&) {
if (const auto coreWindow = Windows::UI::Core::CoreWindow::GetForCurrentThread())
{
coreWindow.PointerCursor(
Windows::UI::Core::CoreCursor{ Windows::UI::Core::CoreCursorType::Arrow, 0 });
}
});
_tabStripSplitter.PointerPressed([weakThis{ get_weak() }](const auto& sender, const WUX::Input::PointerRoutedEventArgs& args) {
if (auto page{ weakThis.get() })
{
auto border = sender.template as<WUX::Controls::Border>();
border.CapturePointer(args.Pointer());
page->_splitterDragging = true;
page->_splitterDragStartX = args.GetCurrentPoint(page->Root()).Position().X;
// Find the tab strip column width
auto tabStripColIdx = (page->_tabPosition == TabPosition::Left) ? 0u : 2u;
page->_splitterDragStartWidth = page->Root().ColumnDefinitions().GetAt(tabStripColIdx).ActualWidth();
args.Handled(true);
}
});
_tabStripSplitter.PointerMoved([weakThis{ get_weak() }](const auto& /*sender*/, const WUX::Input::PointerRoutedEventArgs& args) {
if (auto page{ weakThis.get() })
{
if (!page->_splitterDragging)
return;
auto currentX = args.GetCurrentPoint(page->Root()).Position().X;
auto delta = currentX - page->_splitterDragStartX;
auto tabStripColIdx = (page->_tabPosition == TabPosition::Left) ? 0u : 2u;
auto newWidth = (page->_tabPosition == TabPosition::Left) ? (page->_splitterDragStartWidth + delta) : (page->_splitterDragStartWidth - delta);
newWidth = std::clamp(newWidth, 100.0, 400.0);
page->Root().ColumnDefinitions().GetAt(tabStripColIdx).Width(WUX::GridLengthHelper::FromPixels(newWidth));
args.Handled(true);
}
});
_tabStripSplitter.PointerReleased([weakThis{ get_weak() }](const auto& sender, const WUX::Input::PointerRoutedEventArgs& args) {
if (auto page{ weakThis.get() })
{
if (page->_splitterDragging)
{
page->_splitterDragging = false;
auto border = sender.template as<WUX::Controls::Border>();
border.ReleasePointerCapture(args.Pointer());
args.Handled(true);
}
}
});
if (_tabPosition == TabPosition::Left)
{
root.ColumnDefinitions().Append(tabStripCol);
root.ColumnDefinitions().Append(splitterCol);
root.ColumnDefinitions().Append(contentCol);
WUX::Controls::Grid::SetColumn(_tabRow, 0);
WUX::Controls::Grid::SetColumn(_tabStripSplitter, 1);
WUX::Controls::Grid::SetColumn(contentGrid, 2);
}
else // Right
{
root.ColumnDefinitions().Append(contentCol);
root.ColumnDefinitions().Append(splitterCol);
root.ColumnDefinitions().Append(tabStripCol);
WUX::Controls::Grid::SetColumn(contentGrid, 0);
WUX::Controls::Grid::SetColumn(_tabStripSplitter, 1);
WUX::Controls::Grid::SetColumn(_tabRow, 2);
}
// Clear any row assignments from XAML
WUX::Controls::Grid::SetRow(_tabRow, 0);
// Insert layout elements at the front so overlay elements
// (command palette, dialogs) remain on top in z-order.
root.Children().InsertAt(0, _tabRow);
root.Children().InsertAt(1, _tabStripSplitter);
root.Children().InsertAt(2, contentGrid);
// Update remaining overlay children (deferred-load stubs for
// CommandPalette, SuggestionsControl, dialogs, InfoBars,
// TeachingTips, etc.) so they span all 3 columns and cover the
// full width of the page, not just the tab strip column.
for (uint32_t i = 3; i < root.Children().Size(); ++i)
{
auto child = root.Children().GetAt(i);
if (const auto& fwe { child.try_as<WUX::FrameworkElement>() })
{
WUX::Controls::Grid::SetColumn(fwe, 0);
WUX::Controls::Grid::SetColumnSpan(fwe, 3);
WUX::Controls::Grid::SetRow(fwe, 0);
}
}
// Apply vertical styles to the TabView from our resource dictionary
if (const auto res = Application::Current().Resources())
{
if (const auto verticalTabViewStyle = res.Lookup(winrt::box_value(L"VerticalTabViewStyle")))
{
_tabView.Style(verticalTabViewStyle.as<WUX::Style>());
}
// Apply the vertical TabViewItem style as an implicit style
// on the TabRow's resources so new tab items pick it up.
if (const auto verticalItemStyle = res.Lookup(winrt::box_value(L"VerticalTabViewItemStyle")))
{
auto tabRowResources = _tabRow.Resources();
if (!tabRowResources)
{
tabRowResources = WUX::ResourceDictionary{};
_tabRow.Resources(tabRowResources);
}
auto itemStyleType = winrt::xaml_typename<MUX::Controls::TabViewItem>();
tabRowResources.Insert(winrt::box_value(itemStyleType), verticalItemStyle);
}
}
// Bug fix: TabRowControl.xaml sets VerticalAlignment="Bottom" on
// the TabView for the default horizontal mode. Override to Top so
// tabs start at the top of the vertical strip.
_tabView.VerticalAlignment(WUX::VerticalAlignment::Top);
// Bug fix: In vertical mode, give the "new tab" button a text
// label ("New tab") next to its "+" icon so it reads naturally
// in the wider sidebar.
if (_newTabButton)
{
auto panel = WUX::Controls::StackPanel();
panel.Orientation(WUX::Controls::Orientation::Horizontal);
panel.Spacing(8);
WUX::Controls::FontIcon plusIcon{};
plusIcon.Glyph(L"\uE710");
plusIcon.FontFamily(WUX::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
plusIcon.FontSize(12);
panel.Children().Append(plusIcon);
WUX::Controls::TextBlock label{};
label.Text(RS_(L"TabNewButtonText"));
label.VerticalAlignment(WUX::VerticalAlignment::Center);
label.FontFamily(WUX::Media::FontFamily{ L"Segoe UI" });
panel.Children().Append(label);
_newTabButton.Content(panel);
}
break;
}
}
}
void TerminalPage::Create()
{
// Hookup the key bindings
@@ -335,37 +636,10 @@ namespace winrt::TerminalApp::implementation
auto tabRowImpl = winrt::get_self<implementation::TabRowControl>(_tabRow);
_newTabButton = tabRowImpl->NewTabButton();
if (_settings.GlobalSettings().ShowTabsInTitlebar())
{
// Remove the TabView from the page. We'll hang on to it, we need to
// put it in the titlebar.
uint32_t index = 0;
if (this->Root().Children().IndexOf(_tabRow, index))
{
this->Root().Children().RemoveAt(index);
}
// Apply the tab position from the theme. This rearranges the grid
// layout and potentially moves the tab row into the titlebar (for Top).
_ApplyTabPosition();
// Inform the host that our titlebar content has changed.
SetTitleBarContent.raise(*this, _tabRow);
// GH#13143 Manually set the tab row's background to transparent here.
//
// We're doing it this way because ThemeResources are tricky. We
// default in XAML to using the appropriate ThemeResource background
// color for our TabRow. When tabs in the titlebar are _disabled_,
// this will ensure that the tab row has the correct theme-dependent
// value. When tabs in the titlebar are _enabled_ (the default),
// we'll switch the BG to Transparent, to let the Titlebar Control's
// background be used as the BG for the tab row.
//
// We can't do it the other way around (default to Transparent, only
// switch to a color when disabling tabs in the titlebar), because
// looking up the correct ThemeResource from and App dictionary is a
// capital-H Hard problem.
const auto transparent = Media::SolidColorBrush();
transparent.Color(Windows::UI::Colors::Transparent());
_tabRow.Background(transparent);
}
_updateThemeColors();
// Initialize the state of the CloseButtonOverlayMode property of
@@ -4951,7 +5225,7 @@ namespace winrt::TerminalApp::implementation
TitlebarBrush(backgroundSolidBrush);
}
if (!_settings.GlobalSettings().ShowTabsInTitlebar())
if (_tabPosition != TabPosition::Top || !_settings.GlobalSettings().ShowTabsInTitlebar())
{
_tabRow.Background(TitlebarBrush());
}
@@ -5022,6 +5296,7 @@ namespace winrt::TerminalApp::implementation
// will eat focus.
_paneResources.focusedBorderBrush = SolidColorBrush{ Colors::Black() };
}
_tabStripSplitter.Background(_paneResources.focusedBorderBrush);
const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush");
if (res.HasKey(unfocusedBorderBrushKey))

View File

@@ -227,6 +227,9 @@ namespace winrt::TerminalApp::implementation
Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr };
winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr };
Microsoft::Terminal::Settings::Model::TabPosition _tabPosition{ Microsoft::Terminal::Settings::Model::TabPosition::Top };
Windows::UI::Xaml::Controls::Border _tabStripSplitter{ nullptr };
Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr };
Windows::Foundation::Collections::IObservableVector<TerminalApp::Tab> _tabs;
@@ -243,6 +246,11 @@ namespace winrt::TerminalApp::implementation
bool _isAlwaysOnTop{ false };
bool _showTabsFullscreen{ false };
// Splitter drag state for left/right tab positions
bool _splitterDragging{ false };
double _splitterDragStartX{ 0.0 };
double _splitterDragStartWidth{ 0.0 };
std::optional<uint32_t> _loadFromPersistedLayoutIdx{};
bool _rearranging{ false };
@@ -342,6 +350,7 @@ namespace winrt::TerminalApp::implementation
void _UpdateTabIcon(Tab& tab);
void _UpdateTabView();
void _UpdateTabWidthMode();
void _ApplyTabPosition();
void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance);
void _DuplicateFocusedTab();

View File

@@ -25,7 +25,8 @@
Grid.Row="0"
KeyUp="_KeyDownHandler" />
<StackPanel Grid.Row="1"
<StackPanel x:Name="InfoBarPanel"
Grid.Row="1"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mux:InfoBar x:Name="KeyboardServiceWarningInfoBar"
x:Load="False"
@@ -84,29 +85,34 @@
<local:AboutDialog x:Name="AboutDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False" />
<ContentDialog x:Name="QuitDialog"
x:Uid="QuitDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False"
DefaultButton="Primary" />
<ContentDialog x:Name="CloseAllDialog"
x:Uid="CloseAllDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False"
DefaultButton="Primary" />
<ContentDialog x:Name="CloseReadOnlyDialog"
x:Uid="CloseReadOnlyDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False"
DefaultButton="Close" />
<ContentDialog x:Name="MultiLinePasteDialog"
x:Uid="MultiLinePasteDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False"
DefaultButton="Primary">
<StackPanel>
@@ -127,12 +133,14 @@
<ContentDialog x:Name="LargePasteDialog"
x:Uid="LargePasteDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False"
DefaultButton="Primary" />
<ContentDialog x:Name="ControlNoticeDialog"
x:Uid="ControlNoticeDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False"
DefaultButton="Primary">
<TextBlock IsTextSelectionEnabled="True"
@@ -147,6 +155,7 @@
<ContentDialog x:Name="CouldNotOpenUriDialog"
x:Uid="CouldNotOpenUriDialog"
Grid.Row="2"
Grid.ColumnSpan="3"
x:Load="False"
DefaultButton="Primary">
<TextBlock IsTextSelectionEnabled="True"
@@ -162,6 +171,7 @@
<local:CommandPalette x:Name="CommandPaletteElement"
Grid.Row="2"
Grid.ColumnSpan="3"
VerticalAlignment="Stretch"
x:Load="False"
PreviewKeyDown="_KeyDownHandler"
@@ -169,6 +179,7 @@
<local:SuggestionsControl x:Name="SuggestionsElement"
Grid.Row="2"
Grid.ColumnSpan="3"
HorizontalAlignment="Left"
VerticalAlignment="Top"
x:Load="False"

View File

@@ -270,6 +270,16 @@ namespace winrt::TerminalApp::implementation
return _settings.GlobalSettings().ShowTabsInTitlebar();
}
Microsoft::Terminal::Settings::Model::TabPosition TerminalWindow::GetTabPosition()
{
auto theme = Theme();
if (auto window = theme.Window())
{
return window.TabPosition();
}
return Microsoft::Terminal::Settings::Model::TabPosition::Top;
}
bool TerminalWindow::GetInitialAlwaysOnTop()
{
return _settings.GlobalSettings().AlwaysOnTop();

View File

@@ -106,6 +106,7 @@ namespace winrt::TerminalApp::implementation
winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme();
Microsoft::Terminal::Settings::Model::LaunchMode GetLaunchMode();
bool GetShowTabsInTitlebar();
Microsoft::Terminal::Settings::Model::TabPosition GetTabPosition();
bool GetInitialAlwaysOnTop();
bool GetInitialShowTabsFullscreen();
float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;

View File

@@ -84,6 +84,7 @@ namespace TerminalApp
Windows.UI.Xaml.ElementTheme GetRequestedTheme();
Microsoft.Terminal.Settings.Model.LaunchMode GetLaunchMode();
Boolean GetShowTabsInTitlebar();
Microsoft.Terminal.Settings.Model.TabPosition GetTabPosition();
Boolean GetInitialAlwaysOnTop();
Boolean GetInitialShowTabsFullscreen();
Single CalcSnappedDimension(Boolean widthOrHeight, Single dimension);

View File

@@ -0,0 +1,268 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information.
Retemplated styles for vertical (Left/Right) tab strip orientation.
These styles override the horizontal TabView layout to stack tabs vertically
in a sidebar configuration.
-->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mux="using:Microsoft.UI.Xaml.Controls"
xmlns:primitives="using:Microsoft.UI.Xaml.Controls.Primitives">
<!--
VerticalTabViewListViewStyle:
Override the ItemsPanel to use vertical orientation and switch scrolling
from horizontal to vertical.
-->
<Style x:Key="VerticalTabViewListViewStyle"
TargetType="primitives:TabViewListView">
<Setter Property="TabNavigation" Value="Once" />
<Setter Property="IsItemClickEnabled" Value="True" />
<Setter Property="IsSwipeEnabled" Value="False" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollMode" Value="Enabled" />
<Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="False" />
<Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="True" />
<Setter Property="SingleSelectionFollowsFocus" Value="False" />
<Setter Property="ItemContainerTransitions">
<Setter.Value>
<TransitionCollection>
<AddDeleteThemeTransition />
<ContentThemeTransition />
<ReorderThemeTransition />
</TransitionCollection>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
VerticalTabViewStyle:
Retemplate the TabView control for a vertical sidebar layout.
Instead of 4 columns in TabContainerGrid, we use 3 rows:
Row 0 (Auto): Header (elevation shield)
Row 1 (*): Tab list (fills vertical space)
Row 2 (Auto): Footer (new tab button)
The TabContentPresenter is removed — WT doesn't use it.
-->
<Style x:Key="VerticalTabViewStyle"
TargetType="mux:TabView">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="TabNavigation" Value="Once" />
<Setter Property="IsAddTabButtonVisible" Value="False" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="mux:TabView">
<Grid x:Name="TabContainerGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header (elevation shield, etc.) -->
<ContentPresenter x:Name="LeftContentPresenter"
Grid.Row="0"
Content="{TemplateBinding TabStripHeader}"
ContentTemplate="{TemplateBinding TabStripHeaderTemplate}" />
<!-- Tab list -->
<primitives:TabViewListView x:Name="TabListView"
Grid.Row="1"
AllowDrop="{TemplateBinding AllowDropTabs}"
CanDragItems="{TemplateBinding CanDragTabs}"
CanReorderItems="{TemplateBinding CanReorderTabs}"
ItemTemplate="{TemplateBinding TabItemTemplate}"
ItemTemplateSelector="{TemplateBinding TabItemTemplateSelector}"
ItemsSource="{TemplateBinding TabItemsSource}"
Style="{StaticResource VerticalTabViewListViewStyle}" />
<!-- Footer (new tab button) -->
<ContentPresenter x:Name="RightContentPresenter"
Grid.Row="2"
Content="{TemplateBinding TabStripFooter}"
ContentTemplate="{TemplateBinding TabStripFooterTemplate}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
VerticalTabViewItemStyle:
Simplified TabViewItem template for vertical tabs.
Removes curved corner arcs, replaces with rounded Border and a
left-edge accent bar for the selected tab. Tabs fill strip width.
-->
<Style x:Key="VerticalTabViewItemStyle"
TargetType="mux:TabViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="Padding" Value="8,6,8,6" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="mux:TabViewItem">
<Grid x:Name="LayoutRoot"
Margin="2,1,2,1">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Target="TabContainer.Background" Value="Transparent" />
<Setter Target="SelectedIndicator.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="TabContainer.Background" Value="{ThemeResource TabViewItemHeaderBackgroundPointerOver}" />
<Setter Target="SelectedIndicator.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="TabContainer.Background" Value="{ThemeResource TabViewItemHeaderBackgroundPressed}" />
<Setter Target="SelectedIndicator.Opacity" Value="0" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Target="TabContainer.Background" Value="{ThemeResource TabViewItemHeaderBackgroundSelected}" />
<Setter Target="SelectedIndicator.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOverSelected">
<VisualState.Setters>
<Setter Target="TabContainer.Background" Value="{ThemeResource TabViewItemHeaderBackgroundSelected}" />
<Setter Target="SelectedIndicator.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PressedSelected">
<VisualState.Setters>
<Setter Target="TabContainer.Background" Value="{ThemeResource TabViewItemHeaderBackgroundPressed}" />
<Setter Target="SelectedIndicator.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="CloseButtonOverlayModeStates">
<VisualState x:Name="CloseButtonVisible" />
<VisualState x:Name="CloseButtonCollapsed">
<VisualState.Setters>
<Setter Target="CloseButton.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="TabViewWidthModeStates">
<!-- In vertical mode, tabs always stretch to full width -->
<VisualState x:Name="StandardWidth" />
<VisualState x:Name="Compact" />
</VisualStateGroup>
<VisualStateGroup x:Name="IconStates">
<VisualState x:Name="Icon" />
<VisualState x:Name="NoIcon">
<VisualState.Setters>
<Setter Target="IconBox.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DisabledStates">
<VisualState x:Name="Enabled" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="LayoutRoot.Opacity" Value="{ThemeResource ListViewItemDisabledThemeOpacity}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- Selected tab background -->
<Border x:Name="TabContainer"
Background="Transparent"
CornerRadius="{ThemeResource ControlCornerRadius}"
Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="IconColumn"
Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Tab icon -->
<Viewbox x:Name="IconBox"
Grid.Column="0"
MaxWidth="16"
MaxHeight="16"
Margin="0,0,8,0">
<ContentControl x:Name="IconControl"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TabViewTemplateSettings.IconElement}"
IsTabStop="False" />
</Viewbox>
<!-- Tab title -->
<ContentPresenter x:Name="ContentPresenter"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
FontWeight="{TemplateBinding FontWeight}"
TextWrapping="NoWrap" />
<!-- Close button -->
<Button x:Name="CloseButton"
Grid.Column="2"
Width="20"
Height="20"
Margin="4,0,0,0"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Background="Transparent"
BorderThickness="0"
Content="&#xE711;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="10"
IsTextScaleFactorEnabled="False" />
</Grid>
</Border>
<!-- Left-edge accent bar for selected tab -->
<Border x:Name="SelectedIndicator"
Width="3"
Height="16"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="2"
Opacity="0" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -158,7 +158,8 @@ Author(s):
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, Frame, "frame", nullptr) \
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedFrame, "unfocusedFrame", nullptr) \
X(bool, RainbowFrame, "experimental.rainbowFrame", false) \
X(bool, UseMica, "useMica", false)
X(bool, UseMica, "useMica", false) \
X(winrt::Microsoft::Terminal::Settings::Model::TabPosition, TabPosition, "tabPosition", winrt::Microsoft::Terminal::Settings::Model::TabPosition::Top)
#define MTSM_THEME_SETTINGS_SETTINGS(X) \
X(winrt::Windows::UI::Xaml::ElementTheme, RequestedTheme, "theme", winrt::Windows::UI::Xaml::ElementTheme::Default)

View File

@@ -673,6 +673,16 @@ JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::IconStyle)
};
};
JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::TabPosition)
{
JSON_MAPPINGS(4) = {
pair_type{ "top", ValueType::Top },
pair_type{ "bottom", ValueType::Bottom },
pair_type{ "left", ValueType::Left },
pair_type{ "right", ValueType::Right },
};
};
// Possible ScrollToMarkDirection values
JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Control::ScrollToMarkDirection)
{

View File

@@ -11,6 +11,14 @@ namespace Microsoft.Terminal.Settings.Model
Monochrome
};
enum TabPosition
{
Top,
Bottom,
Left,
Right
};
enum ThemeColorType
{
Accent,
@@ -64,6 +72,7 @@ namespace Microsoft.Terminal.Settings.Model
Boolean RainbowFrame { get; };
ThemeColor Frame { get; };
ThemeColor UnfocusedFrame { get; };
TabPosition TabPosition { get; };
}
runtimeclass TabRowTheme {

View File

@@ -28,6 +28,7 @@ namespace SettingsModelUnitTests
TEST_METHOD(ParseNullWindowTheme);
TEST_METHOD(ParseThemeWithNullThemeColor);
TEST_METHOD(InvalidCurrentTheme);
TEST_METHOD(ParseTabPosition);
static Core::Color rgb(uint8_t r, uint8_t g, uint8_t b) noexcept
{
@@ -265,4 +266,103 @@ namespace SettingsModelUnitTests
throw e;
}
}
void ThemeTests::ParseTabPosition()
{
Log::Comment(L"Verify tabPosition round-trips through JSON serialization.");
// Test each of the four tab positions
static constexpr std::string_view leftTheme{ R"({
"name": "leftTabs",
"window":
{
"tabPosition": "left"
}
})" };
static constexpr std::string_view rightTheme{ R"({
"name": "rightTabs",
"window":
{
"tabPosition": "right"
}
})" };
static constexpr std::string_view bottomTheme{ R"({
"name": "bottomTabs",
"window":
{
"tabPosition": "bottom"
}
})" };
static constexpr std::string_view topTheme{ R"({
"name": "topTabs",
"window":
{
"tabPosition": "top"
}
})" };
// Test default (no tabPosition specified)
static constexpr std::string_view defaultTheme{ R"({
"name": "defaultTabs",
"window":
{
"useMica": false
}
})" };
// Parse and verify "left"
{
const auto schemeObject = VerifyParseSucceeded(leftTheme);
auto theme = Theme::FromJson(schemeObject);
VERIFY_ARE_EQUAL(L"leftTabs", theme->Name());
VERIFY_IS_NOT_NULL(theme->Window());
VERIFY_ARE_EQUAL(Settings::Model::TabPosition::Left, theme->Window().TabPosition());
// Re-serialize and verify the key is present
const auto reJson = theme->ToJson();
VERIFY_IS_TRUE(reJson.isMember("window"));
VERIFY_IS_TRUE(reJson["window"].isMember("tabPosition"));
VERIFY_ARE_EQUAL("left", reJson["window"]["tabPosition"].asString());
}
// Parse and verify "right"
{
const auto schemeObject = VerifyParseSucceeded(rightTheme);
auto theme = Theme::FromJson(schemeObject);
VERIFY_ARE_EQUAL(Settings::Model::TabPosition::Right, theme->Window().TabPosition());
const auto reJson = theme->ToJson();
VERIFY_ARE_EQUAL("right", reJson["window"]["tabPosition"].asString());
}
// Parse and verify "bottom"
{
const auto schemeObject = VerifyParseSucceeded(bottomTheme);
auto theme = Theme::FromJson(schemeObject);
VERIFY_ARE_EQUAL(Settings::Model::TabPosition::Bottom, theme->Window().TabPosition());
const auto reJson = theme->ToJson();
VERIFY_ARE_EQUAL("bottom", reJson["window"]["tabPosition"].asString());
}
// Parse and verify "top"
{
const auto schemeObject = VerifyParseSucceeded(topTheme);
auto theme = Theme::FromJson(schemeObject);
VERIFY_ARE_EQUAL(Settings::Model::TabPosition::Top, theme->Window().TabPosition());
const auto reJson = theme->ToJson();
VERIFY_ARE_EQUAL("top", reJson["window"]["tabPosition"].asString());
}
// Parse default — should be Top
{
const auto schemeObject = VerifyParseSucceeded(defaultTheme);
auto theme = Theme::FromJson(schemeObject);
VERIFY_ARE_EQUAL(Settings::Model::TabPosition::Top, theme->Window().TabPosition());
}
}
}

View File

@@ -51,7 +51,9 @@ AppHost::AppHost(WindowEmperor* manager, const winrt::TerminalApp::AppLogic& log
_HandleCommandlineArgs(args);
// _HandleCommandlineArgs will create a _windowLogic
_useNonClientArea = _windowLogic.GetShowTabsInTitlebar();
const auto tabPos = _windowLogic.GetTabPosition();
_useNonClientArea = (tabPos == winrt::Microsoft::Terminal::Settings::Model::TabPosition::Top) &&
_windowLogic.GetShowTabsInTitlebar();
if (_useNonClientArea)
{