diff --git a/src/Wpf.Ui/Controls/TitleBar.cs b/src/Wpf.Ui/Controls/TitleBar.cs index f95153e60..0535b4228 100644 --- a/src/Wpf.Ui/Controls/TitleBar.cs +++ b/src/Wpf.Ui/Controls/TitleBar.cs @@ -5,33 +5,39 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.Windows; using System.Windows.Input; +using System.Windows.Interop; using System.Windows.Media; +using Wpf.Ui.Common; using Wpf.Ui.Dpi; -using Wpf.Ui.TitleBar; +using Wpf.Ui.Extensions; +using Wpf.Ui.Interop; namespace Wpf.Ui.Controls; /// /// Custom navigation buttons for the window. /// -[TemplatePart(Name = "PART_MainGrid", Type = typeof(System.Windows.Controls.Grid))] -[TemplatePart(Name = "PART_MaximizeButton", Type = typeof(Wpf.Ui.Controls.Button))] -[TemplatePart(Name = "PART_RestoreButton", Type = typeof(Wpf.Ui.Controls.Button))] +[TemplatePart(Name = ElementMainGrid, Type = typeof(System.Windows.Controls.Grid))] +[TemplatePart(Name = ElementIcon, Type = typeof(System.Windows.Controls.Image))] +[TemplatePart(Name = ElementHelpButton, Type = typeof(TitleBarButton))] +[TemplatePart(Name = ElementMinimizeButton, Type = typeof(TitleBarButton))] +[TemplatePart(Name = ElementMaximizeButton, Type = typeof(TitleBarButton))] +[TemplatePart(Name = ElementRestoreButton, Type = typeof(TitleBarButton))] +[TemplatePart(Name = ElementCloseButton, Type = typeof(TitleBarButton))] public class TitleBar : System.Windows.Controls.Control, IThemeControl { + private const string ElementIcon = "PART_Icon"; private const string ElementMainGrid = "PART_MainGrid"; - + private const string ElementHelpButton = "PART_HelpButton"; + private const string ElementMinimizeButton = "PART_MinimizeButton"; private const string ElementMaximizeButton = "PART_MaximizeButton"; - private const string ElementRestoreButton = "PART_RestoreButton"; + private const string ElementCloseButton = "PART_CloseButton"; - private System.Windows.Window _parent; - - internal Interop.WinDef.POINT _doubleClickPoint; - - internal SnapLayout _snapLayout; + #region Static properties /// /// Property for . @@ -74,13 +80,6 @@ public class TitleBar : System.Windows.Controls.Control, IThemeControl nameof(MinimizeToTray), typeof(bool), typeof(TitleBar), new PropertyMetadata(false)); - /// - /// Property for . - /// - public static readonly DependencyProperty UseSnapLayoutProperty = DependencyProperty.Register( - nameof(UseSnapLayout), - typeof(bool), typeof(TitleBar), new PropertyMetadata(false)); - /// /// Property for . /// @@ -136,6 +135,13 @@ public class TitleBar : System.Windows.Controls.Control, IThemeControl nameof(Icon), typeof(ImageSource), typeof(TitleBar), new PropertyMetadata(null)); + /// + /// Property for . + /// + public static readonly DependencyProperty CloseWindowByDoubleClickOnIconProperty = DependencyProperty.Register( + nameof(CloseWindowByDoubleClickOnIcon), + typeof(bool), typeof(TitleBar), new PropertyMetadata(true)); + /// /// Property for . /// @@ -174,6 +180,10 @@ public class TitleBar : System.Windows.Controls.Control, IThemeControl DependencyProperty.Register(nameof(TemplateButtonCommand), typeof(Common.IRelayCommand), typeof(TitleBar), new PropertyMetadata(null)); + #endregion + + #region Properties + /// public Appearance.ThemeType Theme { @@ -228,15 +238,6 @@ public bool MinimizeToTray set => SetValue(MinimizeToTrayProperty, value); } - /// - /// Gets or sets information whether the use Windows 11 Snap Layout. - /// - public bool UseSnapLayout - { - get => (bool)GetValue(UseSnapLayoutProperty); - set => SetValue(UseSnapLayoutProperty, value); - } - /// /// Gets or sets information whether the current window is maximized. /// @@ -309,6 +310,15 @@ public ImageSource Icon set => SetValue(IconProperty, value); } + /// + /// Enables or disable closing the window by double clicking on the icon + /// + public bool CloseWindowByDoubleClickOnIcon + { + get => (bool)GetValue(CloseWindowByDoubleClickOnIconProperty); + set => SetValue(CloseWindowByDoubleClickOnIconProperty, value); + } + /// /// Tray icon. /// @@ -357,31 +367,35 @@ public event RoutedEventHandler HelpClicked /// /// Command triggered after clicking the titlebar button. /// - public Common.IRelayCommand TemplateButtonCommand => (Common.IRelayCommand)GetValue(TemplateButtonCommandProperty); + public IRelayCommand TemplateButtonCommand => (IRelayCommand)GetValue(TemplateButtonCommandProperty); /// /// Lets you override the behavior of the Maximize/Restore button with an . /// - public Action MaximizeActionOverride { get; set; } = null!; + public Action? MaximizeActionOverride { get; set; } /// /// Lets you override the behavior of the Minimize button with an . /// - public Action MinimizeActionOverride { get; set; } = null!; + public Action? MinimizeActionOverride { get; set; } - /// - /// Window containing the TitleBar. - /// - internal System.Windows.Window ParentWindow => _parent ??= System.Windows.Window.GetWindow(this); + #endregion + + private Interop.WinDef.POINT _doubleClickPoint; + private System.Windows.Window _currentWindow = null!; + private System.Windows.Controls.Grid _mainGrid = null!; + private System.Windows.Controls.Image _icon = null!; + private readonly TitleBarButton[] _buttons = new TitleBarButton[4]; /// /// Creates a new instance of the class and sets the default event. /// public TitleBar() { - SetValue(TemplateButtonCommandProperty, new Common.RelayCommand(o => OnTemplateButtonClick(o ?? String.Empty))); + SetValue(TemplateButtonCommandProperty, new Common.RelayCommand(OnTemplateButtonClick)); Loaded += OnLoaded; + Unloaded += OnUnloaded; } /// @@ -395,8 +409,26 @@ protected override void OnInitialized(EventArgs e) protected virtual void OnLoaded(object sender, RoutedEventArgs e) { - if (ParentWindow != null) - ParentWindow.StateChanged += OnParentWindowStateChanged; + if (DesignerHelper.IsInDesignMode) + return; + + _currentWindow = System.Windows.Window.GetWindow(this) ?? throw new ArgumentNullException("Window is null"); + _currentWindow.StateChanged += OnParentWindowStateChanged; + + var handle = new WindowInteropHelper(_currentWindow).EnsureHandle(); + var windowSource = HwndSource.FromHwnd(handle) ?? throw new ArgumentNullException("Window source is null"); + windowSource.AddHook(HwndSourceHook); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + Loaded -= OnLoaded; + Unloaded -= OnUnloaded; + + Appearance.Theme.Changed -= OnThemeChanged; + + _mainGrid.MouseLeftButtonDown -= OnMainGridMouseLeftButtonDown; + _mainGrid.MouseMove -= OnMainGridMouseMove; } /// @@ -407,18 +439,21 @@ public override void OnApplyTemplate() { base.OnApplyTemplate(); - var mainGrid = GetTemplateChild(ElementMainGrid) as System.Windows.Controls.Grid; - var maximizeButton = GetTemplateChild(ElementMaximizeButton) as Wpf.Ui.Controls.Button; - var restoreButton = GetTemplateChild(ElementRestoreButton) as Wpf.Ui.Controls.Button; + _mainGrid = GetTemplateChild(ElementMainGrid); + _mainGrid.MouseLeftButtonDown += OnMainGridMouseLeftButtonDown; + _mainGrid.MouseMove += OnMainGridMouseMove; - if (mainGrid != null) - { - mainGrid.MouseLeftButtonDown += OnMainGridMouseLeftButtonDown; - mainGrid.MouseMove += OnMainGridMouseMove; - } + _icon = GetTemplateChild(ElementIcon); + + var helpButton = GetTemplateChild(ElementHelpButton); + var minimizeButton = GetTemplateChild(ElementMinimizeButton); + var maximizeButton = GetTemplateChild(ElementMaximizeButton); + var closeButton = GetTemplateChild(ElementCloseButton); - if (ShowMaximize && UseSnapLayout && maximizeButton != null && restoreButton != null) - InitializeSnapLayout(maximizeButton, restoreButton); + _buttons[0] = maximizeButton; + _buttons[1] = minimizeButton; + _buttons[2] = closeButton; + _buttons[3] = helpButton; } /// @@ -426,31 +461,24 @@ public override void OnApplyTemplate() /// protected virtual void OnThemeChanged(Appearance.ThemeType currentTheme, Color systemAccent) { -#if DEBUG - System.Diagnostics.Debug.WriteLine($"INFO | {typeof(TitleBar)} received theme - {currentTheme}", + Debug.WriteLine($"INFO | {typeof(TitleBar)} received theme - {currentTheme}", "Wpf.Ui.TitleBar"); -#endif - Theme = currentTheme; - if (_snapLayout != null) - _snapLayout.Theme = currentTheme; + Theme = currentTheme; } private void CloseWindow() { -#if DEBUG - System.Diagnostics.Debug.WriteLine($"INFO | {typeof(TitleBar)}.CloseWindow:ForceShutdown - {ForceShutdown}", + Debug.WriteLine($"INFO | {typeof(TitleBar)}.CloseWindow:ForceShutdown - {ForceShutdown}", "Wpf.Ui.TitleBar"); -#endif if (ForceShutdown) { Application.Current.Shutdown(); - return; } - Appearance.Theme.Changed -= OnThemeChanged; - ParentWindow.Close(); + + _currentWindow.Close(); } private void MinimizeWindow() @@ -458,14 +486,14 @@ private void MinimizeWindow() if (MinimizeToTray && Tray.IsRegistered && MinimizeWindowToTray()) return; - if (MinimizeActionOverride != null) + if (MinimizeActionOverride is not null) { - MinimizeActionOverride(this, _parent); + MinimizeActionOverride(this, _currentWindow); return; } - ParentWindow.WindowState = WindowState.Minimized; + _currentWindow.WindowState = WindowState.Minimized; } private void MaximizeWindow() @@ -473,22 +501,22 @@ private void MaximizeWindow() if (!CanMaximize) return; - if (MaximizeActionOverride != null) + if (MaximizeActionOverride is not null) { - MaximizeActionOverride(this, _parent); + MaximizeActionOverride(this, _currentWindow); return; } - if (ParentWindow.WindowState == WindowState.Normal) + if (_currentWindow.WindowState == WindowState.Normal) { IsMaximized = true; - ParentWindow.WindowState = WindowState.Maximized; + _currentWindow.WindowState = WindowState.Maximized; } else { IsMaximized = false; - ParentWindow.WindowState = WindowState.Normal; + _currentWindow.WindowState = WindowState.Normal; } } @@ -497,22 +525,22 @@ private void RestoreWindow() if (!CanMaximize) return; - if (MaximizeActionOverride != null) + if (MaximizeActionOverride is not null) { - MaximizeActionOverride(this, _parent); + MaximizeActionOverride(this, _currentWindow); return; } - if (ParentWindow.WindowState == WindowState.Normal) + if (_currentWindow.WindowState == WindowState.Normal) { IsMaximized = true; - ParentWindow.WindowState = WindowState.Maximized; + _currentWindow.WindowState = WindowState.Maximized; } else { IsMaximized = false; - ParentWindow.WindowState = WindowState.Normal; + _currentWindow.WindowState = WindowState.Normal; } } @@ -521,44 +549,19 @@ private bool MinimizeWindowToTray() if (!Tray.IsRegistered) return false; - ParentWindow.WindowState = WindowState.Minimized; - ParentWindow.Hide(); + _currentWindow.WindowState = WindowState.Minimized; + _currentWindow.Hide(); return true; } - private void InitializeSnapLayout(Wpf.Ui.Controls.Button maximizeButton, Wpf.Ui.Controls.Button restoreButton) - { - if (!SnapLayout.IsSupported()) - return; - - _snapLayout = SnapLayout.Register(ParentWindow, maximizeButton, restoreButton); - - // Can be taken it from the Template, but honestly - a classic - TODO: - // ButtonsBackground, but - _snapLayout.HoverColorLight = new SolidColorBrush(Color.FromArgb( - (byte)0x1A, - (byte)0x00, - (byte)0x00, - (byte)0x00) - ); - _snapLayout.HoverColorDark = new SolidColorBrush(Color.FromArgb( - (byte)0x17, - (byte)0xFF, - (byte)0xFF, - (byte)0xFF) - ); - - _snapLayout.Theme = Theme; - } - private void OnMainGridMouseMove(object sender, MouseEventArgs e) { - if (e.LeftButton != MouseButtonState.Pressed || ParentWindow == null) + if (e.LeftButton != MouseButtonState.Pressed) return; // prevent firing from double clicking when the mouse never actually moved - Interop.User32.GetCursorPos(out var currentMousePos); + User32.GetCursorPos(out var currentMousePos); if (currentMousePos.x == _doubleClickPoint.x && currentMousePos.y == _doubleClickPoint.y) return; @@ -577,29 +580,26 @@ private void OnMainGridMouseMove(object sender, MouseEventArgs e) // how the OS operates. // - It should be set as a % (e.g. screen X / maximized width), // then offset from the left to line up more naturally. - ParentWindow.Left = screenPoint.X - (ParentWindow.RestoreBounds.Width * 0.5); - ParentWindow.Top = screenPoint.Y; + _currentWindow.Left = screenPoint.X - (_currentWindow.RestoreBounds.Width * 0.5); + _currentWindow.Top = screenPoint.Y; // style has to be quickly swapped to avoid restore animation delay - var style = ParentWindow.WindowStyle; - ParentWindow.WindowStyle = WindowStyle.None; - ParentWindow.WindowState = WindowState.Normal; - ParentWindow.WindowStyle = style; + var style = _currentWindow.WindowStyle; + _currentWindow.WindowStyle = WindowStyle.None; + _currentWindow.WindowState = WindowState.Normal; + _currentWindow.WindowStyle = style; } // Call drag move only when mouse down, check again // if() if (e.LeftButton == MouseButtonState.Pressed) - ParentWindow.DragMove(); + _currentWindow.DragMove(); } private void OnParentWindowStateChanged(object sender, EventArgs e) { - if (ParentWindow == null) - return; - - if (IsMaximized != (ParentWindow.WindowState == WindowState.Maximized)) - IsMaximized = ParentWindow.WindowState == WindowState.Maximized; + if (IsMaximized != (_currentWindow.WindowState == WindowState.Maximized)) + IsMaximized = _currentWindow.WindowState == WindowState.Maximized; } private void OnMainGridMouseLeftButtonDown(object sender, MouseButtonEventArgs e) @@ -612,33 +612,86 @@ private void OnMainGridMouseLeftButtonDown(object sender, MouseButtonEventArgs e MaximizeWindow(); } - private void OnTemplateButtonClick(string parameter) + private void OnTemplateButtonClick(TitleBarButtonType buttonType) { - switch (parameter) + switch (buttonType) { - case "maximize": + case TitleBarButtonType.Maximize: RaiseEvent(new RoutedEventArgs(MaximizeClickedEvent, this)); MaximizeWindow(); break; - case "restore": + case TitleBarButtonType.Restore: RaiseEvent(new RoutedEventArgs(MaximizeClickedEvent, this)); RestoreWindow(); break; - case "close": + case TitleBarButtonType.Close: RaiseEvent(new RoutedEventArgs(CloseClickedEvent, this)); CloseWindow(); break; - case "minimize": + case TitleBarButtonType.Minimize: RaiseEvent(new RoutedEventArgs(MinimizeClickedEvent, this)); MinimizeWindow(); break; - case "help": + case TitleBarButtonType.Help: RaiseEvent(new RoutedEventArgs(HelpClickedEvent, this)); break; } } + + private IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + var message = (User32.WM)msg; + + if (message is not (User32.WM.NCHITTEST or User32.WM.NCMOUSELEAVE or User32.WM.NCLBUTTONDOWN or User32.WM.NCLBUTTONUP)) + return IntPtr.Zero; + + foreach (var button in _buttons) + { + if (!button.ReactToHwndHook(message, lParam, out var returnIntPtr)) + continue; + + //It happens that the background is not removed from the buttons and you can make all the buttons are in the IsHovered=true + //It cleans up + foreach (var anotherButton in _buttons) + { + if (anotherButton == button) + continue; + + if (anotherButton.IsHovered && button.IsHovered) + { + anotherButton.RemoveHover(); + } + } + + handled = true; + return returnIntPtr; + } + + switch (message) + { + case User32.WM.NCHITTEST when CloseWindowByDoubleClickOnIcon && _icon.IsMouseOverElement(lParam): + handled = true; + //Ideally, clicking on the icon should open the system menu, but when the system menu is opened manually, double-clicking on the icon does not close the window + return (IntPtr)User32.WM_NCHITTEST.HTSYSMENU; + case User32.WM.NCHITTEST when this.IsMouseOverElement(lParam): + handled = true; + return (IntPtr)User32.WM_NCHITTEST.HTCAPTION; + default: + return IntPtr.Zero; + } + } + + private T GetTemplateChild(string name) where T : DependencyObject + { + var element = base.GetTemplateChild(name); + + if (element is null) + throw new ArgumentNullException($"{name} is null"); + + return (T)element; + } } diff --git a/src/Wpf.Ui/Controls/TitleBarButton.cs b/src/Wpf.Ui/Controls/TitleBarButton.cs new file mode 100644 index 000000000..0b818f5c0 --- /dev/null +++ b/src/Wpf.Ui/Controls/TitleBarButton.cs @@ -0,0 +1,141 @@ +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Automation.Peers; +using System.Windows.Automation.Provider; +using System.Windows.Media; +using Wpf.Ui.Extensions; +using Wpf.Ui.Interop; + +namespace Wpf.Ui.Controls; + +internal class TitleBarButton : Wpf.Ui.Controls.Button +{ + + /// + /// Property for . + /// + public static readonly DependencyProperty ButtonTypeProperty = DependencyProperty.Register(nameof(ButtonType), + typeof(TitleBarButtonType), typeof(TitleBarButton), new PropertyMetadata(TitleBarButtonType.Unknown, ButtonTypePropertyCallback)); + + /// + /// Property for . + /// + public static readonly DependencyProperty ButtonsForegroundProperty = DependencyProperty.Register( + nameof(ButtonsForeground), + typeof(Brush), typeof(TitleBarButton), new FrameworkPropertyMetadata(SystemColors.ControlTextBrush, + FrameworkPropertyMetadataOptions.Inherits)); + + /// + /// Sets or gets the + /// + public TitleBarButtonType ButtonType + { + get => (TitleBarButtonType)GetValue(ButtonTypeProperty); + set => SetValue(ButtonTypeProperty, value); + } + + /// + /// Foreground of the navigation buttons. + /// + public Brush ButtonsForeground + { + get => (Brush)GetValue(ButtonsForegroundProperty); + set => SetValue(ButtonsForegroundProperty, value); + } + + public bool IsHovered { get; private set; } + + private User32.WM_NCHITTEST _returnValue; + private Brush _defaultBackgroundBrush = Brushes.Transparent; //Should it be transparent? + + private bool _isClickedDown; + + /// + /// Forces button background to change. + /// + public void Hover() + { + if (IsHovered) + return; + + Background = MouseOverBackground; + IsHovered = true; + } + + /// + /// Forces button background to change. + /// + public void RemoveHover() + { + if (!IsHovered) + return; + + Background = _defaultBackgroundBrush; + + IsHovered = false; + _isClickedDown = false; + } + + /// + /// Invokes click on the button. + /// + public void InvokeClick() + { + if (new ButtonAutomationPeer(this).GetPattern(PatternInterface.Invoke) is IInvokeProvider invokeProvider) + invokeProvider.Invoke(); + + _isClickedDown = false; + } + + internal bool ReactToHwndHook(User32.WM msg, IntPtr lParam, out IntPtr returnIntPtr) + { + returnIntPtr = IntPtr.Zero; + + switch (msg) + { + case User32.WM.NCHITTEST: + if (this.IsMouseOverElement(lParam)) + { + //Debug.WriteLine($"Hitting {ButtonType} | return code {_returnValue}"); + + Hover(); + returnIntPtr = (IntPtr)_returnValue; + return true; + } + + RemoveHover(); + return false; + + case User32.WM.NCMOUSELEAVE: // Mouse leaves the window + RemoveHover(); + return false; + case User32.WM.NCLBUTTONDOWN when this.IsMouseOverElement(lParam): // Left button clicked down + _isClickedDown = true; + return true; + case User32.WM.NCLBUTTONUP when _isClickedDown && this.IsMouseOverElement(lParam): // Left button clicked up + InvokeClick(); + return true; + default: + return false; + } + } + + private void UpdateReturnValue(TitleBarButtonType buttonType) => + _returnValue = buttonType switch + { + TitleBarButtonType.Unknown => User32.WM_NCHITTEST.HTNOWHERE, + TitleBarButtonType.Help => User32.WM_NCHITTEST.HTHELP, + TitleBarButtonType.Minimize => User32.WM_NCHITTEST.HTMINBUTTON, + TitleBarButtonType.Close => User32.WM_NCHITTEST.HTCLOSE, + TitleBarButtonType.Restore => User32.WM_NCHITTEST.HTMAXBUTTON, + TitleBarButtonType.Maximize => User32.WM_NCHITTEST.HTMAXBUTTON, + _ => throw new ArgumentOutOfRangeException(nameof(buttonType), buttonType, null) + }; + + private static void ButtonTypePropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var titleBarButton = (TitleBarButton)d; + titleBarButton.UpdateReturnValue((TitleBarButtonType)e.NewValue); + } +} diff --git a/src/Wpf.Ui/TitleBar/TitleBarButton.cs b/src/Wpf.Ui/Controls/TitleBarButtonType.cs similarity index 93% rename from src/Wpf.Ui/TitleBar/TitleBarButton.cs rename to src/Wpf.Ui/Controls/TitleBarButtonType.cs index 7d02b9bfe..1e2196c3c 100644 --- a/src/Wpf.Ui/TitleBar/TitleBarButton.cs +++ b/src/Wpf.Ui/Controls/TitleBarButtonType.cs @@ -3,12 +3,12 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. -namespace Wpf.Ui.TitleBar; +namespace Wpf.Ui.Controls; /// /// Type of the Title Bar button. /// -internal enum TitleBarButton +public enum TitleBarButtonType { /// /// Unknown button. diff --git a/src/Wpf.Ui/Extensions/UiElementExtensions.cs b/src/Wpf.Ui/Extensions/UiElementExtensions.cs new file mode 100644 index 000000000..97e5fca0d --- /dev/null +++ b/src/Wpf.Ui/Extensions/UiElementExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Windows; + +namespace Wpf.Ui.Extensions; + +public static class UiElementExtensions +{ + /// + /// Do not call it outside of NCHITTEST, NCLBUTTONUP, NCLBUTTONDOWN messages! + /// + public static bool IsMouseOverElement(this UIElement element, IntPtr lParam) + { + // This method will be invoked very often and must be as simple as possible. + + if (lParam == IntPtr.Zero) + return false; + + var mousePosScreen = new Point(Get_X_LParam(lParam), Get_Y_LParam(lParam)); + var bounds = new Rect(new Point(), element.RenderSize); + var mousePosRelative = element.PointFromScreen(mousePosScreen); + return bounds.Contains(mousePosRelative); + } + + private static int Get_X_LParam(IntPtr lParam) + { + return (short)(lParam.ToInt32() & 0xFFFF); + } + + private static int Get_Y_LParam(IntPtr lParam) + { + return (short)(lParam.ToInt32() >> 16); + } +} diff --git a/src/Wpf.Ui/Styles/Controls/TitleBar.xaml b/src/Wpf.Ui/Styles/Controls/TitleBar.xaml index e0d1ee4b8..4dd48238b 100644 --- a/src/Wpf.Ui/Styles/Controls/TitleBar.xaml +++ b/src/Wpf.Ui/Styles/Controls/TitleBar.xaml @@ -10,60 +10,77 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:Wpf.Ui.Controls"> - - -