Skip to content

Commit 1daedc4

Browse files
committed
feat(ui): implement native rounded corners and enhance window styling
- Added Semi theme and related styles to App.axaml for improved UI. - Updated MainWindow and various dialog views to support rounded corners. - Refactored window initialization to use RoundedWindowHelper for consistent styling across platforms. - Enhanced logging and error dialogs for better user experience.
1 parent ab99f18 commit 1daedc4

13 files changed

Lines changed: 535 additions & 379 deletions

App.axaml

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
<Application xmlns="https://github.com/avaloniaui"
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3-
x:Class="FrameExtractor.App"
4-
RequestedThemeVariant="Default">
3+
xmlns:semi="https://irihi.tech/semi"
4+
x:Class="FrameExtractor.App">
55
<Application.Styles>
6-
<FluentTheme />
7-
<Style Selector="TextBox:focus">
8-
<Setter Property="Background" Value="Transparent"/>
9-
<Setter Property="BorderBrush" Value="Transparent"/>
10-
</Style>
6+
<semi:SemiTheme />
7+
<semi:SemiPopupAnimations />
8+
<semi:ColorPickerSemiTheme />
9+
<semi:DataGridSemiTheme />
10+
<semi:TreeDataGridSemiTheme />
11+
12+
<!-- <FluentTheme>
13+
<FluentTheme.Palettes>
14+
<ColorPaletteResources x:Key="Light" Accent="#ff0073cf" AltHigh="White" AltLow="White" AltMedium="White" AltMediumHigh="White" AltMediumLow="White" BaseHigh="Black" BaseLow="#ffcccccc" BaseMedium="#ff898989" BaseMediumHigh="#ff5d5d5d" BaseMediumLow="#ff737373" ChromeAltLow="#ff5d5d5d" ChromeBlackHigh="Black" ChromeBlackLow="#ffcccccc" ChromeBlackMedium="#ff5d5d5d" ChromeBlackMediumLow="#ff898989" ChromeDisabledHigh="#ffcccccc" ChromeDisabledLow="#ff898989" ChromeGray="#ff737373" ChromeHigh="#ffcccccc" ChromeLow="#ffececec" ChromeMedium="#ffe6e6e6" ChromeMediumLow="#ffececec" ChromeWhite="White" ListLow="#ffe6e6e6" ListMedium="#ffcccccc" RegionColor="#f8f6fbff" />
15+
<ColorPaletteResources x:Key="Dark" Accent="#ba007de1" AltHigh="Black" AltLow="Black" AltMedium="Black" AltMediumHigh="Black" AltMediumLow="Black" BaseHigh="White" BaseLow="#ff4c4c4c" BaseMedium="#ffa6a6a6" BaseMediumHigh="#ffbdbdbd" BaseMediumLow="#ff797979" ChromeAltLow="#ffbdbdbd" ChromeBlackHigh="Black" ChromeBlackLow="#ffbdbdbd" ChromeBlackMedium="Black" ChromeBlackMediumLow="Black" ChromeDisabledHigh="#ff4c4c4c" ChromeDisabledLow="#ffa6a6a6" ChromeGray="#ff909090" ChromeHigh="#ff909090" ChromeLow="#ff212121" ChromeMedium="#ff2c2c2c" ChromeMediumLow="#ff414141" ChromeWhite="White" ListLow="#ff2c2c2c" ListMedium="#ff4c4c4c" RegionColor="#ff1a1a1a" />
16+
</FluentTheme.Palettes>
17+
</FluentTheme> -->
18+
19+
<FluentTheme>
20+
<FluentTheme.Palettes>
21+
<ColorPaletteResources x:Key="Light" Accent="#ff8961cc" AltHigh="White" AltLow="White" AltMedium="White" AltMediumHigh="White" AltMediumLow="White" BaseHigh="Black" BaseLow="#ffeeceff" BaseMedium="#ffa987bc" BaseMediumHigh="#ff7b5890" BaseMediumLow="#ff9270a6" ChromeAltLow="#ff7b5890" ChromeBlackHigh="Black" ChromeBlackLow="#ffeeceff" ChromeBlackMedium="#ff7b5890" ChromeBlackMediumLow="#ffa987bc" ChromeDisabledHigh="#ffeeceff" ChromeDisabledLow="#ffa987bc" ChromeGray="#ff9270a6" ChromeHigh="#ffeeceff" ChromeLow="#fffeeaff" ChromeMedium="#fffbe4ff" ChromeMediumLow="#fffeeaff" ChromeWhite="White" ListLow="#fffbe4ff" ListMedium="#ffeeceff" RegionColor="#fffef6ff" />
22+
<ColorPaletteResources x:Key="Dark" Accent="#ff8961cc" AltHigh="#412561" AltLow="#412561" AltMedium="#412561" AltMediumHigh="#78697f" AltMediumLow="#78697f" BaseHigh="White" BaseLow="#ff64576b" BaseMedium="#ffb6aabc" BaseMediumHigh="#ffcbbfd0" BaseMediumLow="#ff8d8193" ChromeAltLow="#ffcbbfd0" ChromeBlackHigh="Black" ChromeBlackLow="#ffcbbfd0" ChromeBlackMedium="Black" ChromeBlackMediumLow="Black" ChromeDisabledHigh="#ff64576b" ChromeDisabledLow="#ffb6aabc" ChromeGray="#ffa295a8" ChromeHigh="#ffa295a8" ChromeLow="#ff332041" ChromeMedium="#ff3f2e4b" ChromeMediumLow="#ff584960" ChromeWhite="White" ListLow="#ff3f2e4b" ListMedium="#ff64576b" RegionColor="#ff262738" />
23+
</FluentTheme.Palettes>
24+
</FluentTheme>
1125
</Application.Styles>
1226
</Application>

App.axaml.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using Avalonia.Controls.ApplicationLifetimes;
33
using Avalonia.Data.Core.Plugins;
44
using Avalonia.Markup.Xaml;
5-
5+
using FrameExtractor.ViewModels;
66
using FrameExtractor.Views;
77

88
namespace FrameExtractor;
@@ -20,7 +20,10 @@ public override void OnFrameworkInitializationCompleted()
2020

2121
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
2222
{
23-
desktop.MainWindow = new MainWindow();
23+
desktop.MainWindow = new MainWindow
24+
{
25+
DataContext = new MainWindowViewModel(),
26+
};
2427
}
2528

2629
base.OnFrameworkInitializationCompleted();

FrameExtractor.csproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@
1414
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.2" />
1515
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.2"/>
1616
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.2"/>
17+
<PackageReference Include="Semi.Avalonia" Version="11.2.1.9" />
18+
<PackageReference Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
19+
<PackageReference Include="Semi.Avalonia.ColorPicker" Version="11.2.1.9" />
20+
<PackageReference Include="Semi.Avalonia.DataGrid" Version="11.2.1.9" />
21+
<PackageReference Include="Semi.Avalonia.Dock" Version="11.3.2" />
22+
<PackageReference Include="Semi.Avalonia.TreeDataGrid" Version="11.0.10.4" />
23+
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
24+
1725
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
1826
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.2">
1927
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
2028
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
2129
</PackageReference>
22-
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
30+
2331
<PackageReference Include="ReactiveUI" Version="20.4.1" />
2432
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
2533
<PackageReference Include="System.IO.Compression" Version="4.3.0" />

Helpers/RoundedWindowHelper.cs

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,183 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using Avalonia.Controls;
4+
using Avalonia.Platform;
5+
using FrameExtractor.Services;
6+
17
namespace FrameExtractor.Helpers;
28

3-
public class RoundedWindowHelper
9+
public static class RoundedWindowHelper
410
{
11+
#region Windows 11 API
12+
[DllImport("dwmapi.dll")]
13+
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
14+
15+
private const int DwmwaWindowCornerPreference = 33;
16+
private const int DwmwcpRound = 2;
17+
private const int DwmwcpRoundsmall = 3;
18+
#endregion
19+
20+
#region macOS API
21+
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
22+
private static extern IntPtr objc_getClass(string className);
23+
24+
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
25+
private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector);
26+
27+
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
28+
private static extern void objc_msgSend_double(IntPtr receiver, IntPtr selector, double value);
29+
30+
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
31+
private static extern IntPtr sel_registerName(string name);
32+
#endregion
33+
34+
private static bool TryEnableWindowsRoundedCorners(IntPtr hwnd, bool smallRadius = false)
35+
{
36+
try
37+
{
38+
int preference = smallRadius ? DwmwcpRoundsmall : DwmwcpRound;
39+
int result = DwmSetWindowAttribute(
40+
hwnd,
41+
DwmwaWindowCornerPreference,
42+
ref preference,
43+
sizeof(int)
44+
);
45+
46+
bool success = result == 0;
47+
if (success)
48+
{
49+
Logger.Info("[Windows 11] Native rounded corners enabled successfully");
50+
}
51+
return success;
52+
}
53+
catch (Exception ex)
54+
{
55+
Logger.Info($"[Windows 11] Failed to enable rounded corners: {ex.Message}");
56+
return false;
57+
}
58+
}
59+
60+
private static bool TryEnableMacOsRoundedCorners(IntPtr nsWindow, double radius = 10.0)
61+
{
62+
try
63+
{
64+
// NSWindow.contentView.layer.cornerRadius = radius
65+
var contentView = objc_msgSend(nsWindow, sel_registerName("contentView"));
66+
if (contentView == IntPtr.Zero) return false;
67+
68+
var layer = objc_msgSend(contentView, sel_registerName("layer"));
69+
if (layer == IntPtr.Zero) return false;
70+
71+
// Set corner radius
72+
objc_msgSend_double(layer, sel_registerName("setCornerRadius:"), radius);
73+
74+
// Enable masksToBounds
75+
objc_msgSend(layer, sel_registerName("setMasksToBounds:"));
76+
77+
Logger.Info($"[macOS] Native rounded corners enabled (radius: {radius})");
78+
return true;
79+
}
80+
catch (Exception ex)
81+
{
82+
Logger.Info($"[macOS] Failed to enable rounded corners: {ex.Message}");
83+
return false;
84+
}
85+
}
86+
87+
public static void SetupRoundedWindow(Window window, bool useNativeChrome = false, double cornerRadius = 16.0)
88+
{
89+
window.ExtendClientAreaToDecorationsHint = true;
90+
window.ExtendClientAreaChromeHints = useNativeChrome
91+
? ExtendClientAreaChromeHints.PreferSystemChrome
92+
: ExtendClientAreaChromeHints.NoChrome;
93+
94+
window.Opened += (s, e) =>
95+
{
96+
var handle = window.TryGetPlatformHandle();
97+
if (handle == null)
98+
{
99+
Logger.Info("[Platform] Could not get platform handle");
100+
return;
101+
}
102+
103+
bool roundedEnabled = false;
104+
105+
// Windows 11
106+
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) &&
107+
handle.HandleDescriptor == "HWND")
108+
{
109+
Logger.Info("[Platform] Detected Windows 11");
110+
roundedEnabled = TryEnableWindowsRoundedCorners(handle.Handle, smallRadius: cornerRadius < 10);
111+
112+
if (roundedEnabled && !useNativeChrome)
113+
{
114+
window.SystemDecorations = SystemDecorations.BorderOnly;
115+
}
116+
}
117+
// macOS
118+
else if (OperatingSystem.IsMacOS() && handle.HandleDescriptor == "NSWindow")
119+
{
120+
Logger.Info("[Platform] Detected macOS");
121+
roundedEnabled = TryEnableMacOsRoundedCorners(handle.Handle, cornerRadius);
122+
123+
if (!useNativeChrome)
124+
{
125+
window.SystemDecorations = SystemDecorations.None;
126+
}
127+
}
128+
// Other platforms
129+
else
130+
{
131+
var platform = OperatingSystem.IsWindows() ? "Windows 10 or older" :
132+
OperatingSystem.IsLinux() ? "Linux" : "Unknown";
133+
Logger.Info($"[Platform] Detected {platform} - native rounded corners not supported, keeping square corners");
134+
135+
if (!useNativeChrome)
136+
{
137+
window.SystemDecorations = SystemDecorations.None;
138+
}
139+
}
140+
141+
if (!roundedEnabled && !useNativeChrome)
142+
{
143+
Logger.Info("[Platform] Window will have square corners with custom chrome");
144+
}
145+
};
146+
}
147+
148+
public static bool IsNativeRoundedCornersSupported()
149+
{
150+
// Windows 11
151+
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
152+
{
153+
Logger.Info("[Check] Windows 11 detected - native rounded corners supported");
154+
return true;
155+
}
156+
157+
// macOS
158+
if (OperatingSystem.IsMacOS())
159+
{
160+
Logger.Info("[Check] macOS detected - native rounded corners supported");
161+
return true;
162+
}
163+
164+
// Other platforms
165+
var platform = OperatingSystem.IsWindows() ? "Windows 10 or older" :
166+
OperatingSystem.IsLinux() ? "Linux" : "Unknown platform";
167+
Logger.Info($"[Check] {platform} - native rounded corners not supported");
168+
return false;
169+
}
5170

171+
public static string GetPlatformName()
172+
{
173+
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
174+
return "Windows 11";
175+
if (OperatingSystem.IsWindows())
176+
return "Windows 10 or older";
177+
if (OperatingSystem.IsMacOS())
178+
return "macOS";
179+
if (OperatingSystem.IsLinux())
180+
return "Linux";
181+
return "Unknown";
182+
}
6183
}

ViewModels/MainWindowViewModel.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,8 @@ public MainWindowViewModel()
6262

6363
private void ShowLogDialog()
6464
{
65-
var mainWindow = GetMainWindow();
66-
if (mainWindow != null)
67-
{
68-
var logDialog = new FrameExtractor.Views.LogDialog();
69-
logDialog.Show(mainWindow);
70-
}
65+
var logDialog = new FrameExtractor.Views.LogDialog();
66+
logDialog.Show();
7167
}
7268

7369
public ReactiveCommand<Unit, Unit> ShowLogCommand { get; }

Views/DownloadDialog.axaml

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,36 @@
77
Title="Downloading FFmpeg"
88
Width="400" Height="120"
99
WindowStartupLocation="CenterOwner"
10-
Background="Transparent"
11-
CanResize="False">
10+
CanResize="False"
11+
SystemDecorations="None"
12+
ExtendClientAreaChromeHints="NoChrome"
13+
ExtendClientAreaToDecorationsHint="True">
1214

13-
<Border CornerRadius="20" Background="Black" ClipToBounds="True">
14-
<Grid RowDefinitions="Auto,*,Auto">
15-
<!-- Title Bar -->
16-
<Grid Grid.Row="0" Height="30" Background="Transparent">
17-
<TextBlock Text="Downloading FFmpeg"
18-
VerticalAlignment="Center"
19-
HorizontalAlignment="Center"
20-
FontWeight="SemiBold"
21-
Foreground="White"
22-
FontSize="14"/>
23-
</Grid>
15+
<Grid RowDefinitions="Auto,*,Auto">
16+
<!-- Title Bar -->
17+
<Grid Grid.Row="0" Height="30" Background="Transparent">
18+
<TextBlock Text="Downloading FFmpeg"
19+
VerticalAlignment="Center"
20+
HorizontalAlignment="Center"
21+
FontWeight="SemiBold"
22+
FontSize="14"/>
23+
</Grid>
2424

25-
<!-- Content -->
26-
<StackPanel Grid.Row="1" Spacing="15" Margin="20">
27-
<TextBlock x:Name="StatusLabel"
28-
Text="Initializing..."
29-
Foreground="White"
30-
HorizontalAlignment="Center"
31-
FontSize="12"/>
25+
<!-- Content -->
26+
<StackPanel Grid.Row="1" Spacing="15" Margin="20">
27+
<TextBlock x:Name="StatusLabel"
28+
Text="Initializing..."
29+
HorizontalAlignment="Center"
30+
FontSize="12"/>
3231

33-
<ProgressBar x:Name="ProgressBar"
34-
Height="8"
35-
Minimum="0"
36-
Maximum="100"
37-
Value="0"/>
38-
</StackPanel>
32+
<ProgressBar x:Name="ProgressBar"
33+
Height="8"
34+
Minimum="0"
35+
Maximum="100"
36+
Value="0"/>
37+
</StackPanel>
3938

40-
<!-- Bottom spacing -->
41-
<Border Grid.Row="2" Height="10"/>
42-
</Grid>
43-
</Border>
39+
<!-- Bottom spacing -->
40+
<Border Grid.Row="2" Height="10"/>
41+
</Grid>
4442
</Window>

Views/DownloadDialog.axaml.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Avalonia.Controls;
22
using Avalonia.Threading;
3+
using FrameExtractor.Helpers;
34

45
namespace FrameExtractor.Views;
56

@@ -8,8 +9,8 @@ public partial class DownloadDialog : Window
89
public DownloadDialog()
910
{
1011
InitializeComponent();
11-
ExtendClientAreaToDecorationsHint = true;
12-
ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.NoChrome;
12+
13+
RoundedWindowHelper.SetupRoundedWindow(this, useNativeChrome: false, cornerRadius: 16);
1314
}
1415

1516
public void UpdateProgress(int percent, string statusText)

0 commit comments

Comments
 (0)