diff --git a/src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml b/src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml
new file mode 100644
index 0000000..c1e9948
--- /dev/null
+++ b/src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml.cs b/src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml.cs
new file mode 100644
index 0000000..768934d
--- /dev/null
+++ b/src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml.cs
@@ -0,0 +1,88 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using WpfButton = System.Windows.Controls.Button;
+using WpfUserControl = System.Windows.Controls.UserControl;
+
+namespace SwitchifyPc.App.Chrome;
+
+public partial class SwitchifyTitleBar : WpfUserControl
+{
+ public static readonly DependencyProperty TitleTextProperty = DependencyProperty.Register(
+ nameof(TitleText),
+ typeof(string),
+ typeof(SwitchifyTitleBar),
+ new PropertyMetadata(string.Empty));
+
+ public static readonly DependencyProperty ShowMinimizeButtonProperty = DependencyProperty.Register(
+ nameof(ShowMinimizeButton),
+ typeof(bool),
+ typeof(SwitchifyTitleBar),
+ new PropertyMetadata(true));
+
+ public SwitchifyTitleBar()
+ {
+ InitializeComponent();
+ }
+
+ public string TitleText
+ {
+ get => (string)GetValue(TitleTextProperty);
+ set => SetValue(TitleTextProperty, value);
+ }
+
+ public bool ShowMinimizeButton
+ {
+ get => (bool)GetValue(ShowMinimizeButtonProperty);
+ set => SetValue(ShowMinimizeButtonProperty, value);
+ }
+
+ private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ClickCount != 1 || IsInsideButton(e.OriginalSource as DependencyObject))
+ {
+ return;
+ }
+
+ Window? window = Window.GetWindow(this);
+ if (window is null) return;
+
+ try
+ {
+ window.DragMove();
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ private void MinimizeButton_Click(object sender, RoutedEventArgs e)
+ {
+ Window? window = Window.GetWindow(this);
+ if (window is not null)
+ {
+ window.WindowState = WindowState.Minimized;
+ }
+ }
+
+ private void CloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ Window.GetWindow(this)?.Close();
+ }
+
+ private static bool IsInsideButton(DependencyObject? source)
+ {
+ while (source is not null)
+ {
+ if (source is WpfButton)
+ {
+ return true;
+ }
+
+ source = VisualTreeHelper.GetParent(source);
+ }
+
+ return false;
+ }
+}
diff --git a/src/SwitchifyPc.App/MainWindow.xaml b/src/SwitchifyPc.App/MainWindow.xaml
index 1e8539f..07134ae 100644
--- a/src/SwitchifyPc.App/MainWindow.xaml
+++ b/src/SwitchifyPc.App/MainWindow.xaml
@@ -4,6 +4,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:SwitchifyPc.Core.Ui;assembly=SwitchifyPc.Core"
+ xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
+ xmlns:chrome="clr-namespace:SwitchifyPc.App.Chrome"
mc:Ignorable="d"
Title="Switchify PC"
MinHeight="520"
@@ -12,9 +14,18 @@
Width="920"
Icon="Assets/icon.ico"
FontFamily="Segoe UI"
- Background="#F5F4F7"
+ Background="#D32F2F"
+ WindowStyle="None"
WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance Type=ui:MainWindowViewModel, IsDesignTimeCreatable=True}">
+
+
+
+
@@ -213,8 +224,21 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
diff --git a/src/SwitchifyPc.App/SettingsWindow.xaml b/src/SwitchifyPc.App/SettingsWindow.xaml
index 550b0bc..c6e586d 100644
--- a/src/SwitchifyPc.App/SettingsWindow.xaml
+++ b/src/SwitchifyPc.App/SettingsWindow.xaml
@@ -4,6 +4,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:SwitchifyPc.Core.Ui;assembly=SwitchifyPc.Core"
+ xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
+ xmlns:chrome="clr-namespace:SwitchifyPc.App.Chrome"
mc:Ignorable="d"
Title="Switchify PC settings"
MinHeight="560"
@@ -13,9 +15,18 @@
ResizeMode="NoResize"
Icon="Assets/icon.ico"
FontFamily="Segoe UI"
- Background="#F5F4F7"
+ Background="#D32F2F"
+ WindowStyle="None"
WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance Type=ui:SettingsViewModel, IsDesignTimeCreatable=True}">
+
+
+
+
@@ -235,23 +246,34 @@
-
+
+
-
+
+
+
+
+
+ Foreground="White" />
-
+
diff --git a/src/SwitchifyPc.Tests/ButtonStyleTests.cs b/src/SwitchifyPc.Tests/ButtonStyleTests.cs
index e05d6e7..e4ea3cc 100644
--- a/src/SwitchifyPc.Tests/ButtonStyleTests.cs
+++ b/src/SwitchifyPc.Tests/ButtonStyleTests.cs
@@ -8,6 +8,7 @@
namespace SwitchifyPc.Tests;
+[Collection(WpfTestCollection.Name)]
public sealed class ButtonStyleTests
{
[Fact]
diff --git a/src/SwitchifyPc.Tests/MainWindowTests.cs b/src/SwitchifyPc.Tests/MainWindowTests.cs
index 6017abf..705524c 100644
--- a/src/SwitchifyPc.Tests/MainWindowTests.cs
+++ b/src/SwitchifyPc.Tests/MainWindowTests.cs
@@ -1,10 +1,16 @@
using System.Threading;
using System.Windows;
+using System.Windows.Automation;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Shell;
using SwitchifyPc.App;
using SwitchifyPc.Core.Ui;
+using WpfButton = System.Windows.Controls.Button;
namespace SwitchifyPc.Tests;
+[Collection(WpfTestCollection.Name)]
public sealed class MainWindowTests
{
[Fact]
@@ -31,6 +37,30 @@ public void MainWindowLoadsWithAndroidDownloadPrompt()
});
}
+ [Fact]
+ public void MainWindowUsesCustomChrome()
+ {
+ RunOnSta(() =>
+ {
+ MainWindow window = new(new MainWindowViewModel());
+ try
+ {
+ window.Show();
+ window.UpdateLayout();
+
+ Assert.Equal(WindowStyle.None, window.WindowStyle);
+ Assert.NotNull(WindowChrome.GetWindowChrome(window));
+ Assert.Contains("Switchify PC", TextBlocks(window));
+ Assert.NotNull(ButtonByAutomationName(window, "Minimize"));
+ Assert.NotNull(ButtonByAutomationName(window, "Close"));
+ }
+ finally
+ {
+ window.Close();
+ }
+ });
+ }
+
private static void RunOnSta(Action action)
{
Exception? exception = null;
@@ -52,4 +82,41 @@ private static void RunOnSta(Action action)
if (exception is not null) throw exception;
}
+
+ private static IReadOnlyList TextBlocks(DependencyObject root)
+ {
+ List text = [];
+ Collect(root, node =>
+ {
+ if (node is TextBlock textBlock)
+ {
+ text.Add(textBlock.Text);
+ }
+ });
+ return text;
+ }
+
+ private static WpfButton? ButtonByAutomationName(DependencyObject root, string name)
+ {
+ WpfButton? result = null;
+ Collect(root, node =>
+ {
+ if (result is null &&
+ node is WpfButton button &&
+ AutomationProperties.GetName(button) == name)
+ {
+ result = button;
+ }
+ });
+ return result;
+ }
+
+ private static void Collect(DependencyObject node, Action visit)
+ {
+ visit(node);
+ for (int index = 0; index < VisualTreeHelper.GetChildrenCount(node); index++)
+ {
+ Collect(VisualTreeHelper.GetChild(node, index), visit);
+ }
+ }
}
diff --git a/src/SwitchifyPc.Tests/SettingsWindowTests.cs b/src/SwitchifyPc.Tests/SettingsWindowTests.cs
index e5412a1..41f79bb 100644
--- a/src/SwitchifyPc.Tests/SettingsWindowTests.cs
+++ b/src/SwitchifyPc.Tests/SettingsWindowTests.cs
@@ -1,13 +1,17 @@
using System.Threading;
using System.Windows;
+using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Media;
+using System.Windows.Shell;
using SwitchifyPc.App;
using SwitchifyPc.Core.Ui;
using SwitchifyPc.Core.Updates;
+using WpfButton = System.Windows.Controls.Button;
namespace SwitchifyPc.Tests;
+[Collection(WpfTestCollection.Name)]
public sealed class SettingsWindowTests
{
[Fact]
@@ -64,6 +68,31 @@ public void SettingsWindowShowsSeparateMouseRepeatIntervals()
});
}
+ [Fact]
+ public void SettingsWindowUsesCustomChrome()
+ {
+ RunOnSta(() =>
+ {
+ SettingsWindow window = new(new SettingsViewModel());
+ try
+ {
+ window.Show();
+ window.UpdateLayout();
+
+ Assert.Equal(WindowStyle.None, window.WindowStyle);
+ Assert.Equal(ResizeMode.NoResize, window.ResizeMode);
+ Assert.NotNull(WindowChrome.GetWindowChrome(window));
+ Assert.Contains("Switchify PC settings", TextBlocks(window));
+ Assert.NotNull(ButtonByAutomationName(window, "Minimize"));
+ Assert.NotNull(ButtonByAutomationName(window, "Close"));
+ }
+ finally
+ {
+ window.Close();
+ }
+ });
+ }
+
private static void RunOnSta(Action action)
{
Exception? exception = null;
@@ -105,4 +134,28 @@ private static void CollectTextBlocks(DependencyObject node, List text)
CollectTextBlocks(VisualTreeHelper.GetChild(node, index), text);
}
}
+
+ private static WpfButton? ButtonByAutomationName(DependencyObject root, string name)
+ {
+ WpfButton? result = null;
+ Collect(root, node =>
+ {
+ if (result is null &&
+ node is WpfButton button &&
+ AutomationProperties.GetName(button) == name)
+ {
+ result = button;
+ }
+ });
+ return result;
+ }
+
+ private static void Collect(DependencyObject node, Action visit)
+ {
+ visit(node);
+ for (int index = 0; index < VisualTreeHelper.GetChildrenCount(node); index++)
+ {
+ Collect(VisualTreeHelper.GetChild(node, index), visit);
+ }
+ }
}
diff --git a/src/SwitchifyPc.Tests/SwitchifyTitleBarTests.cs b/src/SwitchifyPc.Tests/SwitchifyTitleBarTests.cs
new file mode 100644
index 0000000..586833e
--- /dev/null
+++ b/src/SwitchifyPc.Tests/SwitchifyTitleBarTests.cs
@@ -0,0 +1,194 @@
+using System.Threading;
+using System.Windows;
+using System.Windows.Automation;
+using System.Windows.Controls;
+using System.Windows.Media;
+using SwitchifyPc.App.Chrome;
+using WpfButton = System.Windows.Controls.Button;
+
+namespace SwitchifyPc.Tests;
+
+[Collection(WpfTestCollection.Name)]
+public sealed class SwitchifyTitleBarTests
+{
+ [Fact]
+ public void ExposesTitleText()
+ {
+ RunOnSta(() =>
+ {
+ SwitchifyTitleBar titleBar = new()
+ {
+ TitleText = "Switchify PC test"
+ };
+ Window window = new()
+ {
+ Content = titleBar,
+ Width = 320,
+ Height = 120
+ };
+ try
+ {
+ window.Show();
+ window.UpdateLayout();
+
+ Assert.Contains("Switchify PC test", TextBlocks(window));
+ }
+ finally
+ {
+ window.Close();
+ }
+ });
+ }
+
+ [Fact]
+ public void MinimizeButtonSetsParentWindowState()
+ {
+ RunOnSta(() =>
+ {
+ SwitchifyTitleBar titleBar = new();
+ Window window = new()
+ {
+ Content = titleBar,
+ Width = 320,
+ Height = 120
+ };
+ try
+ {
+ window.Show();
+ window.UpdateLayout();
+
+ WpfButton minimize = Assert.IsType(ButtonByAutomationName(window, "Minimize"));
+ minimize.RaiseEvent(new RoutedEventArgs(WpfButton.ClickEvent));
+
+ Assert.Equal(WindowState.Minimized, window.WindowState);
+ }
+ finally
+ {
+ window.Close();
+ }
+ });
+ }
+
+ [Fact]
+ public void CloseButtonInvokesParentWindowClose()
+ {
+ RunOnSta(() =>
+ {
+ bool closingCalled = false;
+ SwitchifyTitleBar titleBar = new();
+ Window window = new()
+ {
+ Content = titleBar,
+ Width = 320,
+ Height = 120
+ };
+ window.Closing += (_, _) => closingCalled = true;
+ try
+ {
+ window.Show();
+ window.UpdateLayout();
+
+ WpfButton close = Assert.IsType(ButtonByAutomationName(window, "Close"));
+ close.RaiseEvent(new RoutedEventArgs(WpfButton.ClickEvent));
+
+ Assert.True(closingCalled);
+ }
+ finally
+ {
+ if (window.IsVisible)
+ {
+ window.Close();
+ }
+ }
+ });
+ }
+
+ [Fact]
+ public void CanHideMinimizeButton()
+ {
+ RunOnSta(() =>
+ {
+ SwitchifyTitleBar titleBar = new()
+ {
+ ShowMinimizeButton = false
+ };
+ Window window = new()
+ {
+ Content = titleBar,
+ Width = 320,
+ Height = 120
+ };
+ try
+ {
+ window.Show();
+ window.UpdateLayout();
+
+ WpfButton minimize = Assert.IsType(ButtonByAutomationName(window, "Minimize"));
+ Assert.Equal(Visibility.Collapsed, minimize.Visibility);
+ }
+ finally
+ {
+ window.Close();
+ }
+ });
+ }
+
+ private static void RunOnSta(Action action)
+ {
+ Exception? exception = null;
+ Thread thread = new(() =>
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception error)
+ {
+ exception = error;
+ }
+ });
+
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ thread.Join();
+
+ if (exception is not null) throw exception;
+ }
+
+ private static IReadOnlyList TextBlocks(DependencyObject root)
+ {
+ List text = [];
+ Collect(root, node =>
+ {
+ if (node is TextBlock textBlock)
+ {
+ text.Add(textBlock.Text);
+ }
+ });
+ return text;
+ }
+
+ private static WpfButton? ButtonByAutomationName(DependencyObject root, string name)
+ {
+ WpfButton? result = null;
+ Collect(root, node =>
+ {
+ if (result is null &&
+ node is WpfButton button &&
+ AutomationProperties.GetName(button) == name)
+ {
+ result = button;
+ }
+ });
+ return result;
+ }
+
+ private static void Collect(DependencyObject node, Action visit)
+ {
+ visit(node);
+ for (int index = 0; index < VisualTreeHelper.GetChildrenCount(node); index++)
+ {
+ Collect(VisualTreeHelper.GetChild(node, index), visit);
+ }
+ }
+}
diff --git a/src/SwitchifyPc.Tests/WpfTestCollection.cs b/src/SwitchifyPc.Tests/WpfTestCollection.cs
new file mode 100644
index 0000000..ab5e977
--- /dev/null
+++ b/src/SwitchifyPc.Tests/WpfTestCollection.cs
@@ -0,0 +1,9 @@
+using Xunit;
+
+namespace SwitchifyPc.Tests;
+
+[CollectionDefinition(Name, DisableParallelization = true)]
+public sealed class WpfTestCollection
+{
+ public const string Name = "WPF tests";
+}