diff --git a/AGENTS.md b/AGENTS.md index c08c9d1..211433c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,10 @@ dotnet test LanMountainDesktop.slnx -c Debug ### UI - 主题、资源和视觉语义优先遵守 `docs/VISUAL_SPEC.md` 与 `docs/CORNER_RADIUS_SPEC.md` -- **组件圆角**:所有内置与插件组件的根边框必须使用 `{DynamicResource DesignCornerRadiusComponent}` 资源。 +- **圆角规范 (AI 强制建议)**: + - **桌面组件根容器**:必须且仅能使用 `{DynamicResource DesignCornerRadiusComponent}`。 + - **内部元素**:必须根据嵌套层级使用 `DesignCornerRadiusSm/Md/Lg` 等 Token,严禁硬编码像素值。 + - **禁止修改系数**:严禁在圆角资源上乘以任何 `scale` 变量,圆角现在由全局样式固定控制。 - 设置页相关改动通常同时落在 `Views/`、`ViewModels/`、`Services/` 和 `.trae/specs/` - UI 启动与窗口生命周期主线在 `Program.cs` 和 `App.axaml.cs` diff --git a/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs b/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs index da6bc51..0203e17 100644 --- a/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs +++ b/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs @@ -6,23 +6,48 @@ namespace LanMountainDesktop.Appearance; public static class AppearanceCornerRadiusTokenFactory { - public static AppearanceCornerRadiusTokens Create(double scale) + public static AppearanceCornerRadiusTokens Create(string style) { - var normalizedScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(scale); - return new AppearanceCornerRadiusTokens( - Radius(6, normalizedScale), - Radius(12, normalizedScale), - Radius(14, normalizedScale), - Radius(20, normalizedScale), - Radius(28, normalizedScale), - Radius(32, normalizedScale), - Radius(36, normalizedScale), - Radius(18, normalizedScale)); - } - - private static CornerRadius Radius(double value, double scale) - { - var scaled = Math.Round(value * scale * 2, MidpointRounding.AwayFromZero) / 2d; - return new CornerRadius(scaled); + var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(style); + return normalized switch + { + GlobalAppearanceSettings.CornerRadiusStyleSharp => new AppearanceCornerRadiusTokens( + Micro: new CornerRadius(4), + Xs: new CornerRadius(8), + Sm: new CornerRadius(10), + Md: new CornerRadius(14), + Lg: new CornerRadius(20), + Xl: new CornerRadius(24), + Island: new CornerRadius(28), + Component: new CornerRadius(20)), + GlobalAppearanceSettings.CornerRadiusStyleRounded => new AppearanceCornerRadiusTokens( + Micro: new CornerRadius(8), + Xs: new CornerRadius(14), + Sm: new CornerRadius(16), + Md: new CornerRadius(24), + Lg: new CornerRadius(32), + Xl: new CornerRadius(36), + Island: new CornerRadius(40), + Component: new CornerRadius(28)), + GlobalAppearanceSettings.CornerRadiusStyleOpen => new AppearanceCornerRadiusTokens( + Micro: new CornerRadius(10), + Xs: new CornerRadius(16), + Sm: new CornerRadius(20), + Md: new CornerRadius(28), + Lg: new CornerRadius(36), + Xl: new CornerRadius(40), + Island: new CornerRadius(44), + Component: new CornerRadius(32)), + // Balanced (default) + _ => new AppearanceCornerRadiusTokens( + Micro: new CornerRadius(6), + Xs: new CornerRadius(12), + Sm: new CornerRadius(14), + Md: new CornerRadius(20), + Lg: new CornerRadius(28), + Xl: new CornerRadius(32), + Island: new CornerRadius(36), + Component: new CornerRadius(24)) + }; } } diff --git a/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs b/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs index 110a0bd..7f38f1b 100644 --- a/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs +++ b/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs @@ -7,6 +7,5 @@ public sealed record ComponentChromeContext( string ComponentId, string? PlacementId, double CellSize, - double GlobalCornerRadiusScale, AppearanceCornerRadiusTokens CornerRadiusTokens, SettingsScope Scope = SettingsScope.App); diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs index d7fdfad..18532d9 100644 --- a/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs +++ b/LanMountainDesktop.PluginSdk/PluginAppearanceContext.cs @@ -9,7 +9,6 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext Snapshot = snapshot with { - GlobalCornerRadiusScale = Math.Max(0d, snapshot.GlobalCornerRadiusScale), ThemeVariant = string.IsNullOrWhiteSpace(snapshot.ThemeVariant) ? "Unknown" : snapshot.ThemeVariant.Trim() @@ -20,13 +19,15 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null) { - var scale = Snapshot.GlobalCornerRadiusScale; - var scaled = Math.Max(0d, baseRadius) * scale; - var scaledMin = minimum.HasValue ? minimum.Value * scale : scaled; - var scaledMax = maximum.HasValue ? maximum.Value * scale : scaled; - return minimum.HasValue || maximum.HasValue - ? Math.Clamp(scaled, scaledMin, scaledMax) - : scaled; + var value = Math.Max(0d, baseRadius); + if (!minimum.HasValue && !maximum.HasValue) + { + return value; + } + + var clampedMin = minimum ?? value; + var clampedMax = maximum ?? value; + return Math.Clamp(value, clampedMin, clampedMax); } public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null) diff --git a/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs index fba0f99..e5f781b 100644 --- a/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs +++ b/LanMountainDesktop.PluginSdk/PluginAppearanceSnapshot.cs @@ -1,6 +1,5 @@ namespace LanMountainDesktop.PluginSdk; public sealed record PluginAppearanceSnapshot( - double GlobalCornerRadiusScale, PluginCornerRadiusTokens CornerRadiusTokens, string ThemeVariant); diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs index 6f592dc..ee6bbd4 100644 --- a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs +++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs @@ -52,8 +52,6 @@ public sealed class PluginDesktopComponentContext public IPluginAppearanceContext Appearance { get; } - public double GlobalCornerRadiusScale => Appearance.Snapshot.GlobalCornerRadiusScale; - public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens; public IPluginSettingsService? PluginSettings { get; } diff --git a/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs index a83601f..65a089b 100644 --- a/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs +++ b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs @@ -2,17 +2,69 @@ namespace LanMountainDesktop.Settings.Core; public static class GlobalAppearanceSettings { + public const string CornerRadiusStyleSharp = "Sharp"; + public const string CornerRadiusStyleBalanced = "Balanced"; + public const string CornerRadiusStyleRounded = "Rounded"; + public const string CornerRadiusStyleOpen = "Open"; + public const string DefaultCornerRadiusStyle = CornerRadiusStyleBalanced; + + /// + /// Kept for backward compatibility during settings migration. + /// New code should not reference this constant. + /// public const double DefaultCornerRadiusScale = 1.0; public const double MinimumCornerRadiusScale = 0.0; - public const double MaximumCornerRadiusScale = 2.50; - public static double NormalizeCornerRadiusScale(double value) + public static string NormalizeCornerRadiusStyle(string? value) { - if (double.IsNaN(value) || double.IsInfinity(value)) + if (string.IsNullOrWhiteSpace(value)) { - return DefaultCornerRadiusScale; + return DefaultCornerRadiusStyle; } - return Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale); + var trimmed = value.Trim(); + if (string.Equals(trimmed, CornerRadiusStyleSharp, StringComparison.OrdinalIgnoreCase)) + { + return CornerRadiusStyleSharp; + } + + if (string.Equals(trimmed, CornerRadiusStyleBalanced, StringComparison.OrdinalIgnoreCase)) + { + return CornerRadiusStyleBalanced; + } + + if (string.Equals(trimmed, CornerRadiusStyleRounded, StringComparison.OrdinalIgnoreCase)) + { + return CornerRadiusStyleRounded; + } + + if (string.Equals(trimmed, CornerRadiusStyleOpen, StringComparison.OrdinalIgnoreCase)) + { + return CornerRadiusStyleOpen; + } + + return DefaultCornerRadiusStyle; + } + + public static readonly IReadOnlyList AllCornerRadiusStyles = + [ + CornerRadiusStyleSharp, + CornerRadiusStyleBalanced, + CornerRadiusStyleRounded, + CornerRadiusStyleOpen + ]; + + /// + /// Backward compatibility: map previous scale values to the closest style. + /// + public static string MigrateScaleToStyle(double scale) + { + return scale switch + { + <= 0.60 => CornerRadiusStyleSharp, + <= 1.20 => CornerRadiusStyleBalanced, + <= 1.70 => CornerRadiusStyleRounded, + _ => CornerRadiusStyleOpen + }; } } diff --git a/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs b/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs index 6116d90..a273999 100644 --- a/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs +++ b/LanMountainDesktop.Tests/BuiltInDesktopHostCornerRadiusBaselineTests.cs @@ -11,19 +11,19 @@ namespace LanMountainDesktop.Tests; public sealed class BuiltInDesktopHostCornerRadiusBaselineTests { [Theory] - [InlineData(80d, 0d)] - [InlineData(120d, 1d)] - [InlineData(160d, 2.5d)] - public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, double globalScale) + [InlineData(80d, "Sharp")] + [InlineData(120d, "Balanced")] + [InlineData(160d, "Rounded")] + public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, string style) { var registry = new DesktopComponentRuntimeRegistry( ComponentRegistry.CreateDefault(), DesktopComponentRuntimeRegistry.GetDefaultRegistrations()); - var expected = AppearanceCornerRadiusTokenFactory.Create(globalScale).Component.TopLeft; + var expected = AppearanceCornerRadiusTokenFactory.Create(style).Component.TopLeft; foreach (var descriptor in registry.GetDesktopComponents()) { - var resolved = descriptor.ResolveCornerRadius(CreateChromeContext(descriptor.Definition.Id, cellSize, globalScale)); + var resolved = descriptor.ResolveCornerRadius(CreateChromeContext(descriptor.Definition.Id, cellSize, style)); Assert.Equal(expected, resolved, 3); } } @@ -31,13 +31,12 @@ public sealed class BuiltInDesktopHostCornerRadiusBaselineTests private static ComponentChromeContext CreateChromeContext( string componentId, double cellSize, - double globalScale) + string style) { return new ComponentChromeContext( componentId, null, cellSize, - globalScale, - AppearanceCornerRadiusTokenFactory.Create(globalScale)); + AppearanceCornerRadiusTokenFactory.Create(style)); } } diff --git a/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs b/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs deleted file mode 100644 index 9bca529..0000000 --- a/LanMountainDesktop.Tests/CornerRadiusScaleTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -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 appearanceContext = new PluginAppearanceContext(new PluginAppearanceSnapshot( - GlobalCornerRadiusScale: 0d, - CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(new AppearanceCornerRadiusTokens( - new CornerRadius(6), - new CornerRadius(12), - new CornerRadius(14), - new CornerRadius(20), - new CornerRadius(28), - new CornerRadius(32), - new CornerRadius(36), - new CornerRadius(8))), - ThemeVariant: "Unknown")); - - 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, - appearanceContext); - - Assert.Equal(0d, context.GlobalCornerRadiusScale, 3); - Assert.Equal(0d, context.ResolveScaledCornerRadius(12d), 3); - Assert.Equal(0d, context.ResolveScaledCornerRadius(12d, 8d, 18d), 3); - } - - [Fact] - public void PluginAppearanceContext_ResolveCornerRadius_DoesNotDoubleScalePresetTokens() - { - var context = new PluginAppearanceContext(new PluginAppearanceSnapshot( - GlobalCornerRadiusScale: 2d, - CornerRadiusTokens: new PluginCornerRadiusTokens( - Micro: 12d, - Xs: 20d, - Sm: 28d, - Md: 36d, - Lg: 48d, - Xl: 60d, - Island: 72d, - Component: 16d), - ThemeVariant: "Light")); - - Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md), 3); - Assert.Equal(36d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md, maximum: 40d), 3); - Assert.Equal(36d, context.ResolveScaledCornerRadius(18d), 3); - } - - private sealed class NullServiceProvider : IServiceProvider - { - public object? GetService(Type serviceType) => null; - } -} diff --git a/LanMountainDesktop.Tests/CornerRadiusStyleTests.cs b/LanMountainDesktop.Tests/CornerRadiusStyleTests.cs new file mode 100644 index 0000000..a8b4978 --- /dev/null +++ b/LanMountainDesktop.Tests/CornerRadiusStyleTests.cs @@ -0,0 +1,71 @@ +using Avalonia; +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Settings.Core; +using LanMountainDesktop.Shared.Contracts; +using Xunit; + +namespace LanMountainDesktop.Tests; + +public sealed class CornerRadiusStyleTests +{ + [Theory] + [InlineData("Sharp", "Sharp")] + [InlineData("Balanced", "Balanced")] + [InlineData("Rounded", "Rounded")] + [InlineData("Open", "Open")] + [InlineData("Unknown", "Balanced")] + [InlineData(null, "Balanced")] + public void NormalizeCornerRadiusStyle_ReturnsValidStyleOrDefault(string? input, string expected) + { + Assert.Equal(expected, GlobalAppearanceSettings.NormalizeCornerRadiusStyle(input)); + } + + [Fact] + public void PluginAppearanceContext_ResolveCornerRadius_ReturnsFixedTokenValues() + { + var context = new PluginAppearanceContext(new PluginAppearanceSnapshot( + CornerRadiusTokens: new PluginCornerRadiusTokens( + Micro: 6d, + Xs: 12d, + Sm: 14d, + Md: 20d, + Lg: 28d, + Xl: 32d, + Island: 36d, + Component: 24d), + ThemeVariant: "Light")); + + // Preset resolution should return fixed values from tokens regardless of any legacy scale + Assert.Equal(20d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md), 3); + Assert.Equal(20d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Md, maximum: 15d), 3); + Assert.Equal(20d, context.ResolveScaledCornerRadius(18d), 3); + Assert.Equal(24d, context.ResolveCornerRadius(PluginCornerRadiusPreset.Component), 3); + } + + [Fact] + public void PluginDesktopComponentContext_ProvidesDirectTokenAccess() + { + var appearanceContext = new PluginAppearanceContext(new PluginAppearanceSnapshot( + CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 24), + ThemeVariant: "Dark")); + + 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, + appearanceContext); + + Assert.Equal(24d, context.ResolveScaledCornerRadius(12d), 3); + Assert.Equal(24d, 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 index c83e636..b4b3461 100644 --- a/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs +++ b/LanMountainDesktop.Tests/DesktopComponentRuntimeRegistrationCornerRadiusTests.cs @@ -10,7 +10,7 @@ namespace LanMountainDesktop.Tests; public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests { [Fact] - public void LegacyCellSizeResolver_AppliesGlobalCornerRadiusScale() + public void LegacyCellSizeResolver_ReturnsUnscaledFixedValue() { var registration = new DesktopComponentRuntimeRegistration( componentId: "test.component", @@ -19,41 +19,42 @@ public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.30, 10, 40)); var resolver = Assert.IsType>(registration.CornerRadiusResolver); - var resolved = resolver(CreateChromeContext(cellSize: 120, globalScale: 2.0)); + // Previously: (120 * 0.30) * 2.0 = 72.0 + // Now: (120 * 0.30) = 36.0 (No scale applied automatically by the wrapper) + var resolved = resolver(CreateChromeContext(cellSize: 120)); - Assert.Equal(72.0, resolved, 3); + Assert.Equal(36.0, resolved, 3); } [Fact] - public void ChromeContextResolver_IsNotDoubleScaledByRegistrationWrapper() + public void ChromeContextResolver_UsesTokenValue() { var registration = new DesktopComponentRuntimeRegistration( componentId: "test.component", displayNameLocalizationKey: null, controlFactory: _ => new Border(), - cornerRadiusResolver: chromeContext => chromeContext.CellSize + chromeContext.GlobalCornerRadiusScale); + cornerRadiusResolver: chromeContext => chromeContext.CornerRadiusTokens.Component.TopLeft); var resolver = Assert.IsType>(registration.CornerRadiusResolver); - var resolved = resolver(CreateChromeContext(cellSize: 50, globalScale: 2.5)); + var resolved = resolver(CreateChromeContext(cellSize: 50)); - Assert.Equal(52.5, resolved, 3); + Assert.Equal(24.0, resolved, 3); } - private static ComponentChromeContext CreateChromeContext(double cellSize, double globalScale) + private static ComponentChromeContext CreateChromeContext(double cellSize) { return new ComponentChromeContext( ComponentId: "test.component", PlacementId: null, CellSize: cellSize, - GlobalCornerRadiusScale: globalScale, CornerRadiusTokens: new AppearanceCornerRadiusTokens( - new CornerRadius(6), - new CornerRadius(12), - new CornerRadius(14), - new CornerRadius(20), - new CornerRadius(28), - new CornerRadius(32), - new CornerRadius(36), - new CornerRadius(8))); + Micro: new CornerRadius(6), + Xs: new CornerRadius(12), + Sm: new CornerRadius(14), + Md: new CornerRadius(20), + Lg: new CornerRadius(28), + Xl: new CornerRadius(32), + Island: new CornerRadius(36), + Component: new CornerRadius(24))); } } diff --git a/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs b/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs index 9468df1..7680f2e 100644 --- a/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs +++ b/LanMountainDesktop.Tests/InfoRecommendationHostCornerRadiusTests.cs @@ -48,26 +48,27 @@ public sealed class InfoRecommendationHostCornerRadiusTests registry.TryGetDescriptor(componentId, out var descriptor), $"Missing runtime registration for '{componentId}'."); - var zero = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 0d)); - var unit = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 1d)); - var max = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 2.5d)); + var sharp = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Sharp")); + var balanced = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Balanced")); + var rounded = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Rounded")); + var open = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Open")); - Assert.Equal(0d, zero, 3); - Assert.Equal(18d, unit, 3); - Assert.Equal(45d, max, 3); - Assert.True(zero <= unit && unit <= max); + // All info widgets should resolve to the Component token in the new system + Assert.Equal(20d, sharp, 3); + Assert.Equal(24d, balanced, 3); + Assert.Equal(28d, rounded, 3); + Assert.Equal(32d, open, 3); } private static ComponentChromeContext CreateChromeContext( string componentId, double cellSize, - double globalScale) + string style) { return new ComponentChromeContext( componentId, null, cellSize, - globalScale, - AppearanceCornerRadiusTokenFactory.Create(globalScale)); + AppearanceCornerRadiusTokenFactory.Create(style)); } } diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 8d41fb7..c09f0b8 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -664,7 +664,7 @@ public partial class App : Application refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) || - changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) || + changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) || (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) && changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) || (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) && diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index e3e0490..120a418 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -19,6 +19,8 @@ public sealed class AppSettingsSnapshot public double GlobalCornerRadiusScale { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusScale; + public string CornerRadiusStyle { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusStyle; + public string ThemeColorMode { get; set; } = "default_neutral"; public string SystemMaterialMode { get; set; } = "none"; diff --git a/LanMountainDesktop/Services/AppearanceThemeService.cs b/LanMountainDesktop/Services/AppearanceThemeService.cs index b3635bf..805c365 100644 --- a/LanMountainDesktop/Services/AppearanceThemeService.cs +++ b/LanMountainDesktop/Services/AppearanceThemeService.cs @@ -44,7 +44,7 @@ public sealed record AppearanceThemeSnapshot( string ThemeColorMode, string? UserThemeColor, string? SelectedWallpaperSeed, - double GlobalCornerRadiusScale, + string CornerRadiusStyle, AppearanceCornerRadiusTokens CornerRadiusTokens, string ResolvedSeedSource, MonetPalette MonetPalette, @@ -551,7 +551,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa if (!refreshAll && !changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) && !changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) && - !changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) && + !changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) && !(respondsToThemeColor && changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) && !(respondsToWallpaper && @@ -573,8 +573,8 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa bool queueWallpaperPaletteBuild) { var availableModes = _windowMaterialService.GetAvailableModes(); - var globalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(themeState.GlobalCornerRadiusScale); - var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(globalCornerRadiusScale); + var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(themeState.CornerRadiusStyle); + var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(cornerRadiusStyle); MonetPalette palette; IReadOnlyList wallpaperSeedCandidates; Color effectiveSeedColor; @@ -614,7 +614,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa themeColorMode, themeState.ThemeColor, selectedWallpaperSeed, - globalCornerRadiusScale, + cornerRadiusStyle, cornerRadiusTokens, resolvedSeedSource, palette, diff --git a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs index a883628..7c76f7a 100644 --- a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs +++ b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs @@ -129,7 +129,6 @@ public static class DesktopComponentRegistryFactory settingsService); var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); var pluginAppearance = new PluginAppearanceContext(new PluginAppearanceSnapshot( - GlobalCornerRadiusScale: appearanceSnapshot.GlobalCornerRadiusScale, CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(appearanceSnapshot.CornerRadiusTokens), ThemeVariant: appearanceSnapshot.IsNightMode ? "Dark" : "Light")); var pluginContext = new PluginDesktopComponentContext( @@ -157,7 +156,6 @@ public static class DesktopComponentRegistryFactory private static IPluginAppearanceContext CreatePluginAppearanceContext(ComponentChromeContext chromeContext) { return new PluginAppearanceContext(new PluginAppearanceSnapshot( - GlobalCornerRadiusScale: chromeContext.GlobalCornerRadiusScale, CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(chromeContext.CornerRadiusTokens), ThemeVariant: "Unknown")); } diff --git a/LanMountainDesktop/Services/IComponentLibraryService.cs b/LanMountainDesktop/Services/IComponentLibraryService.cs index c2fa8c7..fc08ed4 100644 --- a/LanMountainDesktop/Services/IComponentLibraryService.cs +++ b/LanMountainDesktop/Services/IComponentLibraryService.cs @@ -20,7 +20,6 @@ public sealed record ComponentLibraryCategoryEntry( public sealed record ComponentLibraryCreateContext( double CellSize, - double GlobalCornerRadiusScale, TimeZoneService TimeZoneService, IWeatherInfoService WeatherInfoService, IRecommendationInfoService RecommendationInfoService, diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index 57090ca..62420dc 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -30,7 +30,7 @@ public sealed record ThemeAppearanceSettingsState( bool IsNightMode, string? ThemeColor, bool UseSystemChrome, - double GlobalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale, + string CornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle, string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral, string SystemMaterialMode = ThemeAppearanceValues.MaterialNone, string? SelectedWallpaperSeed = null); diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index 06fd6d5..a9d0d12 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -254,11 +254,19 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService public ThemeAppearanceSettingsState Get() { var snapshot = _settingsService.Load(); + var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(snapshot.CornerRadiusStyle); + if (string.Equals(cornerRadiusStyle, GlobalAppearanceSettings.DefaultCornerRadiusStyle, StringComparison.OrdinalIgnoreCase) && + string.IsNullOrWhiteSpace(snapshot.CornerRadiusStyle) && + Math.Abs(snapshot.GlobalCornerRadiusScale - GlobalAppearanceSettings.DefaultCornerRadiusScale) > 0.01) + { + cornerRadiusStyle = GlobalAppearanceSettings.MigrateScaleToStyle(snapshot.GlobalCornerRadiusScale); + } + return new ThemeAppearanceSettingsState( snapshot.IsNightMode ?? false, snapshot.ThemeColor, snapshot.UseSystemChrome, - GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale), + cornerRadiusStyle, ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor), ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode), snapshot.SelectedWallpaperSeed); @@ -269,7 +277,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService var snapshot = _settingsService.Load(); var changedKeys = new List(); var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor; - var normalizedCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(state.GlobalCornerRadiusScale); + var normalizedCornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(state.CornerRadiusStyle); var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor); var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode); var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed) @@ -294,10 +302,10 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome)); } - if (Math.Abs(GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale) - normalizedCornerRadiusScale) > 0.0001d) + if (!string.Equals(GlobalAppearanceSettings.NormalizeCornerRadiusStyle(snapshot.CornerRadiusStyle), normalizedCornerRadiusStyle, StringComparison.OrdinalIgnoreCase)) { - snapshot.GlobalCornerRadiusScale = normalizedCornerRadiusScale; - changedKeys.Add(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale)); + snapshot.CornerRadiusStyle = normalizedCornerRadiusStyle; + changedKeys.Add(nameof(AppSettingsSnapshot.CornerRadiusStyle)); } if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase)) diff --git a/LanMountainDesktop/ViewModels/MainWindowViewModel.cs b/LanMountainDesktop/ViewModels/MainWindowViewModel.cs index cdfaae1..7ba9f93 100644 --- a/LanMountainDesktop/ViewModels/MainWindowViewModel.cs +++ b/LanMountainDesktop/ViewModels/MainWindowViewModel.cs @@ -1,6 +1,32 @@ -namespace LanMountainDesktop.ViewModels; +using CommunityToolkit.Mvvm.Input; +using System.Diagnostics; +using System.IO; + +namespace LanMountainDesktop.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public string Greeting { get; } = "A modern desktop shell powered by FluentAvalonia."; + + [RelayCommand] + private void OpenDesignSpec(string? fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) return; + + var fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "docs", fileName); + if (!File.Exists(fullPath)) + { + // Try relative to project root in dev + fullPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", "docs", fileName)); + } + + if (File.Exists(fullPath)) + { + Process.Start(new ProcessStartInfo + { + FileName = fullPath, + UseShellExecute = true + }); + } + } } diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs index 6af1761..941e257 100644 --- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs +++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs @@ -614,10 +614,10 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase private string _systemMaterialLabel = string.Empty; [ObservableProperty] - private string _globalCornerRadiusLabel = string.Empty; + private string _cornerRadiusStyleLabel = string.Empty; [ObservableProperty] - private string _globalCornerRadiusDescription = string.Empty; + private string _cornerRadiusStyleDescription = string.Empty; [ObservableProperty] private string _themeHeader = string.Empty; @@ -701,6 +701,15 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase public IBrush NeutralDarkPreviewBrush => NeutralDarkBrushValue; + [ObservableProperty] + private string _cornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle; + + [ObservableProperty] + private IReadOnlyList _cornerRadiusStyleOptions = []; + + [ObservableProperty] + private SelectionOption? _selectedCornerRadiusStyle; + public void Load() { var theme = _settingsFacade.Theme.Get(); @@ -740,29 +749,14 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase PersistCurrentState(restartRequired: false); } - partial void OnGlobalCornerRadiusScaleChanged(double value) + partial void OnSelectedCornerRadiusStyleChanged(SelectionOption? value) { - if (_isInitializing) + if (_isInitializing || value is null) { return; } - var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value); - if (Math.Abs(normalized - value) > 0.0001d) - { - _isInitializing = true; - try - { - GlobalCornerRadiusScale = normalized; - } - finally - { - _isInitializing = false; - } - - return; - } - + CornerRadiusStyle = value.Value; PersistCurrentState(restartRequired: false); } @@ -830,8 +824,12 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color"); ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source"); SystemMaterialLabel = L("settings.appearance.system_material_label", "System material"); - GlobalCornerRadiusLabel = L("settings.appearance.corner_radius.label", "Global corner radius"); - GlobalCornerRadiusDescription = L("settings.appearance.corner_radius.description", "Adjust the shared radius scale used by cards, panels, and component containers."); + CornerRadiusStyleLabel = L("settings.appearance.corner_radius.label", "Global corner radius style"); + CornerRadiusStyleDescription = L("settings.appearance.corner_radius.description", "Select a fixed corner radius style inspired by Xiaomi HyperOS."); + + CornerRadiusStyleOptions = GlobalAppearanceSettings.AllCornerRadiusStyles + .Select(style => new SelectionOption(style, L($"settings.appearance.corner_radius.style_{style.ToLower()}", style))) + .ToList(); ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral"); ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet"); ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet"); @@ -876,7 +874,10 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase IsNightMode = theme.IsNightMode; ThemeColor = theme.ThemeColor ?? string.Empty; UseSystemChrome = theme.UseSystemChrome; - GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale); + CornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(theme.CornerRadiusStyle); + SelectedCornerRadiusStyle = CornerRadiusStyleOptions.FirstOrDefault(option => + string.Equals(option.Value, CornerRadiusStyle, StringComparison.OrdinalIgnoreCase)) + ?? CornerRadiusStyleOptions.FirstOrDefault(o => o.Value == GlobalAppearanceSettings.DefaultCornerRadiusStyle); _selectedWallpaperSeed = theme.SelectedWallpaperSeed; SyncCustomSeedPickerWithSavedThemeColor(); @@ -926,7 +927,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase IsNightMode, themeColor, UseSystemChrome, - GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale), + GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle), themeColorMode, ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value), _selectedWallpaperSeed); @@ -1070,20 +1071,22 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase private string _spacingPresetLabel = string.Empty; [ObservableProperty] - private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale; + private string _cornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle; - public double GlobalCornerRadiusMinimum => GlobalAppearanceSettings.MinimumCornerRadiusScale; + [ObservableProperty] + private IReadOnlyList _cornerRadiusStyleOptions = []; - public double GlobalCornerRadiusMaximum => GlobalAppearanceSettings.MaximumCornerRadiusScale; + [ObservableProperty] + private SelectionOption? _selectedCornerRadiusStyle; [ObservableProperty] private string _componentRadiusHeader = string.Empty; [ObservableProperty] - private string _globalCornerRadiusLabel = string.Empty; + private string _cornerRadiusStyleLabel = string.Empty; [ObservableProperty] - private string _globalCornerRadiusDescription = string.Empty; + private string _cornerRadiusStyleDescription = string.Empty; public void Load() { @@ -1096,7 +1099,10 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase ?? SpacingPresets[1]; var theme = _settingsFacade.Theme.Get(); - GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale); + CornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(theme.CornerRadiusStyle); + SelectedCornerRadiusStyle = CornerRadiusStyleOptions.FirstOrDefault(option => + string.Equals(option.Value, CornerRadiusStyle, StringComparison.OrdinalIgnoreCase)) + ?? CornerRadiusStyleOptions.FirstOrDefault(o => o.Value == GlobalAppearanceSettings.DefaultCornerRadiusStyle); } partial void OnShortSideCellsChanged(int value) @@ -1129,29 +1135,14 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase SaveGrid(); } - partial void OnGlobalCornerRadiusScaleChanged(double value) + partial void OnSelectedCornerRadiusStyleChanged(SelectionOption? value) { - if (_isInitializing) + if (_isInitializing || value is null) { return; } - var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value); - if (Math.Abs(normalized - value) > 0.0001d) - { - _isInitializing = true; - try - { - GlobalCornerRadiusScale = normalized; - } - finally - { - _isInitializing = false; - } - - return; - } - + CornerRadiusStyle = value.Value; SaveComponentCornerRadius(); } @@ -1170,7 +1161,7 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase theme.IsNightMode, theme.ThemeColor, theme.UseSystemChrome, - GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale), + GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle), theme.ThemeColorMode, theme.SystemMaterialMode, theme.SelectedWallpaperSeed)); @@ -1194,10 +1185,14 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase EdgeInsetPercentLabel = L("settings.components.edge_inset_label", "Screen Inset"); 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( + CornerRadiusStyleLabel = L("settings.components.corner_radius.label", "Component Corner Radius Style"); + CornerRadiusStyleDescription = 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."); + "Select a fixed corner radius style (inspired by Xiaomi HyperOS) to ensure consistency across all components."); + + CornerRadiusStyleOptions = GlobalAppearanceSettings.AllCornerRadiusStyles + .Select(style => new SelectionOption(style, L($"settings.appearance.corner_radius.style_{style.ToLower()}", style))) + .ToList(); } private string L(string key, string fallback) diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs index 51c221d..6a4a5da 100644 --- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -552,7 +552,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, { Width = 160, Height = 90, - CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16, 8, 22), + CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16, 8, 22), ClipToBounds = true, Background = new SolidColorBrush(Color.Parse("#E6E6E6")), IsHitTestVisible = false @@ -647,8 +647,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, News1ImageHost.Height = imageHeight; News2ImageHost.Width = imageWidth; News2ImageHost.Height = imageHeight; - News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); - News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); + News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22); + News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22); News1ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6")); News2ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6")); @@ -691,7 +691,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, row.ImageHost.Width = imageWidth; row.ImageHost.Height = imageHeight; - row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); + row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22); row.ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6")); row.TitleTextBlock.MaxWidth = availableTextWidth; diff --git a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs index 2b7bb9c..d3a096d 100644 --- a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs +++ b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs @@ -3,13 +3,14 @@ using Avalonia.Controls; using Avalonia.Media; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Settings.Core; namespace LanMountainDesktop.Views.Components; internal static class ComponentChromeCornerRadiusHelper { - public static double ResolveMainRectangleRadiusValue(ComponentChromeContext? chromeContext = null, double fallback = 18d) + public static double ResolveMainRectangleRadiusValue(ComponentChromeContext? chromeContext = null, double fallback = 24d) { if (chromeContext is not null) { @@ -20,7 +21,7 @@ internal static class ComponentChromeCornerRadiusHelper var resolved = snapshot.CornerRadiusTokens.Component.TopLeft; return double.IsFinite(resolved) ? Math.Max(0d, resolved) - : Math.Max(0d, fallback * ResolveScale(chromeContext)); + : Math.Max(0d, fallback); } public static CornerRadius ResolveMainRectangleRadius(ComponentChromeContext? chromeContext = null, double fallback = 24d) @@ -28,24 +29,6 @@ internal static class ComponentChromeCornerRadiusHelper return new CornerRadius(ResolveMainRectangleRadiusValue(chromeContext, fallback)); } - public static double ResolveScale(ComponentChromeContext? chromeContext = null) - { - if (chromeContext is not null) - { - return Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale); - } - - return Math.Max( - GlobalAppearanceSettings.MinimumCornerRadiusScale, - HostAppearanceThemeProvider.GetOrCreate().GetCurrent().GlobalCornerRadiusScale); - } - - public static CornerRadius Scale(double baseRadius, double min, double max, ComponentChromeContext? chromeContext = null) - { - var scale = ResolveScale(chromeContext); - return new CornerRadius(Math.Clamp(baseRadius * scale, min * scale, max * scale)); - } - public static void Apply(CornerRadius radius, params Border?[] chromeLayers) { foreach (var chromeLayer in chromeLayers) @@ -67,28 +50,57 @@ internal static class ComponentChromeCornerRadiusHelper : new CornerRadius(fallback); } - public static double ScaleValue(double value, ComponentChromeContext? chromeContext = null) + public static double SafeValue(double value, double min, double max, ComponentChromeContext? context = null) { - return value * ResolveScale(chromeContext); + _ = context; + return Math.Clamp(value, min, max); } - public static double ResolveContentSafetyScale( - ComponentChromeContext? chromeContext = null, - double responsiveness = 0.45d) + public static double Scale(double value, double min, double max, ComponentChromeContext? context = null) { - var scale = ResolveScale(chromeContext); - var normalizedResponsiveness = Math.Clamp(responsiveness, 0d, 1d); - return 1d + ((scale - 1d) * normalizedResponsiveness); + _ = context; + return Math.Clamp(value, min, max); } - public static double SafeValue( - double baseValue, - double min, - double max, - ComponentChromeContext? chromeContext = null, - double responsiveness = 0.45d) + public static CornerRadius SafeRadius(double value, double min, double max, ComponentChromeContext? context = null) { - var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness); - return Math.Clamp(baseValue * safetyScale, min * safetyScale, max * safetyScale); + _ = context; + return new CornerRadius(Math.Clamp(value, min, max)); + } + + public static CornerRadius ScaleRadius(double value, double min, double max, ComponentChromeContext? context = null) + { + _ = context; + return new CornerRadius(Math.Clamp(value, min, max)); + } + + public static double Mini(ComponentChromeContext? context = null) + { + if (context is not null) return context.CornerRadiusTokens.Micro.TopLeft; + return ResolveToken("DesignCornerRadiusMicro", 6).TopLeft; + } + + public static double Micro(ComponentChromeContext? context = null) + { + if (context is not null) return context.CornerRadiusTokens.Micro.TopLeft; + return ResolveToken("DesignCornerRadiusMicro", 6).TopLeft; + } + + public static double Small(ComponentChromeContext? context = null) + { + if (context is not null) return context.CornerRadiusTokens.Sm.TopLeft; + return ResolveToken("DesignCornerRadiusSm", 14).TopLeft; + } + + public static double Medium(ComponentChromeContext? context = null) + { + if (context is not null) return context.CornerRadiusTokens.Md.TopLeft; + return ResolveToken("DesignCornerRadiusMd", 20).TopLeft; + } + + public static double Large(ComponentChromeContext? context = null) + { + if (context is not null) return context.CornerRadiusTokens.Lg.TopLeft; + return ResolveToken("DesignCornerRadiusLg", 28).TopLeft; } } diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs index f2f9e94..908d775 100644 --- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs +++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs @@ -39,8 +39,7 @@ public sealed class DesktopComponentRuntimeRegistration _ => controlFactory(), cornerRadiusResolver is null ? null - : chromeContext => cornerRadiusResolver(chromeContext.CellSize) * - Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale)) + : chromeContext => cornerRadiusResolver(chromeContext.CellSize)) { } @@ -55,8 +54,7 @@ public sealed class DesktopComponentRuntimeRegistration controlFactory, cornerRadiusResolver is null ? null - : chromeContext => cornerRadiusResolver(chromeContext.CellSize) * - Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale)) + : chromeContext => cornerRadiusResolver(chromeContext.CellSize)) { } @@ -131,7 +129,6 @@ public sealed class DesktopComponentRuntimeDescriptor Definition.Id, placementId, cellSize, - appearanceSnapshot.GlobalCornerRadiusScale, appearanceSnapshot.CornerRadiusTokens); var control = _controlFactory(new DesktopComponentControlFactoryContext( Definition, @@ -226,8 +223,7 @@ public sealed class DesktopComponentRuntimeDescriptor Definition.Id, null, Math.Max(1, cellSize), - 1d, - AppearanceCornerRadiusTokenFactory.Create(1d))); + AppearanceCornerRadiusTokenFactory.Create(GlobalAppearanceSettings.DefaultCornerRadiusStyle))); } private static void ApplySettingsDependencies( diff --git a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs index 26367c4..2be11c5 100644 --- a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -730,7 +730,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe _imageHost.Width = imageWidth; _imageHost.Height = imageHeight; - _imageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(imageHeight * 0.15, 8, 16); + _imageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(imageHeight * 0.15, 8, 16); var textWidth = Math.Max(84, innerWidth - imageWidth - columnGap); _titleTextBlock.MaxWidth = textWidth; diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs index 1cf546b..961b5a5 100644 --- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -638,7 +638,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I foreach (var visual in _itemVisuals) { - visual.Host.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(10 * softScale, 6, 14); + visual.Host.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(10 * softScale, 6, 14); visual.Host.Padding = new Thickness(rowPaddingHorizontal, rowPaddingVertical); visual.RowGrid.ColumnSpacing = Math.Clamp(8 * softScale, 4, 12); diff --git a/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs b/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs index 7bad92c..4d62d90 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentPreviewImages.cs @@ -295,7 +295,7 @@ public partial class MainWindow var renderScale = RenderScaling > 0 ? RenderScaling : 1d; return string.Create( CultureInfo.InvariantCulture, - $"{key}|Cell={renderCellSize:F2}|Scale={renderScale:F2}|Night={(appearance.IsNightMode ? 1 : 0)}|Corner={appearance.GlobalCornerRadiusScale:F3}|Accent={FormatSignatureColor(appearance.AccentColor)}"); + $"{key}|Cell={renderCellSize:F2}|Scale={renderScale:F2}|Night={(appearance.IsNightMode ? 1 : 0)}|Corner={appearance.CornerRadiusStyle}|Accent={FormatSignatureColor(appearance.AccentColor)}"); } private ComponentPreviewKey CreateComponentTypePreviewKey(string componentId, int widthCells, int heightCells) diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index 6751f94..a00256d 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -1548,7 +1548,6 @@ public partial class MainWindow var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); return new ComponentLibraryCreateContext( cellSize, - appearanceSnapshot.GlobalCornerRadiusScale, _timeZoneService, _weatherDataService, _recommendationInfoService, @@ -2552,12 +2551,10 @@ public partial class MainWindow componentId, null, _currentDesktopCellSize, - appearanceSnapshot.GlobalCornerRadiusScale, appearanceSnapshot.CornerRadiusTokens)); } - var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, appearanceSnapshot.GlobalCornerRadiusScale); - return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18) * scale; + return Math.Max(0d, appearanceSnapshot.CornerRadiusTokens.Component.TopLeft); } private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells) @@ -2809,7 +2806,6 @@ public partial class MainWindow var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); var createContext = new ComponentLibraryCreateContext( cellSize, - appearanceSnapshot.GlobalCornerRadiusScale, _timeZoneService, _weatherDataService, _recommendationInfoService, diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs index 3276772..61ab1cc 100644 --- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs +++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs @@ -51,6 +51,7 @@ public partial class MainWindow string.Equals(key, nameof(AppSettingsSnapshot.ThemeColorMode), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.SelectedWallpaperSeed), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.LastUpdateCheckUtcMs), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateInstallerPath), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateVersion), StringComparison.OrdinalIgnoreCase) || @@ -611,7 +612,7 @@ public partial class MainWindow SystemMaterialMode = latestThemeState.SystemMaterialMode, SelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed, UseSystemChrome = latestThemeState.UseSystemChrome, - GlobalCornerRadiusScale = latestThemeState.GlobalCornerRadiusScale, + CornerRadiusStyle = latestThemeState.CornerRadiusStyle, WallpaperPath = latestWallpaperState.WallpaperPath, WallpaperType = latestWallpaperState.Type, WallpaperColor = string.Equals(latestWallpaperState.Type, "SolidColor", StringComparison.OrdinalIgnoreCase) diff --git a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml index 240febc..47099c4 100644 --- a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml @@ -73,28 +73,27 @@ Text="{Binding ComponentRadiusHeader}" Margin="0,12,0,4" /> - + - - - - - - - + + + + + + + + + + + + diff --git a/LanMountainDesktop/plugins/PluginLoader.cs b/LanMountainDesktop/plugins/PluginLoader.cs index d421884..ae509b2 100644 --- a/LanMountainDesktop/plugins/PluginLoader.cs +++ b/LanMountainDesktop/plugins/PluginLoader.cs @@ -339,8 +339,7 @@ public sealed class PluginLoader private static PluginAppearanceSnapshot BuildAppearanceSnapshot(IServiceProvider? hostServices) { var defaultSnapshot = new PluginAppearanceSnapshot( - GlobalCornerRadiusScale: 1d, - CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 18), + CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 24), ThemeVariant: "Unknown"); if (hostServices?.GetService(typeof(IAppearanceThemeService)) is not IAppearanceThemeService appearanceThemeService) @@ -352,7 +351,6 @@ public sealed class PluginLoader { var hostSnapshot = appearanceThemeService.GetCurrent(); return new PluginAppearanceSnapshot( - GlobalCornerRadiusScale: Math.Max(0d, hostSnapshot.GlobalCornerRadiusScale), CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(hostSnapshot.CornerRadiusTokens), ThemeVariant: hostSnapshot.IsNightMode ? "Dark" : "Light"); } diff --git a/docs/CORNER_RADIUS_SPEC.md b/docs/CORNER_RADIUS_SPEC.md index 338824d..f9f027f 100644 --- a/docs/CORNER_RADIUS_SPEC.md +++ b/docs/CORNER_RADIUS_SPEC.md @@ -1,39 +1,59 @@ -# 圆角设计规范 +# 圆角设计规范 (LanMountain Desktop Corner Radius Spec) -## 中文 +## 核心理念 (Core Philosophy) -本规范用于统一阑山桌面不同层级容器和控件的圆角尺度。 +为了确保桌面组件在不同尺寸、缩放比例下都能保持视觉一致性和美感,阑山桌面采用了 **固定圆角风格预设 (Fixed Corner Radius Styles)**,全面参考小米澎湃OS (Xiaomi HyperOS) 的设计语言。 -### 基础层级 +所有的组件和容器必须使用统一的资源键,禁止在 XAML 或代码中使用硬编码的像素值。 -- Level 1:12px,小元素和图标容器 -- Level 2:16px,小型色块和紧凑控件 -- Level 3:20px,普通按钮 -- Level 4:24px,输入面板和小型容器 -- Component:18px,桌面组件的标准圆角(默认值) -- Level 5:28px,普通玻璃面板 -- Level 6:32px,强化容器 -- Level 7:36px,大容器、窗口、任务栏 +## 预设风格 (Preset Styles) -### 使用建议 +用户可以在设置中选择以下四种风格之一。系统会自动根据选中的风格动态映射全局圆角 Token。 -- 同层级元素保持相同圆角。 -- 大容器的圆角大于内部子面板。 -- 动态尺寸组件可按 `cellSize` 计算圆角,但仍要落在统一范围内。 +| 风格 (ID) | 名称 (Local) | 组件圆角 (Component) | 设计语义 | +| :--- | :--- | :--- | :--- | +| **Sharp** | 锐利 | 20px | 紧凑、精确、利落 | +| **Balanced** | 平衡 | 24px | **默认值**。和谐、自然、普适 | +| **Rounded** | 圆润 | 28px | 保守、柔和、亲切 | +| **Open** | 开放 | 32px | 现代、沉浸、夸张 | -### 动态圆角建议 +## Token 阶梯映射 (Token Step Mapping) -```csharp -var cornerRadius = Math.Clamp(cellSize * 0.45, 24, 44); -``` +每个风格都定义了一套完整的圆角阶梯,以确保在大容器包裹小元素时满足 **圆角嵌套一致性 (Nesting Consistency)**。 -## English +| Token | Sharp | Balanced | Rounded | Open | 典型场景 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **Micro** | 4px | 6px | 8px | 10px | 小图标容器、角标 (Badge) | +| **Xs** | 8px | 12px | 14px | 16px | 小标签 (Tag)、输入框 | +| **Sm** | 10px | 14px | 16px | 20px | 普通按钮、搜索栏、复选框 | +| **Md** | 14px | 20px | 24px | 28px | 悬浮菜单、小提示框、子卡片 | +| **Lg** | 20px | 28px | 32px | 36px | 普通面板、对话框内容区 | +| **Xl** | 24px | 32px | 36px | 40px | 大尺寸容器、设置中心页面 | +| **Island** | 28px | 36px | 40px | 44px | 任务栏、全局大悬浮容器 | +| **Component** | **20px** | **24px** | **28px** | **32px** | **所有桌面组件 (Widget) 的主边框** | -This specification keeps corner radius usage consistent across containers and controls. +## 开发准则 (Implementation Rules) -### Reference levels +> [!IMPORTANT] +> **1. 桌面组件强制约束**: +> 所有桌面组件(Widget / Desktop Component)的根容器边框必须使用 `{DynamicResource DesignCornerRadiusComponent}`。严禁对其进行任何比例运算或系数乘积(如 `* scale`),必须保持固定。 -- 12px for small elements -- 20px for common buttons -- 28px for normal glass panels -- 36px for large containers and windows +> [!TIP] +> **2. 圆角嵌套规则**: +> 当一个容器包裹另一个元素时,外层圆角应比内层圆角大一个阶梯。例如: +> - 外部使用 `DesignCornerRadiusLg` +> - 内部紧贴边缘的内容应使用 `DesignCornerRadiusMd` +> 这样可以保证两条圆弧的圆心趋于重合,视觉重心更稳固。 + +> [!CAUTION] +> **3. 禁止硬编码 (No Hardcoding)**: +> 禁止写死数字(如 `CornerRadius="24"`)或私有资源。如果现有 Token 无法满足需求,应优先考虑使用 `SafeValue` 辅助方法封装,但必须声明理由。 + +## 常用资源键 (Common Resource Keys) + +- `DesignCornerRadiusComponent` (最常用) +- `DesignCornerRadiusMicro` +- `DesignCornerRadiusSm` +- `DesignCornerRadiusMd` +- `DesignCornerRadiusLg` +- `DesignCornerRadiusXl` diff --git a/docs/TYPOGRAPHY_SPEC.md b/docs/TYPOGRAPHY_SPEC.md new file mode 100644 index 0000000..5a39abc --- /dev/null +++ b/docs/TYPOGRAPHY_SPEC.md @@ -0,0 +1,62 @@ +# 字体排版设计规范 (Typography Specification) + +## 中文 + +本规范用于统一阑山桌面各组件(Widget)及页面的字体样式,解决目前组件间字体不协调、厚度不一的问题。通过引入标准化的设计 Token,确保在不同 DPI 和设备上呈现一致的高级感(Premium Look)。 + +### 1. 字体家族 (Font Family) + +- **默认字体**:优先使用内置的 `MiSans VF` (Variable Font)。 +- **回退顺序**:`MiSans VF` -> `MiSans` -> `Microsoft YaHei` -> `Sans-serif`。 + +### 2. 字重标准 (Font Weights) + +为了达到“不粗不细”的协调感,我们采用 `Medium (500)` 作为默认正文字重,以应对复杂的背景环境。 + +| 角色 | Token | MiSans 权重 | 说明 | +| --- | --- | --- | --- | +| **Caption/Secondary** | `DesignFontWeightCaption` | `Normal (400)` | 用于不重要的补充说明信息 | +| **Body (Default)** | `DesignFontWeightBody` | `Medium (500)` | **核心全局字重**,用于所有常规正文 | +| **Title/Header** | `DesignFontWeightTitle` | `SemiBold (600)` | 用于卡片标题、分类标题 | +| **Display (Large)** | `DesignFontWeightDisplay` | `SemiBold (600)` | 用于超大号文本(如温度数字) | + +> **注意**:除非极特殊艺术需求,应避免使用 `Thin`, `ExtraLight`, `Light` 或 `Bold (700)`, `Heavy`。 + +### 3. 字号标准 (Font Sizes) + +| 角色 | Token | 数值 (px) | 典型应用场景 | +| --- | --- | --- | --- | +| **Caption** | `DesignFontSizeCaption` | 12 | 底部说明、状态提示 | +| **BodySmall** | `DesignFontSizeBodySmall` | 13 | 设置项描述、次要标签 | +| **Body** | `DesignFontSizeBody` | 14 | 标准文本、正文内容 | +| **BodyLarge** | `DesignFontSizeBodyLarge` | 16 | 加大正文、菜单项 | +| **Subtitle** | `DesignFontSizeSubtitle` | 18 | 小节标题、大按钮文字 | +| **Title** | `DesignFontSizeTitle` | 24 | 组件标题、大卡片标题 | +| **Headline** | `DesignFontSizeHeadline` | 32 | 重要数据指标 | +| **Display** | `DesignFontSizeDisplay` | 48 | 天气温度、时间分钟 | +| **DisplayLarge** | `DesignFontSizeDisplayLarge` | 54 | 诗词正文、欢迎语 | + +### 4. 行高标准 (Line Heights) + +统一行高可以增强视觉节奏感。 + +| Token | 数值 (倍率) | 应用场景 | +| --- | --- | --- | +| `DesignLineHeightStandard` | 1.2 | 单行标签、紧凑卡片 | +| `DesignLineHeightLoose` | 1.5 | 多行诗词、新闻摘要、说明文档 | + +### 5. 使用规范 + +1. **禁止硬编码**:严禁在 `.axaml` 中直接写入 `FontSize="18"` 或 `FontWeight="Bold"`。 +2. **动态资源绑定**:始终使用 `{DynamicResource DesignFontSize...}` 进行绑定。 +3. **全局样式继承**:`App.axaml` 已经设置了 `TextBlock` 的默认 `FontWeight` 为 `Medium`,除非是 `Caption` 或 `Title`,否则无需重复声明。 + +--- + +## English (Summary) + +- **Default Font**: MiSans VF. +- **Base Weight**: `Medium (500)` for better readability on glass/dark backgrounds. +- **Header Weight**: `SemiBold (600)` for a modern premium feel. +- **Line Height**: Standardized to 1.2x and 1.5x. +- **Tokens**: All components must use `DesignFontSize...` and `DesignFontWeight...` resource keys. diff --git a/docs/VISUAL_SPEC.md b/docs/VISUAL_SPEC.md index 1a972f9..55e2485 100644 --- a/docs/VISUAL_SPEC.md +++ b/docs/VISUAL_SPEC.md @@ -25,6 +25,12 @@ - `glass-strong`:主要大容器 - `glass-panel`:子区域、小面板、卡片 +### 形状与圆角 (Shape & Corner Radius) + +- **全局统一**:所有 UI 元素的圆角必须遵循 [圆角设计规范](file:///c:/Users/USER154971/Documents/GitHub/LanMountainDesktop/docs/CORNER_RADIUS_SPEC.md)。 +- **禁止硬编码**:严禁在资源库以外的地方硬编码 `CornerRadius` 数值。 +- **动态适配**:桌面组件必须使用 `DesignCornerRadiusComponent` 动态资源,以支持用户在设置中全局切换“锐利/平衡/圆润/开放”风格。 + ### 可访问性 - 正文对比度目标不低于 `4.5:1`