Skip to content

Commit de1d96a

Browse files
authored
feat: UX overhaul with custom window chrome and single-instance support (#13)
- Add custom title bar with Visual Studio purple branding (#68217A) - Implement square window corners using DWM API - Add custom minimize/close buttons (no maximize) - Add purple border around window - Update tab underline to match purple theme - Add single-instance support with mutex - Hide Settings tab (placeholder for future) - Bring existing instance to front when launching second instance
1 parent bec3ba7 commit de1d96a

File tree

4 files changed

+475
-133
lines changed

4 files changed

+475
-133
lines changed

src/CodingWithCalvin.VSToolbox/App.xaml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,64 @@
4040
<Setter Property="Padding" Value="8"/>
4141
<Setter Property="Opacity" Value="0.5"/>
4242
</Style>
43+
44+
<!-- Modern pill-style tab for RadioButton -->
45+
<Style x:Key="PillTabStyle" TargetType="RadioButton">
46+
<Setter Property="Background" Value="Transparent"/>
47+
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}"/>
48+
<Setter Property="BorderThickness" Value="0"/>
49+
<Setter Property="Padding" Value="12,8"/>
50+
<Setter Property="MinWidth" Value="0"/>
51+
<Setter Property="MinHeight" Value="0"/>
52+
<Setter Property="CornerRadius" Value="6"/>
53+
<Setter Property="Template">
54+
<Setter.Value>
55+
<ControlTemplate TargetType="RadioButton">
56+
<Grid x:Name="RootGrid">
57+
<Grid.RowDefinitions>
58+
<RowDefinition Height="*"/>
59+
<RowDefinition Height="Auto"/>
60+
</Grid.RowDefinitions>
61+
<VisualStateManager.VisualStateGroups>
62+
<VisualStateGroup x:Name="CheckStates">
63+
<VisualState x:Name="Checked">
64+
<VisualState.Setters>
65+
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}"/>
66+
<Setter Target="ActiveIndicator.Opacity" Value="1"/>
67+
</VisualState.Setters>
68+
</VisualState>
69+
<VisualState x:Name="Unchecked"/>
70+
<VisualState x:Name="Indeterminate"/>
71+
</VisualStateGroup>
72+
</VisualStateManager.VisualStateGroups>
73+
<Border
74+
x:Name="RootBorder"
75+
Grid.Row="0"
76+
Background="{TemplateBinding Background}"
77+
CornerRadius="{TemplateBinding CornerRadius}"
78+
Padding="{TemplateBinding Padding}">
79+
<ContentPresenter
80+
x:Name="ContentPresenter"
81+
Content="{TemplateBinding Content}"
82+
ContentTemplate="{TemplateBinding ContentTemplate}"
83+
Foreground="{TemplateBinding Foreground}"
84+
HorizontalAlignment="Center"
85+
VerticalAlignment="Center"/>
86+
</Border>
87+
<!-- Active indicator bar -->
88+
<Border
89+
x:Name="ActiveIndicator"
90+
Grid.Row="1"
91+
Height="3"
92+
Margin="4,4,4,0"
93+
CornerRadius="2"
94+
Background="#68217A"
95+
Opacity="0"/>
96+
</Grid>
97+
</ControlTemplate>
98+
</Setter.Value>
99+
</Setter>
100+
</Style>
43101
</ResourceDictionary>
44102
</Application.Resources>
45103
</Application>

src/CodingWithCalvin.VSToolbox/App.xaml.cs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Runtime.InteropServices;
2+
using System.Threading;
23
using CodingWithCalvin.VSToolbox.Services;
34
using Microsoft.UI;
45
using Microsoft.UI.Windowing;
@@ -7,19 +8,83 @@
78

89
namespace CodingWithCalvin.VSToolbox;
910

11+
// Windows API for setting window corner preference
12+
internal static class NativeMethods
13+
{
14+
[DllImport("dwmapi.dll")]
15+
internal static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
16+
17+
[DllImport("user32.dll")]
18+
internal static extern int GetWindowLong(IntPtr hwnd, int nIndex);
19+
20+
[DllImport("user32.dll")]
21+
internal static extern int SetWindowLong(IntPtr hwnd, int nIndex, int dwNewLong);
22+
23+
[DllImport("user32.dll")]
24+
internal static extern bool SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
25+
26+
internal const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
27+
internal const int DWMWCP_DONOTROUND = 1;
28+
internal const int DWMWA_CAPTION_COLOR = 35;
29+
internal const int DWMWA_BORDER_COLOR = 34;
30+
31+
internal const int GWL_STYLE = -16;
32+
internal const int WS_CAPTION = 0x00C00000;
33+
internal const int WS_THICKFRAME = 0x00040000;
34+
internal const uint SWP_FRAMECHANGED = 0x0020;
35+
internal const uint SWP_NOMOVE = 0x0002;
36+
internal const uint SWP_NOSIZE = 0x0001;
37+
internal const uint SWP_NOZORDER = 0x0004;
38+
39+
// For finding and showing existing window
40+
internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
41+
42+
[DllImport("user32.dll")]
43+
internal static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
44+
45+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
46+
internal static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
47+
48+
[DllImport("user32.dll")]
49+
internal static extern bool IsWindowVisible(IntPtr hWnd);
50+
51+
[DllImport("user32.dll")]
52+
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
53+
54+
[DllImport("user32.dll")]
55+
internal static extern bool SetForegroundWindow(IntPtr hWnd);
56+
57+
internal const int SW_RESTORE = 9;
58+
internal const int SW_SHOW = 5;
59+
}
60+
1061
public partial class App : Application
1162
{
63+
private const string MutexName = "CodingWithCalvin.VSToolbox.SingleInstance";
64+
private static Mutex? _mutex;
1265
private Window? _window;
1366
private AppWindow? _appWindow;
1467
private TrayIconService? _trayIconService;
1568

69+
public Window? MainWindow => _window;
70+
1671
public App()
1772
{
1873
InitializeComponent();
1974
}
2075

2176
protected override void OnLaunched(LaunchActivatedEventArgs e)
2277
{
78+
// Check for single instance
79+
_mutex = new Mutex(true, MutexName, out var createdNew);
80+
if (!createdNew)
81+
{
82+
// Another instance is already running - try to bring it to front
83+
BringExistingInstanceToFront();
84+
Environment.Exit(0);
85+
return;
86+
}
87+
2388
_window = new Window
2489
{
2590
Title = "Visual Studio Toolbox"
@@ -37,6 +102,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
37102
_appWindow.SetIcon(iconPath);
38103
}
39104

105+
// Configure custom title bar with square corners
106+
ConfigureCustomTitleBar();
107+
40108
// Set up the main content
41109
var rootFrame = new Frame();
42110
rootFrame.NavigationFailed += OnNavigationFailed;
@@ -57,6 +125,41 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
57125
_window.Activate();
58126
}
59127

128+
private void ConfigureCustomTitleBar()
129+
{
130+
if (_appWindow is null || _window is null) return;
131+
132+
// Get the window handle for native API calls
133+
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(_window);
134+
135+
// Set square corners using DWM API
136+
var cornerPreference = NativeMethods.DWMWCP_DONOTROUND;
137+
NativeMethods.DwmSetWindowAttribute(hwnd, NativeMethods.DWMWA_WINDOW_CORNER_PREFERENCE,
138+
ref cornerPreference, sizeof(int));
139+
140+
// Set caption and border color to purple (#68217A = 0x007A2168 in COLORREF BGR format)
141+
var purpleColor = 0x007A2168; // BGR format for #68217A
142+
NativeMethods.DwmSetWindowAttribute(hwnd, NativeMethods.DWMWA_CAPTION_COLOR,
143+
ref purpleColor, sizeof(int));
144+
NativeMethods.DwmSetWindowAttribute(hwnd, NativeMethods.DWMWA_BORDER_COLOR,
145+
ref purpleColor, sizeof(int));
146+
147+
// Remove the caption from window style to eliminate the title bar area
148+
var style = NativeMethods.GetWindowLong(hwnd, NativeMethods.GWL_STYLE);
149+
style &= ~NativeMethods.WS_CAPTION; // Remove caption
150+
NativeMethods.SetWindowLong(hwnd, NativeMethods.GWL_STYLE, style);
151+
NativeMethods.SetWindowPos(hwnd, IntPtr.Zero, 0, 0, 0, 0,
152+
NativeMethods.SWP_FRAMECHANGED | NativeMethods.SWP_NOMOVE | NativeMethods.SWP_NOSIZE | NativeMethods.SWP_NOZORDER);
153+
154+
// Make window borderless (no system title bar at all)
155+
if (_appWindow.Presenter is OverlappedPresenter presenter)
156+
{
157+
presenter.SetBorderAndTitleBar(false, false);
158+
presenter.IsResizable = true;
159+
presenter.IsMaximizable = false;
160+
}
161+
}
162+
60163
private void PositionWindowBottomRight(int width, int height)
61164
{
62165
if (_appWindow is null) return;
@@ -86,4 +189,29 @@ private void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
86189
{
87190
throw new InvalidOperationException($"Failed to load Page {e.SourcePageType.FullName}");
88191
}
192+
193+
private static void BringExistingInstanceToFront()
194+
{
195+
const string windowTitle = "Visual Studio Toolbox";
196+
IntPtr foundWindow = IntPtr.Zero;
197+
198+
NativeMethods.EnumWindows((hWnd, lParam) =>
199+
{
200+
var sb = new System.Text.StringBuilder(256);
201+
NativeMethods.GetWindowText(hWnd, sb, sb.Capacity);
202+
if (sb.ToString() == windowTitle)
203+
{
204+
foundWindow = hWnd;
205+
return false; // Stop enumeration
206+
}
207+
return true; // Continue enumeration
208+
}, IntPtr.Zero);
209+
210+
if (foundWindow != IntPtr.Zero)
211+
{
212+
// Show and bring the window to front
213+
NativeMethods.ShowWindow(foundWindow, NativeMethods.SW_RESTORE);
214+
NativeMethods.SetForegroundWindow(foundWindow);
215+
}
216+
}
89217
}

0 commit comments

Comments
 (0)