// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include #include "ColorPickupFlyout.h" #include "Tab.h" #include "Tab.g.cpp" #include "Utils.h" #include "ColorHelper.h" using namespace winrt; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Core; using namespace winrt::Microsoft::Terminal::TerminalControl; using namespace winrt::Windows::System; namespace winrt { namespace MUX = Microsoft::UI::Xaml; } namespace winrt::TerminalApp::implementation { Tab::Tab(const GUID& profile, const TermControl& control) { _rootPane = std::make_shared(profile, control, true); _rootPane->Closed([=](auto&& /*s*/, auto&& /*e*/) { _ClosedHandlers(nullptr, nullptr); }); _activePane = _rootPane; _MakeTabViewItem(); } // Method Description: // - Initializes a TabViewItem for this Tab instance. // Arguments: // - // Return Value: // - void Tab::_MakeTabViewItem() { _tabViewItem = ::winrt::MUX::Controls::TabViewItem{}; _tabViewItem.DoubleTapped([weakThis = get_weak()](auto&& /*s*/, auto&& /*e*/) { if (auto tab{ weakThis.get() }) { tab->_inRename = true; tab->_UpdateTabHeader(); } }); _UpdateTitle(); } // Method Description: // - Get the root UIElement of this Tab's root pane. // Arguments: // - // Return Value: // - The UIElement acting as root of the Tab's root pane. UIElement Tab::GetRootElement() { return _rootPane->GetRootElement(); } // Method Description: // - Returns nullptr if no children of this tab were the last control to be // focused, or the TermControl that _was_ the last control to be focused (if // there was one). // - This control might not currently be focused, if the tab itself is not // currently focused. // Arguments: // - // Return Value: // - nullptr if no children were marked `_lastFocused`, else the TermControl // that was last focused. TermControl Tab::GetActiveTerminalControl() const { return _activePane->GetTerminalControl(); } // Method Description: // - Gets the TabViewItem that represents this Tab // Arguments: // - // Return Value: // - The TabViewItem that represents this Tab winrt::MUX::Controls::TabViewItem Tab::GetTabViewItem() { return _tabViewItem; } // Method Description: // - Called after construction of a Tab object to bind event handlers to its // associated Pane and TermControl object and to create the context menu of // the tab item // Arguments: // - control: reference to the TermControl object to bind event to // Return Value: // - void Tab::Initialize(const TermControl& control) { _BindEventHandlers(control); _CreateContextMenu(); } // Method Description: // - Returns true if this is the currently focused tab. For any set of tabs, // there should only be one tab that is marked as focused, though each tab has // no control over the other tabs in the set. // Arguments: // - // Return Value: // - true iff this tab is focused. bool Tab::IsFocused() const noexcept { return _focused; } // Method Description: // - Updates our focus state. If we're gaining focus, make sure to transfer // focus to the last focused terminal control in our tree of controls. // Arguments: // - focused: our new focus state. If true, we should be focused. If false, we // should be unfocused. // Return Value: // - void Tab::SetFocused(const bool focused) { _focused = focused; if (_focused) { _Focus(); } } // Method Description: // - Returns nullopt if no children of this tab were the last control to be // focused, or the GUID of the profile of the last control to be focused (if // there was one). // Arguments: // - // Return Value: // - nullopt if no children of this tab were the last control to be // focused, else the GUID of the profile of the last control to be focused std::optional Tab::GetFocusedProfile() const noexcept { return _activePane->GetFocusedProfile(); } // Method Description: // - Called after construction of a Tab object to bind event handlers to its // associated Pane and TermControl object // Arguments: // - control: reference to the TermControl object to bind event to // Return Value: // - void Tab::_BindEventHandlers(const TermControl& control) noexcept { _AttachEventHandlersToPane(_rootPane); _AttachEventHandlersToControl(control); } // Method Description: // - Attempts to update the settings of this tab's tree of panes. // Arguments: // - settings: The new TerminalSettings to apply to any matching controls // - profile: The GUID of the profile these settings should apply to. // Return Value: // - void Tab::UpdateSettings(const TerminalSettings& settings, const GUID& profile) { _rootPane->UpdateSettings(settings, profile); } // Method Description: // - Focus the last focused control in our tree of panes. // Arguments: // - // Return Value: // - void Tab::_Focus() { _focused = true; auto lastFocusedControl = GetActiveTerminalControl(); if (lastFocusedControl) { lastFocusedControl.Focus(FocusState::Programmatic); } } // Method Description: // - Set the icon on the TabViewItem for this tab. // Arguments: // - iconPath: The new path string to use as the IconPath for our TabViewItem // Return Value: // - winrt::fire_and_forget Tab::UpdateIcon(const winrt::hstring iconPath) { // Don't reload our icon if it hasn't changed. if (iconPath == _lastIconPath) { return; } _lastIconPath = iconPath; auto weakThis{ get_weak() }; co_await winrt::resume_foreground(_tabViewItem.Dispatcher()); if (auto tab{ weakThis.get() }) { IconPath(_lastIconPath); _tabViewItem.IconSource(GetColoredIcon(_lastIconPath)); } } // Method Description: // - Gets the title string of the last focused terminal control in our tree. // Returns the empty string if there is no such control. // Arguments: // - // Return Value: // - the title string of the last focused terminal control in our tree. winrt::hstring Tab::GetActiveTitle() const { if (!_runtimeTabText.empty()) { return _runtimeTabText; } const auto lastFocusedControl = GetActiveTerminalControl(); return lastFocusedControl ? lastFocusedControl.Title() : L""; } // Method Description: // - Set the text on the TabViewItem for this tab, and bubbles the new title // value up to anyone listening for changes to our title. Callers can // listen for the title change with a PropertyChanged even handler. // Arguments: // - // Return Value: // - winrt::fire_and_forget Tab::_UpdateTitle() { auto weakThis{ get_weak() }; co_await winrt::resume_foreground(_tabViewItem.Dispatcher()); if (auto tab{ weakThis.get() }) { // Bubble our current tab text to anyone who's listening for changes. Title(GetActiveTitle()); // Update the UI to reflect the changed _UpdateTabHeader(); } } // Method Description: // - Move the viewport of the terminal up or down a number of lines. Negative // values of `delta` will move the view up, and positive values will move // the viewport down. // Arguments: // - delta: a number of lines to move the viewport relative to the current viewport. // Return Value: // - winrt::fire_and_forget Tab::Scroll(const int delta) { auto control = GetActiveTerminalControl(); co_await winrt::resume_foreground(control.Dispatcher()); const auto currentOffset = control.GetScrollOffset(); control.ScrollViewport(currentOffset + delta); } // Method Description: // - Determines whether the focused pane has sufficient space to be split. // Arguments: // - splitType: The type of split we want to create. // Return Value: // - True if the focused pane can be split. False otherwise. bool Tab::CanSplitPane(winrt::TerminalApp::SplitState splitType) { return _activePane->CanSplit(splitType); } // Method Description: // - Split the focused pane in our tree of panes, and place the // given TermControl into the newly created pane. // Arguments: // - splitType: The type of split we want to create. // - profile: The profile GUID to associate with the newly created pane. // - control: A TermControl to use in the new pane. // Return Value: // - void Tab::SplitPane(winrt::TerminalApp::SplitState splitType, const GUID& profile, TermControl& control) { auto [first, second] = _activePane->Split(splitType, profile, control); _activePane = first; _AttachEventHandlersToControl(control); // Add a event handlers to the new panes' GotFocus event. When the pane // gains focus, we'll mark it as the new active pane. _AttachEventHandlersToPane(first); _AttachEventHandlersToPane(second); // Immediately update our tracker of the focused pane now. If we're // splitting panes during startup (from a commandline), then it's // possible that the focus events won't propagate immediately. Updating // the focus here will give the same effect though. _UpdateActivePane(second); } // Method Description: // - See Pane::CalcSnappedDimension float Tab::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const { return _rootPane->CalcSnappedDimension(widthOrHeight, dimension); } // Method Description: // - Update the size of our panes to fill the new given size. This happens when // the window is resized. // Arguments: // - newSize: the amount of space that the panes have to fill now. // Return Value: // - void Tab::ResizeContent(const winrt::Windows::Foundation::Size& newSize) { // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. _rootPane->ResizeContent(newSize); } // Method Description: // - Attempt to move a separator between panes, as to resize each child on // either size of the separator. See Pane::ResizePane for details. // Arguments: // - direction: The direction to move the separator in. // Return Value: // - void Tab::ResizePane(const winrt::TerminalApp::Direction& direction) { // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. _rootPane->ResizePane(direction); } // Method Description: // - Attempt to move focus between panes, as to focus the child on // the other side of the separator. See Pane::NavigateFocus for details. // Arguments: // - direction: The direction to move the focus in. // Return Value: // - void Tab::NavigateFocus(const winrt::TerminalApp::Direction& direction) { // NOTE: This _must_ be called on the root pane, so that it can propagate // throughout the entire tree. _rootPane->NavigateFocus(direction); } // Method Description: // - Prepares this tab for being removed from the UI hierarchy by shutting down all active connections. void Tab::Shutdown() { _rootPane->Shutdown(); } // Method Description: // - Closes the currently focused pane in this tab. If it's the last pane in // this tab, our Closed event will be fired (at a later time) for anyone // registered as a handler of our close event. // Arguments: // - // Return Value: // - void Tab::ClosePane() { _activePane->Close(); } void Tab::SetTabText(winrt::hstring title) { _runtimeTabText = title; _UpdateTitle(); } void Tab::ResetTabText() { _runtimeTabText = L""; _UpdateTitle(); } // Method Description: // - Register any event handlers that we may need with the given TermControl. // This should be called on each and every TermControl that we add to the tree // of Panes in this tab. We'll add events too: // * notify us when the control's title changed, so we can update our own // title (if necessary) // Arguments: // - control: the TermControl to add events to. // Return Value: // - void Tab::_AttachEventHandlersToControl(const TermControl& control) { auto weakThis{ get_weak() }; control.TitleChanged([weakThis](auto newTitle) { // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) { // The title of the control changed, but not necessarily the title of the tab. // Set the tab's text to the active panes' text. tab->_UpdateTitle(); } }); // This is called when the terminal changes its font size or sets it for the first // time (because when we just create terminal via its ctor it has invalid font size). // On the latter event, we tell the root pane to resize itself so that its descendants // (including ourself) can properly snap to character grids. In future, we may also // want to do that on regular font changes. control.FontSizeChanged([this](const int /* fontWidth */, const int /* fontHeight */, const bool isInitialChange) { if (isInitialChange) { _rootPane->Relayout(); } }); } // Method Description: // - Mark the given pane as the active pane in this tab. All other panes // will be marked as inactive. We'll also update our own UI state to // reflect this newly active pane. // Arguments: // - pane: a Pane to mark as active. // Return Value: // - void Tab::_UpdateActivePane(std::shared_ptr pane) { // Clear the active state of the entire tree, and mark only the pane as active. _rootPane->ClearActive(); _activePane = pane; _activePane->SetActive(); // Update our own title text to match the newly-active pane. _UpdateTitle(); // Raise our own ActivePaneChanged event. _ActivePaneChangedHandlers(); } // Method Description: // - Add an event handler to this pane's GotFocus event. When that pane gains // focus, we'll mark it as the new active pane. We'll also query the title of // that pane when it's focused to set our own text, and finally, we'll trigger // our own ActivePaneChanged event. // Arguments: // - // Return Value: // - void Tab::_AttachEventHandlersToPane(std::shared_ptr pane) { auto weakThis{ get_weak() }; pane->GotFocus([weakThis](std::shared_ptr sender) { // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; if (tab && sender != tab->_activePane) { tab->_UpdateActivePane(sender); } }); } // Method Description: // - Creates a context menu attached to the tab. // Currently contains elements allowing to select or // to close the current tab // Arguments: // - // Return Value: // - void Tab::_CreateContextMenu() { auto weakThis{ get_weak() }; // Close Controls::MenuFlyoutItem closeTabMenuItem; Controls::FontIcon closeSymbol; closeSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); closeSymbol.Glyph(L"\xE8BB"); closeTabMenuItem.Click([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { tab->_rootPane->Close(); } }); closeTabMenuItem.Text(RS_(L"TabClose")); closeTabMenuItem.Icon(closeSymbol); // "Color..." Controls::MenuFlyoutItem chooseColorMenuItem; Controls::FontIcon colorPickSymbol; colorPickSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); colorPickSymbol.Glyph(L"\xE790"); chooseColorMenuItem.Click([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { tab->ActivateColorPicker(); } }); chooseColorMenuItem.Text(RS_(L"TabColorChoose")); chooseColorMenuItem.Icon(colorPickSymbol); // Color Picker (it's convenient to have it here) _tabColorPickup.ColorSelected([weakThis](auto newTabColor) { if (auto tab{ weakThis.get() }) { tab->SetTabColor(newTabColor); } }); _tabColorPickup.ColorCleared([weakThis]() { if (auto tab{ weakThis.get() }) { tab->ResetTabColor(); } }); Controls::MenuFlyoutItem renameTabMenuItem; { // "Rename Tab" Controls::FontIcon renameTabSymbol; renameTabSymbol.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); renameTabSymbol.Glyph(L"\xE932"); // Label renameTabMenuItem.Click([weakThis](auto&&, auto&&) { if (auto tab{ weakThis.get() }) { tab->_inRename = true; tab->_UpdateTabHeader(); } }); renameTabMenuItem.Text(RS_(L"RenameTabText")); renameTabMenuItem.Icon(renameTabSymbol); } // Build the menu Controls::MenuFlyout newTabFlyout; Controls::MenuFlyoutSeparator menuSeparator; newTabFlyout.Items().Append(chooseColorMenuItem); newTabFlyout.Items().Append(renameTabMenuItem); newTabFlyout.Items().Append(menuSeparator); newTabFlyout.Items().Append(closeTabMenuItem); _tabViewItem.ContextFlyout(newTabFlyout); } // Method Description: // - This will update the contents of our TabViewItem for our current state. // - If we're not in a rename, we'll set the Header of the TabViewItem to // simply our current tab text (either the runtime tab text or the // active terminal's text). // - If we're in a rename, then we'll set the Header to a TextBox with the // current tab text. The user can then use that TextBox to set a string // to use as an override for the tab's text. // Arguments: // - // Return Value: // - void Tab::_UpdateTabHeader() { winrt::hstring tabText{ GetActiveTitle() }; if (!_inRename) { // If we're not currently in the process of renaming the tab, then just set the tab's text to whatever our active title is. _tabViewItem.Header(winrt::box_value(tabText)); } else { _ConstructTabRenameBox(tabText); } } // Method Description: // - Create a new TextBox to use as the control for renaming the tab text. // If the text box is already created, then this will do nothing, and // leave the current box unmodified. // Arguments: // - tabText: This should be the text to initialize the rename text box with. // Return Value: // - void Tab::_ConstructTabRenameBox(const winrt::hstring& tabText) { if (_tabViewItem.Header().try_as()) { return; } Controls::TextBox tabTextBox; tabTextBox.Text(tabText); // The TextBox has a MinHeight already set by default, which is // larger than we want. Get rid of it. tabTextBox.MinHeight(0); // Also get rid of the internal padding on the text box, between the // border and the text content, on the top and bottom. This will // help the box fit within the bounds of the tab. Thickness internalPadding = ThicknessHelper::FromLengths(4, 0, 4, 0); tabTextBox.Padding(internalPadding); // Make the margin (0, -8, 0, -8), to counteract the padding that // the TabViewItem has. // // This is maybe a bit fragile, as the actual value might not be exactly // (0, 8, 0, 8), but using TabViewItemHeaderPadding to look up the real // value at runtime didn't work. So this is good enough for now. Thickness negativeMargins = ThicknessHelper::FromLengths(0, -8, 0, -8); tabTextBox.Margin(negativeMargins); // Set up some event handlers on the text box. We need three of them: // * A LostFocus event, so when the TextBox loses focus, we'll // remove it and return to just the text on the tab. // * A KeyUp event, to be able to submit the tab text on Enter or // dismiss the text box on Escape // * A LayoutUpdated event, so that we can auto-focus the text box // when it's added to the tree. auto weakThis{ get_weak() }; // When the text box loses focus, update the tab title of our tab. // - If there are any contents in the box, we'll use that value as // the new "runtime text", which will override any text set by the // application. // - If the text box is empty, we'll reset the "runtime text", and // return to using the active terminal's title. tabTextBox.LostFocus([weakThis](const IInspectable& sender, auto&&) { auto tab{ weakThis.get() }; auto textBox{ sender.try_as() }; if (tab && textBox) { tab->_runtimeTabText = textBox.Text(); tab->_inRename = false; tab->_UpdateTitle(); } }); // NOTE: (Preview)KeyDown does not work here. If you use that, we'll // remove the TextBox from the UI tree, then the following KeyUp // will bubble to the NewTabButton, which we don't want to have // happen. tabTextBox.KeyUp([weakThis](const IInspectable& sender, Input::KeyRoutedEventArgs const& e) { auto tab{ weakThis.get() }; auto textBox{ sender.try_as() }; if (tab && textBox) { switch (e.OriginalKey()) { case VirtualKey::Enter: tab->_runtimeTabText = textBox.Text(); [[fallthrough]]; case VirtualKey::Escape: e.Handled(true); textBox.Text(tab->_runtimeTabText); tab->_inRename = false; tab->_UpdateTitle(); break; } } }); // As soon as the text box is added to the UI tree, focus it. We can't focus it till it's in the tree. _tabRenameBoxLayoutUpdatedRevoker = tabTextBox.LayoutUpdated(winrt::auto_revoke, [this](auto&&, auto&&) { // Curiously, the sender for this event is null, so we have to // get the TextBox from the Tab's Header(). auto textBox{ _tabViewItem.Header().try_as() }; if (textBox) { textBox.SelectAll(); textBox.Focus(FocusState::Programmatic); } // Only let this succeed once. _tabRenameBoxLayoutUpdatedRevoker.revoke(); }); _tabViewItem.Header(tabTextBox); } // Method Description: // Returns the tab color, if any // Arguments: // - // Return Value: // - The tab's color, if any std::optional Tab::GetTabColor() { return _tabColor; } // Method Description: // - Sets the tab background color to the color chosen by the user // - Sets the tab foreground color depending on the luminance of // the background color // Arguments: // - color: the shiny color the user picked for their tab // Return Value: // - void Tab::SetTabColor(const winrt::Windows::UI::Color& color) { auto weakThis{ get_weak() }; _tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, color]() { auto ptrTab = weakThis.get(); if (!ptrTab) return; auto tab{ ptrTab }; Media::SolidColorBrush selectedTabBrush{}; Media::SolidColorBrush deselectedTabBrush{}; Media::SolidColorBrush fontBrush{}; Media::SolidColorBrush hoverTabBrush{}; // calculate the luminance of the current color and select a font // color based on that // see https://www.w3.org/TR/WCAG20/#relativeluminancedef if (TerminalApp::ColorHelper::IsBrightColor(color)) { fontBrush.Color(winrt::Windows::UI::Colors::Black()); } else { fontBrush.Color(winrt::Windows::UI::Colors::White()); } hoverTabBrush.Color(TerminalApp::ColorHelper::GetAccentColor(color)); selectedTabBrush.Color(color); // currently if a tab has a custom color, a deselected state is // signified by using the same color with a bit ot transparency auto deselectedTabColor = color; deselectedTabColor.A = 64; deselectedTabBrush.Color(deselectedTabColor); // currently if a tab has a custom color, a deselected state is // signified by using the same color with a bit ot transparency tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), selectedTabBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackground"), deselectedTabBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPointerOver"), hoverTabBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundPressed"), selectedTabBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderForeground"), fontBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundSelected"), fontBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPointerOver"), fontBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderForegroundPressed"), fontBrush); tab->_tabViewItem.Resources().Insert(winrt::box_value(L"TabViewButtonForegroundActiveTab"), fontBrush); tab->_RefreshVisualState(); tab->_tabColor.emplace(color); tab->_colorSelected(color); }); } // Method Description: // Clear the custom color of the tab, if any // the background color // Arguments: // - // Return Value: // - void Tab::ResetTabColor() { auto weakThis{ get_weak() }; _tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis]() { auto ptrTab = weakThis.get(); if (!ptrTab) return; auto tab{ ptrTab }; winrt::hstring keys[] = { L"TabViewItemHeaderBackground", L"TabViewItemHeaderBackgroundSelected", L"TabViewItemHeaderBackgroundPointerOver", L"TabViewItemHeaderForeground", L"TabViewItemHeaderForegroundSelected", L"TabViewItemHeaderForegroundPointerOver", L"TabViewItemHeaderBackgroundPressed", L"TabViewItemHeaderForegroundPressed", L"TabViewButtonForegroundActiveTab" }; // simply clear any of the colors in the tab's dict for (auto keyString : keys) { auto key = winrt::box_value(keyString); if (tab->_tabViewItem.Resources().HasKey(key)) { tab->_tabViewItem.Resources().Remove(key); } } tab->_RefreshVisualState(); tab->_tabColor.reset(); tab->_colorCleared(); }); } // Method Description: // - Display the tab color picker at the location of the TabViewItem for this tab. // Arguments: // - // Return Value: // - void Tab::ActivateColorPicker() { _tabColorPickup.ShowAt(_tabViewItem); } // Method Description: // Toggles the visual state of the tab view item, // so that changes to the tab color are reflected immediately // Arguments: // - // Return Value: // - void Tab::_RefreshVisualState() { if (_focused) { VisualStateManager::GoToState(_tabViewItem, L"Normal", true); VisualStateManager::GoToState(_tabViewItem, L"Selected", true); } else { VisualStateManager::GoToState(_tabViewItem, L"Selected", true); VisualStateManager::GoToState(_tabViewItem, L"Normal", true); } } // - Get the total number of leaf panes in this tab. This will be the number // of actual controls hosted by this tab. // Arguments: // - // Return Value: // - The total number of leaf panes hosted by this tab. int Tab::_GetLeafPaneCount() const noexcept { return _rootPane->GetLeafPaneCount(); } // Method Description: // - This is a helper to determine which direction an "Automatic" split should // happen in for the active pane of this tab, but without using the ActualWidth() and // ActualHeight() methods. // - See Pane::PreCalculateAutoSplit // Arguments: // - availableSpace: The theoretical space that's available for this Tab's content // Return Value: // - The SplitState that we should use for an `Automatic` split given // `availableSpace` SplitState Tab::PreCalculateAutoSplit(winrt::Windows::Foundation::Size availableSpace) const { return _rootPane->PreCalculateAutoSplit(_activePane, availableSpace).value_or(SplitState::Vertical); } bool Tab::PreCalculateCanSplit(SplitState splitType, winrt::Windows::Foundation::Size availableSpace) const { return _rootPane->PreCalculateCanSplit(_activePane, splitType, availableSpace).value_or(false); } DEFINE_EVENT(Tab, ActivePaneChanged, _ActivePaneChangedHandlers, winrt::delegate<>); DEFINE_EVENT(Tab, ColorSelected, _colorSelected, winrt::delegate); DEFINE_EVENT(Tab, ColorCleared, _colorCleared, winrt::delegate<>); }