CmdPal: Add a dock (#45824)

Add support for a "dock" window in CmdPal. The dock is a toolbar powered
by the `APPBAR` APIs. This gives you a persistent region to display
commands for quick shortcuts or glanceable widgets.

The dock can be pinned to any side of the screen.
The dock can be independently styled with any of the theming controls
cmdpal already has
The dock has three "regions" to pin to - the "start", the "center", and
the "end".
Elements on the dock are grouped as "bands", which contains a set of
"items". Each "band" is one atomic unit. For example, the Media Player
extension produces 4 items, but one _band_.
The dock has only one size (for now)
The dock will only appear on your primary display (for now)

This PR includes support for pinning arbitrary top-level commands to the
dock - however, we're planning on replacing that with a more universal
ability to pin any command to the dock or top level. (see #45191). This
is at least usable for now.

This is definitely still _even more preview_ than usual PowerToys
features, but it's more than usable. I'd love to get it out there and
start collecting feedback on where to improve next. I'll probably add a
follow-up issue for tracking the remaining bugs & nits.

closes #45201

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Mike Griese
2026-02-27 07:24:23 -06:00
committed by GitHub
parent 494c14fb88
commit 70bf430d9f
90 changed files with 7148 additions and 193 deletions

View File

@@ -0,0 +1,213 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Markup;
namespace Microsoft.CmdPal.UI.Dock;
[ContentProperty(Name = nameof(Icon))]
public sealed partial class DockItemControl : Control
{
public DockItemControl()
{
DefaultStyleKey = typeof(DockItemControl);
}
public static readonly DependencyProperty ToolTipProperty =
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null));
public string ToolTip
{
get => (string)GetValue(ToolTipProperty);
set => SetValue(ToolTipProperty, value);
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly DependencyProperty SubtitleProperty =
DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public string Subtitle
{
get => (string)GetValue(SubtitleProperty);
set => SetValue(SubtitleProperty, value);
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DockItemControl), new PropertyMetadata(null, OnIconPropertyChanged));
public object Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly DependencyProperty TextVisibilityProperty =
DependencyProperty.Register(nameof(TextVisibility), typeof(Visibility), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
public Visibility TextVisibility
{
get => (Visibility)GetValue(TextVisibilityProperty);
set => SetValue(TextVisibilityProperty, value);
}
private const string IconPresenterName = "IconPresenter";
private FrameworkElement? _iconPresenter;
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockItemControl control)
{
control.UpdateTextVisibility();
control.UpdateAlignment();
}
}
private static void OnIconPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockItemControl control)
{
control.UpdateIconVisibility();
control.UpdateAlignment();
}
}
internal bool HasTitle => !string.IsNullOrEmpty(Title);
internal bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
internal bool HasText => HasTitle || HasSubtitle;
private void UpdateTextVisibility()
{
UpdateTextVisibilityState();
}
private void UpdateTextVisibilityState()
{
// Determine which visual state to use based on title/subtitle presence
var stateName = (HasTitle, HasSubtitle) switch
{
(true, true) => "TextVisible",
(true, false) => "TitleOnly",
(false, true) => "SubtitleOnly",
(false, false) => "TextHidden",
};
VisualStateManager.GoToState(this, stateName, true);
}
private void UpdateIconVisibility()
{
if (Icon is IconBox icon)
{
var dt = icon.DataContext;
var src = icon.Source;
if (_iconPresenter is not null)
{
// n.b. this might be wrong - I think we always have an Icon (an IconBox),
// we need to check if the box has an icon
_iconPresenter.Visibility = Icon is null ? Visibility.Collapsed : Visibility.Visible;
}
UpdateIconVisibilityState();
}
}
private void UpdateIconVisibilityState()
{
var hasIcon = Icon is not null;
VisualStateManager.GoToState(this, hasIcon ? "IconVisible" : "IconHidden", true);
}
private void UpdateAlignment()
{
// If this item has both an icon and a label, left align so that the
// icons don't wobble if the text changes.
//
// Otherwise, center align.
var requestedTheme = ActualTheme;
var isLight = requestedTheme == ElementTheme.Light;
var showText = HasText;
if (Icon is IconBox icoBox &&
icoBox.DataContext is DockItemViewModel item &&
item.Icon is IconInfoViewModel icon)
{
var showIcon = icon is not null && icon.HasIcon(isLight);
if (showText && showIcon)
{
HorizontalAlignment = HorizontalAlignment.Left;
return;
}
}
HorizontalAlignment = HorizontalAlignment.Center;
}
private void UpdateAllVisibility()
{
UpdateTextVisibility();
UpdateIconVisibility();
UpdateAlignment();
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
IsEnabledChanged -= OnIsEnabledChanged;
PointerEntered -= Control_PointerEntered;
PointerExited -= Control_PointerExited;
PointerEntered += Control_PointerEntered;
PointerExited += Control_PointerExited;
IsEnabledChanged += OnIsEnabledChanged;
// Get template children for visibility updates
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
// Set initial visibility
UpdateAllVisibility();
}
private void Control_PointerEntered(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "PointerOver", true);
}
private void Control_PointerExited(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "Normal", true);
}
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
if (IsEnabled)
{
base.OnPointerPressed(e);
VisualStateManager.GoToState(this, "Pressed", true);
}
}
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
}