From 44456be4eb65efc768f4122e53b3ba67c759c177 Mon Sep 17 00:00:00 2001 From: sagarbhure-msft Date: Fri, 3 Apr 2026 23:16:58 +0530 Subject: [PATCH 1/2] Add per-pane title header for split panes (#4717) Shows a thin title bar above each pane when multiple panes are open. Headers display the pane title, update dynamically via Dispatcher when the shell changes the title (e.g. via escape sequences), and use the focus-state border color as background. Hidden when only one pane exists. The header is placed directly in the pane root Grid (row 0, auto-sized) with the content border in row 1 (star-sized), keeping the TermControl as the direct child of the border so SwapChainPanel renders correctly. --- src/cascadia/TerminalApp/Pane.cpp | 109 ++++++++++++++++++++++++++++-- src/cascadia/TerminalApp/Pane.h | 8 +++ src/cascadia/TerminalApp/Tab.cpp | 4 ++ 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index ed02e5250e..ad7a56a3ce 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -31,10 +31,14 @@ Pane::Pane(IPaneContent content, const bool lastFocused) : _lastActive{ lastFocused } { _setPaneContent(std::move(content)); - _root.Children().Append(_borderFirst); + _CreatePaneHeader(); const auto& control{ _content.GetRoot() }; - _borderFirst.Child(control); + + // Set up leaf layout: header in _root row 0, content in _borderFirst row 1. + // The TermControl stays as the direct child of _borderFirst (no Grid wrapper) + // so the SwapChainPanel renders correctly. + _SetupLeafLayout(control); // Register an event with the control to have it inform us when it gains focus. if (control) @@ -1228,6 +1232,12 @@ void Pane::UpdateVisuals() const auto& brush{ _ComputeBorderColor() }; _borderFirst.BorderBrush(brush); _borderSecond.BorderBrush(brush); + + // Update pane header color to match focus state + if (_paneHeaderBorder && _paneHeaderBorder.Visibility() == winrt::Windows::UI::Xaml::Visibility::Visible) + { + _paneHeaderBorder.Background(brush); + } } // Method Description: @@ -1450,9 +1460,9 @@ void Pane::_CloseChild(const bool closeFirst) _root.RowDefinitions().Clear(); // Reattach the TermControl to our grid. - _root.Children().Append(_borderFirst); + _CreatePaneHeader(); const auto& control{ _content.GetRoot() }; - _borderFirst.Child(control); + _SetupLeafLayout(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 @@ -1755,7 +1765,92 @@ void Pane::_setPaneContent(IPaneContent content) } // Method Description: -// - Sets up row/column definitions for this pane. There are three total +// - Creates the pane header UI elements (title bar shown above the content). +// The header is initially collapsed and only shown via ShowPaneHeaders(). +void Pane::_CreatePaneHeader() +{ + namespace WUX = winrt::Windows::UI::Xaml; + + _paneHeaderText = Controls::TextBlock{}; + _paneHeaderText.FontSize(12); + _paneHeaderText.Padding({ 8, 2, 8, 2 }); + _paneHeaderText.IsTextSelectionEnabled(false); + _paneHeaderText.TextTrimming(WUX::TextTrimming::CharacterEllipsis); + if (_content) + { + _paneHeaderText.Text(_content.Title()); + _titleChangedRevoker = _content.TitleChanged(winrt::auto_revoke, [this](auto&&, auto&&) { + _paneHeaderBorder.Dispatcher().RunAsync( + winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, + [this]() { + if (_content && _paneHeaderText) + { + _paneHeaderText.Text(_content.Title()); + } + }); + }); + } + + _paneHeaderBorder = Controls::Border{}; + _paneHeaderBorder.Padding({ 0, 0, 0, 0 }); + _paneHeaderBorder.Child(_paneHeaderText); + _paneHeaderBorder.Visibility(WUX::Visibility::Collapsed); +} + +// Method Description: +// - Sets up the leaf pane layout in _root: a header row (auto-sized) and a +// content row (star-sized). The TermControl stays as the direct child of +// _borderFirst so the SwapChainPanel renders correctly. +void Pane::_SetupLeafLayout(const winrt::Windows::UI::Xaml::UIElement& control) +{ + auto headerRow = Controls::RowDefinition{}; + headerRow.Height(GridLengthHelper::Auto()); + auto contentRow = Controls::RowDefinition{}; + contentRow.Height(GridLengthHelper::FromValueAndType(1, GridUnitType::Star)); + _root.RowDefinitions().Append(headerRow); + _root.RowDefinitions().Append(contentRow); + + Controls::Grid::SetRow(_paneHeaderBorder, 0); + Controls::Grid::SetRow(_borderFirst, 1); + + _root.Children().Append(_paneHeaderBorder); + _root.Children().Append(_borderFirst); + + if (control) + { + _borderFirst.Child(control); + } +} + +// Method Description: +// - Show or hide the pane header title bar on all leaf panes in the tree. +// Called by Tab when the number of panes changes. +void Pane::ShowPaneHeaders(bool show) +{ + if (_IsLeaf()) + { + if (_paneHeaderBorder) + { + namespace WUX = winrt::Windows::UI::Xaml; + _paneHeaderBorder.Visibility(show ? WUX::Visibility::Visible : WUX::Visibility::Collapsed); + + if (show) + { + const auto& brush = _ComputeBorderColor(); + _paneHeaderBorder.Background(brush); + _paneHeaderText.Foreground(winrt::Windows::UI::Xaml::Media::SolidColorBrush(winrt::Windows::UI::Colors::White())); + } + } + } + else + { + _firstChild->ShowPaneHeaders(show); + _secondChild->ShowPaneHeaders(show); + } +} + +// Method Description: +// - Sets up row/column definitions for this pane.There are three total // row/cols. The middle one is for the separator. The first and third are for // each of the child panes, and are given a size in pixels, based off the // available space, and the percent of the space they respectively consume, @@ -2321,6 +2416,10 @@ std::pair, std::shared_ptr> Pane::_Split(SplitDirect _root.RowDefinitions().Clear(); _CreateRowColDefinitions(); + // Reset Grid.Row on _borderFirst — it may have been set to row 1 in the + // leaf layout (header=row0, content=row1). + Controls::Grid::SetRow(_borderFirst, 0); + _borderFirst.Child(_firstChild->GetRootElement()); _borderSecond.Child(_secondChild->GetRootElement()); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index ecc81fad82..ae61334e42 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -150,6 +150,7 @@ public: bool ContainsReadOnly() const; void EnableBroadcast(bool enabled); + void ShowPaneHeaders(bool show); void BroadcastKey(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl, const WORD vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown); void BroadcastChar(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl, const wchar_t vkey, const WORD scanCode, const winrt::Microsoft::Terminal::Core::ControlKeyStates modifiers); void BroadcastString(const winrt::Microsoft::Terminal::Control::TermControl& sourceControl, const winrt::hstring& text); @@ -235,6 +236,11 @@ private: winrt::Windows::UI::Xaml::Controls::Border _borderFirst{}; winrt::Windows::UI::Xaml::Controls::Border _borderSecond{}; + // Per-pane title header (visible when there are split panes) + winrt::Windows::UI::Xaml::Controls::Border _paneHeaderBorder{ nullptr }; + winrt::Windows::UI::Xaml::Controls::TextBlock _paneHeaderText{ nullptr }; + winrt::TerminalApp::IPaneContent::TitleChanged_revoker _titleChangedRevoker; + PaneResources _themeResources; #pragma region Properties that need to be transferred between child / parent panes upon splitting / closing @@ -266,6 +272,8 @@ private: void _SetupChildCloseHandlers(); winrt::TerminalApp::IPaneContent _takePaneContent(); void _setPaneContent(winrt::TerminalApp::IPaneContent content); + void _CreatePaneHeader(); + void _SetupLeafLayout(const winrt::Windows::UI::Xaml::UIElement& control); bool _HasChild(const std::shared_ptr child); winrt::TerminalApp::TerminalPaneContent _getTerminalContent() const; diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 4bbf58e50a..639f85b20e 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -646,6 +646,9 @@ namespace winrt::TerminalApp::implementation // After split, Close Pane Menu Item should be visible _closePaneMenuItem.Visibility(WUX::Visibility::Visible); + // Show pane headers now that we have multiple panes + _rootPane->ShowPaneHeaders(true); + // The active pane has an id if it is a leaf if (activePaneId) { @@ -1324,6 +1327,7 @@ namespace winrt::TerminalApp::implementation if (_rootPane->GetLeafPaneCount() == 1) { _closePaneMenuItem.Visibility(WUX::Visibility::Collapsed); + _rootPane->ShowPaneHeaders(false); } _RecalculateAndApplyReadOnly(); From 527922664670492d90fdb909f792380d71e808f2 Mon Sep 17 00:00:00 2001 From: sagarbhure-msft Date: Tue, 7 Apr 2026 22:12:09 +0530 Subject: [PATCH 2/2] Add showPaneHeaders global appearance setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 'showPaneHeaders' boolean setting (default: true) to control whether per-pane title headers are displayed when multiple panes are open. The setting is available in Settings > Appearance as a toggle and persists via settings.json. Headers are refreshed live when the setting is changed — toggling off hides headers on all existing panes immediately. --- src/cascadia/TerminalApp/Tab.cpp | 13 +++++++++++-- .../TerminalSettingsEditor/GlobalAppearance.xaml | 7 +++++++ .../GlobalAppearanceViewModel.h | 1 + .../GlobalAppearanceViewModel.idl | 1 + .../Resources/en-US/Resources.resw | 8 ++++++++ .../TerminalSettingsModel/GlobalAppSettings.idl | 1 + src/cascadia/TerminalSettingsModel/MTSMSettings.h | 3 ++- 7 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 639f85b20e..512fea378c 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -361,6 +361,10 @@ namespace winrt::TerminalApp::implementation // The tabWidthMode may have changed, update the header control accordingly _UpdateHeaderControlMaxWidth(); + // Refresh pane header visibility based on the current setting + const auto showHeaders = settings.GlobalSettings().ShowPaneHeaders() && _rootPane->GetLeafPaneCount() > 1; + _rootPane->ShowPaneHeaders(showHeaders); + // Update the settings on all our panes. _rootPane->WalkTree([&](const auto& pane) { pane->UpdateSettings(settings); @@ -646,8 +650,13 @@ namespace winrt::TerminalApp::implementation // After split, Close Pane Menu Item should be visible _closePaneMenuItem.Visibility(WUX::Visibility::Visible); - // Show pane headers now that we have multiple panes - _rootPane->ShowPaneHeaders(true); + // Show pane headers now that we have multiple panes (if the setting is enabled) + try + { + const auto settings{ winrt::TerminalApp::implementation::AppLogic::CurrentAppSettings() }; + _rootPane->ShowPaneHeaders(settings.GlobalSettings().ShowPaneHeaders()); + } + CATCH_LOG(); // The active pane has an id if it is a leaf if (activePaneId) diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml index 70fc972a53..23e4040030 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearance.xaml @@ -75,6 +75,13 @@ Style="{StaticResource ToggleSwitchInExpanderStyle}" /> + + + + + diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.h b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.h index 108636a747..dc758cae66 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.h +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.h @@ -33,6 +33,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, AlwaysShowTabs); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, ShowTabsFullscreen); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, ShowPaneHeaders); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, ShowTabsInTitlebar); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, UseAcrylicInTabRow); PERMANENT_OBSERVABLE_PROJECTED_SETTING(_GlobalSettings, ShowTitleInTitlebar); diff --git a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.idl b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.idl index fb75021608..4a10edc406 100644 --- a/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.idl +++ b/src/cascadia/TerminalSettingsEditor/GlobalAppearanceViewModel.idl @@ -27,6 +27,7 @@ namespace Microsoft.Terminal.Settings.Editor PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, AlwaysShowTabs); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ShowTabsFullscreen); + PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ShowPaneHeaders); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ShowTabsInTitlebar); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, UseAcrylicInTabRow); PERMANENT_OBSERVABLE_PROJECTED_SETTING(Boolean, ShowTitleInTitlebar); diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index e52b4c84ac..305f68ed9a 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -2593,6 +2593,14 @@ When enabled, the tab bar will be visible when the app is full screen. A description for what the "show tabs in full screen" setting does. + + Show pane title headers + Header for a control to toggle if the app should show title headers above each pane when multiple panes are open. + + + When enabled, a title header is shown above each pane when multiple panes are open. + A description for what the "show pane headers" setting does. Presented near "Globals_ShowPaneHeaders.Header". + Path translation Name for a control to select how file and directory paths are translated. diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 77bcfc494d..b95bd3052d 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -59,6 +59,7 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(Int32, InitialCols); INHERITABLE_SETTING(Boolean, AlwaysShowTabs); INHERITABLE_SETTING(Boolean, ShowTabsFullscreen); + INHERITABLE_SETTING(Boolean, ShowPaneHeaders); INHERITABLE_SETTING(NewTabPosition, NewTabPosition); INHERITABLE_SETTING(Boolean, ShowTitleInTitlebar); INHERITABLE_SETTING(Boolean, ConfirmCloseAllTabs); diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h index b95aad938e..dea8955695 100644 --- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h +++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h @@ -71,7 +71,8 @@ Author(s): X(winrt::Windows::Foundation::Collections::IVector, NewTabMenu, "newTabMenu", winrt::single_threaded_vector({ Model::RemainingProfilesEntry{} })) \ X(bool, AllowHeadless, "compatibility.allowHeadless", false) \ X(hstring, SearchWebDefaultQueryUrl, "searchWebDefaultQueryUrl", L"https://www.bing.com/search?q=%22%s%22") \ - X(bool, ShowTabsFullscreen, "showTabsFullscreen", false) + X(bool, ShowTabsFullscreen, "showTabsFullscreen", false) \ + X(bool, ShowPaneHeaders, "showPaneHeaders", true) // Also add these settings to: // * Profile.idl