This commit is contained in:
lincube
2026-03-20 18:05:42 +08:00
parent 65a3cf832a
commit 20cd6041a7
15 changed files with 170 additions and 30 deletions

View File

@@ -33,7 +33,7 @@ public sealed class PluginDesktopComponentContext
ComponentId = componentId.Trim(); ComponentId = componentId.Trim();
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim(); PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
CellSize = Math.Max(1, cellSize); CellSize = Math.Max(1, cellSize);
GlobalCornerRadiusScale = Math.Max(0.1d, globalCornerRadiusScale); GlobalCornerRadiusScale = Math.Max(0d, globalCornerRadiusScale);
CornerRadiusTokens = cornerRadiusTokens; CornerRadiusTokens = cornerRadiusTokens;
PluginSettings = pluginSettings; PluginSettings = pluginSettings;
} }

View File

@@ -3,9 +3,8 @@ namespace LanMountainDesktop.Settings.Core;
public static class GlobalAppearanceSettings public static class GlobalAppearanceSettings
{ {
public const double DefaultCornerRadiusScale = 1.0; public const double DefaultCornerRadiusScale = 1.0;
public const double MinimumCornerRadiusScale = 0.70; public const double MinimumCornerRadiusScale = 0.0;
public const double MaximumCornerRadiusScale = 1.40; public const double MaximumCornerRadiusScale = 2.50;
public const double CornerRadiusScaleStep = 0.05;
public static double NormalizeCornerRadiusScale(double value) public static double NormalizeCornerRadiusScale(double value)
{ {
@@ -14,7 +13,6 @@ public static class GlobalAppearanceSettings
return DefaultCornerRadiusScale; return DefaultCornerRadiusScale;
} }
var clamped = Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale); return Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale);
return Math.Round(clamped / CornerRadiusScaleStep, MidpointRounding.AwayFromZero) * CornerRadiusScaleStep;
} }
} }

View File

@@ -0,0 +1,67 @@
using Avalonia;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.Shared.Contracts;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class CornerRadiusScaleTests
{
[Theory]
[InlineData(-1d, 0d)]
[InlineData(0d, 0d)]
[InlineData(0.33d, 0.33d)]
[InlineData(1.234d, 1.234d)]
[InlineData(2.5d, 2.5d)]
[InlineData(3d, 2.5d)]
public void NormalizeCornerRadiusScale_ClampsWithoutSnapping(double input, double expected)
{
Assert.Equal(expected, GlobalAppearanceSettings.NormalizeCornerRadiusScale(input), 3);
}
[Fact]
public void NormalizeCornerRadiusScale_UsesDefaultForInvalidValues()
{
Assert.Equal(
GlobalAppearanceSettings.DefaultCornerRadiusScale,
GlobalAppearanceSettings.NormalizeCornerRadiusScale(double.NaN),
3);
Assert.Equal(
GlobalAppearanceSettings.DefaultCornerRadiusScale,
GlobalAppearanceSettings.NormalizeCornerRadiusScale(double.PositiveInfinity),
3);
}
[Fact]
public void PluginDesktopComponentContext_AllowsZeroRadiusScaling()
{
var context = new PluginDesktopComponentContext(
new PluginManifest("plugin.id", "Plugin Name", "plugin.dll"),
"C:\\Plugins\\plugin.id",
"C:\\Data\\plugin.id",
new NullServiceProvider(),
new Dictionary<string, object?>(),
"component-1",
null,
96d,
0d,
new AppearanceCornerRadiusTokens(
new CornerRadius(6),
new CornerRadius(10),
new CornerRadius(14),
new CornerRadius(18),
new CornerRadius(24),
new CornerRadius(30),
new CornerRadius(36)));
Assert.Equal(0d, context.GlobalCornerRadiusScale, 3);
Assert.Equal(0d, context.ResolveScaledCornerRadius(12d), 3);
Assert.Equal(0d, context.ResolveScaledCornerRadius(12d, 8d, 18d), 3);
}
private sealed class NullServiceProvider : IServiceProvider
{
public object? GetService(Type serviceType) => null;
}
}

View File

@@ -0,0 +1,58 @@
using Avalonia;
using Avalonia.Controls;
using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Shared.Contracts;
using LanMountainDesktop.Views.Components;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests
{
[Fact]
public void LegacyCellSizeResolver_AppliesGlobalCornerRadiusScale()
{
var registration = new DesktopComponentRuntimeRegistration(
componentId: "test.component",
displayNameLocalizationKey: null,
controlFactory: () => new Border(),
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.30, 10, 40));
var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(registration.CornerRadiusResolver);
var resolved = resolver(CreateChromeContext(cellSize: 120, globalScale: 2.0));
Assert.Equal(72.0, resolved, 3);
}
[Fact]
public void ChromeContextResolver_IsNotDoubleScaledByRegistrationWrapper()
{
var registration = new DesktopComponentRuntimeRegistration(
componentId: "test.component",
displayNameLocalizationKey: null,
controlFactory: _ => new Border(),
cornerRadiusResolver: chromeContext => chromeContext.CellSize + chromeContext.GlobalCornerRadiusScale);
var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(registration.CornerRadiusResolver);
var resolved = resolver(CreateChromeContext(cellSize: 50, globalScale: 2.5));
Assert.Equal(52.5, resolved, 3);
}
private static ComponentChromeContext CreateChromeContext(double cellSize, double globalScale)
{
return new ComponentChromeContext(
ComponentId: "test.component",
PlacementId: null,
CellSize: cellSize,
GlobalCornerRadiusScale: globalScale,
CornerRadiusTokens: new AppearanceCornerRadiusTokens(
new CornerRadius(6),
new CornerRadius(10),
new CornerRadius(14),
new CornerRadius(18),
new CornerRadius(24),
new CornerRadius(30),
new CornerRadius(36)));
}
}

View File

@@ -313,7 +313,7 @@
"settings.components.spacing_relaxed": "Relaxed", "settings.components.spacing_relaxed": "Relaxed",
"settings.components.corner_radius.header": "Corner Design", "settings.components.corner_radius.header": "Corner Design",
"settings.components.corner_radius.label": "Component Corner Radius", "settings.components.corner_radius.label": "Component Corner Radius",
"settings.components.corner_radius.description": "Adjust the shared corner radius used by component containers, and expand the internal safe area with it.", "settings.components.corner_radius.description": "Adjust the shared corner radius from a square edge to a capsule-like shape, and expand the internal safe area with it.",
"settings.update.title": "Update", "settings.update.title": "Update",
"settings.update.current_version_label": "Current Version", "settings.update.current_version_label": "Current Version",
"settings.update.latest_version_label": "Latest Release", "settings.update.latest_version_label": "Latest Release",

View File

@@ -312,7 +312,7 @@
"settings.components.spacing_relaxed": "宽松", "settings.components.spacing_relaxed": "宽松",
"settings.components.corner_radius.header": "圆角设计", "settings.components.corner_radius.header": "圆角设计",
"settings.components.corner_radius.label": "组件圆角", "settings.components.corner_radius.label": "组件圆角",
"settings.components.corner_radius.description": "统一调整组件容器圆角,并随圆角增大同步扩展内部安全区。", "settings.components.corner_radius.description": "组件容器圆角从直角连续调到接近胶囊的形态,并随圆角增大同步扩展内部安全区。",
"settings.update.title": "更新", "settings.update.title": "更新",
"settings.update.current_version_label": "当前版本", "settings.update.current_version_label": "当前版本",
"settings.update.latest_version_label": "最新发布", "settings.update.latest_version_label": "最新发布",

View File

@@ -1010,6 +1010,10 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale; private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale;
public double GlobalCornerRadiusMinimum => GlobalAppearanceSettings.MinimumCornerRadiusScale;
public double GlobalCornerRadiusMaximum => GlobalAppearanceSettings.MaximumCornerRadiusScale;
[ObservableProperty] [ObservableProperty]
private string _componentRadiusHeader = string.Empty; private string _componentRadiusHeader = string.Empty;
@@ -1129,7 +1133,9 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
SpacingPresetLabel = L("settings.components.spacing_label", "Component Spacing"); SpacingPresetLabel = L("settings.components.spacing_label", "Component Spacing");
ComponentRadiusHeader = L("settings.components.corner_radius.header", "Corner Design"); ComponentRadiusHeader = L("settings.components.corner_radius.header", "Corner Design");
GlobalCornerRadiusLabel = L("settings.components.corner_radius.label", "Component Corner Radius"); GlobalCornerRadiusLabel = L("settings.components.corner_radius.label", "Component Corner Radius");
GlobalCornerRadiusDescription = L("settings.components.corner_radius.description", "Adjust the shared corner radius used by component containers, and expand the internal safe area with it."); GlobalCornerRadiusDescription = L(
"settings.components.corner_radius.description",
"Adjust the shared corner radius from a square edge to a capsule-like shape, and expand the internal safe area with it.");
} }
private string L(string key, string fallback) private string L(string key, string fallback)

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Settings.Core;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
@@ -12,10 +13,12 @@ internal static class ComponentChromeCornerRadiusHelper
{ {
if (chromeContext is not null) if (chromeContext is not null)
{ {
return Math.Max(0.1d, chromeContext.GlobalCornerRadiusScale); return Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale);
} }
return Math.Max(0.1d, HostAppearanceThemeProvider.GetOrCreate().GetCurrent().GlobalCornerRadiusScale); return Math.Max(
GlobalAppearanceSettings.MinimumCornerRadiusScale,
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().GlobalCornerRadiusScale);
} }
public static CornerRadius Scale(double baseRadius, double min, double max, ComponentChromeContext? chromeContext = null) public static CornerRadius Scale(double baseRadius, double min, double max, ComponentChromeContext? chromeContext = null)

View File

@@ -9,6 +9,7 @@ using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Settings.Core;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
@@ -36,7 +37,10 @@ public sealed class DesktopComponentRuntimeRegistration
componentId, componentId,
displayNameLocalizationKey, displayNameLocalizationKey,
_ => controlFactory(), _ => controlFactory(),
cornerRadiusResolver is null ? null : chromeContext => cornerRadiusResolver(chromeContext.CellSize)) cornerRadiusResolver is null
? null
: chromeContext => cornerRadiusResolver(chromeContext.CellSize) *
Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale))
{ {
} }
@@ -49,7 +53,10 @@ public sealed class DesktopComponentRuntimeRegistration
componentId, componentId,
displayNameLocalizationKey, displayNameLocalizationKey,
controlFactory, controlFactory,
cornerRadiusResolver is null ? null : chromeContext => cornerRadiusResolver(chromeContext.CellSize)) cornerRadiusResolver is null
? null
: chromeContext => cornerRadiusResolver(chromeContext.CellSize) *
Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale))
{ {
} }
@@ -84,7 +91,7 @@ public sealed class DesktopComponentRuntimeDescriptor
private static readonly Func<ComponentChromeContext, double> DefaultCornerRadiusResolver = private static readonly Func<ComponentChromeContext, double> DefaultCornerRadiusResolver =
chromeContext => chromeContext =>
{ {
var scale = Math.Max(0.1d, chromeContext.GlobalCornerRadiusScale); var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale);
var baseRadius = Math.Clamp(chromeContext.CellSize * 0.22, 8, 18); var baseRadius = Math.Clamp(chromeContext.CellSize * 0.22, 8, 18);
return Math.Clamp(baseRadius * scale, 8 * scale, 18 * scale); return Math.Clamp(baseRadius * scale, 8 * scale, 18 * scale);
}; };
@@ -492,8 +499,9 @@ public sealed class DesktopComponentRuntimeRegistry
new DesktopComponentRuntimeRegistration( new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopOfficeRecentDocuments, BuiltInComponentIds.DesktopOfficeRecentDocuments,
"component.office_recent_documents", "component.office_recent_documents",
() => new OfficeRecentDocumentsWidget(), _ => new OfficeRecentDocumentsWidget(),
cellSize => Math.Clamp(cellSize * 0.50, 10, 24)), chromeContext => Math.Clamp(chromeContext.CellSize * 0.50, 10, 24) *
Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale)),
new DesktopComponentRuntimeRegistration( new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopRemovableStorage, BuiltInComponentIds.DesktopRemovableStorage,
"component.removable_storage", "component.removable_storage",

View File

@@ -63,8 +63,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
_currentCellSize = Math.Max(1, cellSize); _currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale(); var scale = ResolveScale();
var rootRadius = Math.Clamp(30 * scale, 16, 44); var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
var rootCornerRadius = new CornerRadius(rootRadius);
RootBorder.CornerRadius = rootCornerRadius; RootBorder.CornerRadius = rootCornerRadius;
ContentPaddingBorder.Padding = new Thickness( ContentPaddingBorder.Padding = new Thickness(
@@ -85,7 +84,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
CoverBorder.Width = Math.Clamp(56 * scale, 38, 86); CoverBorder.Width = Math.Clamp(56 * scale, 38, 86);
CoverBorder.Height = Math.Clamp(56 * scale, 38, 86); CoverBorder.Height = Math.Clamp(56 * scale, 38, 86);
CoverBorder.CornerRadius = new CornerRadius(Math.Clamp(12 * scale, 8, 16)); CoverBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 8, 16);
TitleTextBlock.FontSize = Math.Clamp(20 * scale, 12, 28); TitleTextBlock.FontSize = Math.Clamp(20 * scale, 12, 28);
ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18); ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);

View File

@@ -10,7 +10,7 @@
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget"> x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
<Border x:Name="RootBorder" <Border x:Name="RootBorder"
CornerRadius="34" CornerRadius="{DynamicResource DesignCornerRadiusIsland}"
Background="#2D5A8E" Background="#2D5A8E"
ClipToBounds="True" ClipToBounds="True"
BorderThickness="0" BorderThickness="0"
@@ -39,7 +39,7 @@
Grid.Column="1" Grid.Column="1"
Width="28" Width="28"
Height="28" Height="28"
CornerRadius="14" CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Background="Transparent" Background="Transparent"
BorderBrush="Transparent" BorderBrush="Transparent"
BorderThickness="0" BorderThickness="0"
@@ -67,7 +67,7 @@
<Border x:Name="DocumentCard" <Border x:Name="DocumentCard"
Width="130" Width="130"
Height="90" Height="90"
CornerRadius="10" CornerRadius="{DynamicResource DesignCornerRadiusXs}"
Background="#3AFFFFFF" Background="#3AFFFFFF"
Padding="10" Padding="10"
Cursor="Hand" Cursor="Hand"

View File

@@ -36,8 +36,7 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen
return; return;
} }
var scale = cellSize / 100.0; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(cellSize * 0.50, 10, 24);
RootBorder.CornerRadius = new Avalonia.CornerRadius(Math.Max(8, 34 * scale));
} }
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)

View File

@@ -17,6 +17,7 @@ using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.Theme; using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
using PathShape = Avalonia.Controls.Shapes.Path; using PathShape = Avalonia.Controls.Shapes.Path;
@@ -1532,7 +1533,7 @@ public partial class MainWindow
appearanceSnapshot.CornerRadiusTokens)); appearanceSnapshot.CornerRadiusTokens));
} }
var scale = Math.Max(0.1d, appearanceSnapshot.GlobalCornerRadiusScale); var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, appearanceSnapshot.GlobalCornerRadiusScale);
return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18) * scale; return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18) * scale;
} }

View File

@@ -516,6 +516,7 @@ public partial class MainWindow
SystemMaterialMode = latestThemeState.SystemMaterialMode, SystemMaterialMode = latestThemeState.SystemMaterialMode,
SelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed, SelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed,
UseSystemChrome = latestThemeState.UseSystemChrome, UseSystemChrome = latestThemeState.UseSystemChrome,
GlobalCornerRadiusScale = latestThemeState.GlobalCornerRadiusScale,
WallpaperPath = latestWallpaperState.WallpaperPath, WallpaperPath = latestWallpaperState.WallpaperPath,
WallpaperType = latestWallpaperState.Type, WallpaperType = latestWallpaperState.Type,
WallpaperColor = string.Equals(latestWallpaperState.Type, "SolidColor", StringComparison.OrdinalIgnoreCase) WallpaperColor = string.Equals(latestWallpaperState.Type, "SolidColor", StringComparison.OrdinalIgnoreCase)

View File

@@ -83,13 +83,13 @@
<TextBlock Text="{Binding GlobalCornerRadiusLabel}" <TextBlock Text="{Binding GlobalCornerRadiusLabel}"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<Slider Grid.Column="1" <Slider Grid.Column="1"
Minimum="0.7" Minimum="{Binding GlobalCornerRadiusMinimum}"
Maximum="1.4" Maximum="{Binding GlobalCornerRadiusMaximum}"
IsSnapToTickEnabled="True" SmallChange="0.01"
TickFrequency="0.05" LargeChange="0.1"
Value="{Binding GlobalCornerRadiusScale}" /> Value="{Binding GlobalCornerRadiusScale}" />
<TextBlock Grid.Column="2" <TextBlock Grid.Column="2"
Width="48" Width="56"
Text="{Binding GlobalCornerRadiusScale, StringFormat={}{0:F2}x}" Text="{Binding GlobalCornerRadiusScale, StringFormat={}{0:F2}x}"
VerticalAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Right" /> HorizontalAlignment="Right" />