diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs index 26010eb..07b85ae 100644 --- a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs +++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs @@ -33,7 +33,7 @@ public sealed class PluginDesktopComponentContext ComponentId = componentId.Trim(); PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim(); CellSize = Math.Max(1, cellSize); - GlobalCornerRadiusScale = Math.Max(0.1d, globalCornerRadiusScale); + GlobalCornerRadiusScale = Math.Max(0d, globalCornerRadiusScale); CornerRadiusTokens = cornerRadiusTokens; PluginSettings = pluginSettings; } diff --git a/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs index 185312d..a83601f 100644 --- a/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs +++ b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs @@ -3,9 +3,8 @@ namespace LanMountainDesktop.Settings.Core; public static class GlobalAppearanceSettings { public const double DefaultCornerRadiusScale = 1.0; - public const double MinimumCornerRadiusScale = 0.70; - public const double MaximumCornerRadiusScale = 1.40; - public const double CornerRadiusScaleStep = 0.05; + public const double MinimumCornerRadiusScale = 0.0; + public const double MaximumCornerRadiusScale = 2.50; public static double NormalizeCornerRadiusScale(double value) { @@ -14,7 +13,6 @@ public static class GlobalAppearanceSettings return DefaultCornerRadiusScale; } - var clamped = Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale); - return Math.Round(clamped / CornerRadiusScaleStep, MidpointRounding.AwayFromZero) * CornerRadiusScaleStep; + return Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale); } } diff --git a/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs b/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs new file mode 100644 index 0000000..4532b89 --- /dev/null +++ b/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs @@ -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(), + "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; + } +} diff --git a/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs b/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs new file mode 100644 index 0000000..83ed058 --- /dev/null +++ b/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs @@ -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>(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>(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))); + } +} diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index 556be17..0d4ae9f 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -313,7 +313,7 @@ "settings.components.spacing_relaxed": "Relaxed", "settings.components.corner_radius.header": "Corner Design", "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.current_version_label": "Current Version", "settings.update.latest_version_label": "Latest Release", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index bf5fdf0..5037b02 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -312,7 +312,7 @@ "settings.components.spacing_relaxed": "宽松", "settings.components.corner_radius.header": "圆角设计", "settings.components.corner_radius.label": "组件圆角", - "settings.components.corner_radius.description": "统一调整组件容器圆角,并随圆角增大同步扩展内部安全区。", + "settings.components.corner_radius.description": "将组件容器圆角从直角连续调到接近胶囊的形态,并随圆角增大同步扩展内部安全区。", "settings.update.title": "更新", "settings.update.current_version_label": "当前版本", "settings.update.latest_version_label": "最新发布", diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs index 3b3f397..eedae2f 100644 --- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs +++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs @@ -1010,6 +1010,10 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase [ObservableProperty] private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale; + public double GlobalCornerRadiusMinimum => GlobalAppearanceSettings.MinimumCornerRadiusScale; + + public double GlobalCornerRadiusMaximum => GlobalAppearanceSettings.MaximumCornerRadiusScale; + [ObservableProperty] private string _componentRadiusHeader = string.Empty; @@ -1129,7 +1133,9 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase SpacingPresetLabel = L("settings.components.spacing_label", "Component Spacing"); ComponentRadiusHeader = L("settings.components.corner_radius.header", "Corner Design"); 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) diff --git a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs index 355ab95..258a108 100644 --- a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs +++ b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Media; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Services; +using LanMountainDesktop.Settings.Core; namespace LanMountainDesktop.Views.Components; @@ -12,10 +13,12 @@ internal static class ComponentChromeCornerRadiusHelper { 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) diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs index f86aab0..b5ef729 100644 --- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs +++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs @@ -9,6 +9,7 @@ using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.Settings.Core; namespace LanMountainDesktop.Views.Components; @@ -36,7 +37,10 @@ public sealed class DesktopComponentRuntimeRegistration componentId, displayNameLocalizationKey, _ => 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, displayNameLocalizationKey, 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 DefaultCornerRadiusResolver = 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); return Math.Clamp(baseRadius * scale, 8 * scale, 18 * scale); }; @@ -492,8 +499,9 @@ public sealed class DesktopComponentRuntimeRegistry new DesktopComponentRuntimeRegistration( BuiltInComponentIds.DesktopOfficeRecentDocuments, "component.office_recent_documents", - () => new OfficeRecentDocumentsWidget(), - cellSize => Math.Clamp(cellSize * 0.50, 10, 24)), + _ => new OfficeRecentDocumentsWidget(), + chromeContext => Math.Clamp(chromeContext.CellSize * 0.50, 10, 24) * + Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale)), new DesktopComponentRuntimeRegistration( BuiltInComponentIds.DesktopRemovableStorage, "component.removable_storage", diff --git a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs index 0af0980..716acad 100644 --- a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs @@ -63,8 +63,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, _currentCellSize = Math.Max(1, cellSize); var scale = ResolveScale(); - var rootRadius = Math.Clamp(30 * scale, 16, 44); - var rootCornerRadius = new CornerRadius(rootRadius); + var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44); RootBorder.CornerRadius = rootCornerRadius; ContentPaddingBorder.Padding = new Thickness( @@ -85,7 +84,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, CoverBorder.Width = 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); ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18); diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml index 59d50c7..43d4efc 100644 --- a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml @@ -10,7 +10,7 @@ x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">