fead.圆角,终于统一

This commit is contained in:
lincube
2026-04-08 00:55:10 +08:00
parent e1d5a0c6de
commit 8583465a67
34 changed files with 514 additions and 340 deletions

View File

@@ -62,7 +62,10 @@ dotnet test LanMountainDesktop.slnx -c Debug
### UI ### UI
- 主题、资源和视觉语义优先遵守 `docs/VISUAL_SPEC.md``docs/CORNER_RADIUS_SPEC.md` - 主题、资源和视觉语义优先遵守 `docs/VISUAL_SPEC.md``docs/CORNER_RADIUS_SPEC.md`
- **组件圆角**:所有内置与插件组件的根边框必须使用 `{DynamicResource DesignCornerRadiusComponent}` 资源。 - **圆角规范 (AI 强制建议)**
- **桌面组件根容器**:必须且仅能使用 `{DynamicResource DesignCornerRadiusComponent}`
- **内部元素**:必须根据嵌套层级使用 `DesignCornerRadiusSm/Md/Lg` 等 Token严禁硬编码像素值。
- **禁止修改系数**:严禁在圆角资源上乘以任何 `scale` 变量,圆角现在由全局样式固定控制。
- 设置页相关改动通常同时落在 `Views/``ViewModels/``Services/``.trae/specs/` - 设置页相关改动通常同时落在 `Views/``ViewModels/``Services/``.trae/specs/`
- UI 启动与窗口生命周期主线在 `Program.cs``App.axaml.cs` - UI 启动与窗口生命周期主线在 `Program.cs``App.axaml.cs`

View File

@@ -6,23 +6,48 @@ namespace LanMountainDesktop.Appearance;
public static class AppearanceCornerRadiusTokenFactory public static class AppearanceCornerRadiusTokenFactory
{ {
public static AppearanceCornerRadiusTokens Create(double scale) public static AppearanceCornerRadiusTokens Create(string style)
{ {
var normalizedScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(scale); var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(style);
return new AppearanceCornerRadiusTokens( return normalized switch
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; GlobalAppearanceSettings.CornerRadiusStyleSharp => new AppearanceCornerRadiusTokens(
return new CornerRadius(scaled); 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))
};
} }
} }

View File

@@ -7,6 +7,5 @@ public sealed record ComponentChromeContext(
string ComponentId, string ComponentId,
string? PlacementId, string? PlacementId,
double CellSize, double CellSize,
double GlobalCornerRadiusScale,
AppearanceCornerRadiusTokens CornerRadiusTokens, AppearanceCornerRadiusTokens CornerRadiusTokens,
SettingsScope Scope = SettingsScope.App); SettingsScope Scope = SettingsScope.App);

View File

@@ -9,7 +9,6 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext
Snapshot = snapshot with Snapshot = snapshot with
{ {
GlobalCornerRadiusScale = Math.Max(0d, snapshot.GlobalCornerRadiusScale),
ThemeVariant = string.IsNullOrWhiteSpace(snapshot.ThemeVariant) ThemeVariant = string.IsNullOrWhiteSpace(snapshot.ThemeVariant)
? "Unknown" ? "Unknown"
: snapshot.ThemeVariant.Trim() : snapshot.ThemeVariant.Trim()
@@ -20,13 +19,15 @@ public sealed class PluginAppearanceContext : IPluginAppearanceContext
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null) public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
{ {
var scale = Snapshot.GlobalCornerRadiusScale; var value = Math.Max(0d, baseRadius);
var scaled = Math.Max(0d, baseRadius) * scale; if (!minimum.HasValue && !maximum.HasValue)
var scaledMin = minimum.HasValue ? minimum.Value * scale : scaled; {
var scaledMax = maximum.HasValue ? maximum.Value * scale : scaled; return value;
return minimum.HasValue || maximum.HasValue }
? Math.Clamp(scaled, scaledMin, scaledMax)
: scaled; 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) public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)

View File

@@ -1,6 +1,5 @@
namespace LanMountainDesktop.PluginSdk; namespace LanMountainDesktop.PluginSdk;
public sealed record PluginAppearanceSnapshot( public sealed record PluginAppearanceSnapshot(
double GlobalCornerRadiusScale,
PluginCornerRadiusTokens CornerRadiusTokens, PluginCornerRadiusTokens CornerRadiusTokens,
string ThemeVariant); string ThemeVariant);

View File

@@ -52,8 +52,6 @@ public sealed class PluginDesktopComponentContext
public IPluginAppearanceContext Appearance { get; } public IPluginAppearanceContext Appearance { get; }
public double GlobalCornerRadiusScale => Appearance.Snapshot.GlobalCornerRadiusScale;
public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens; public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens;
public IPluginSettingsService? PluginSettings { get; } public IPluginSettingsService? PluginSettings { get; }

View File

@@ -2,17 +2,69 @@ namespace LanMountainDesktop.Settings.Core;
public static class GlobalAppearanceSettings 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;
/// <summary>
/// Kept for backward compatibility during settings migration.
/// New code should not reference this constant.
/// </summary>
public const double DefaultCornerRadiusScale = 1.0; public const double DefaultCornerRadiusScale = 1.0;
public const double MinimumCornerRadiusScale = 0.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<string> AllCornerRadiusStyles =
[
CornerRadiusStyleSharp,
CornerRadiusStyleBalanced,
CornerRadiusStyleRounded,
CornerRadiusStyleOpen
];
/// <summary>
/// Backward compatibility: map previous scale values to the closest style.
/// </summary>
public static string MigrateScaleToStyle(double scale)
{
return scale switch
{
<= 0.60 => CornerRadiusStyleSharp,
<= 1.20 => CornerRadiusStyleBalanced,
<= 1.70 => CornerRadiusStyleRounded,
_ => CornerRadiusStyleOpen
};
} }
} }

View File

@@ -11,19 +11,19 @@ namespace LanMountainDesktop.Tests;
public sealed class BuiltInDesktopHostCornerRadiusBaselineTests public sealed class BuiltInDesktopHostCornerRadiusBaselineTests
{ {
[Theory] [Theory]
[InlineData(80d, 0d)] [InlineData(80d, "Sharp")]
[InlineData(120d, 1d)] [InlineData(120d, "Balanced")]
[InlineData(160d, 2.5d)] [InlineData(160d, "Rounded")]
public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, double globalScale) public void BuiltInDesktopHosts_ResolveToTheUnifiedLgBaseline(double cellSize, string style)
{ {
var registry = new DesktopComponentRuntimeRegistry( var registry = new DesktopComponentRuntimeRegistry(
ComponentRegistry.CreateDefault(), ComponentRegistry.CreateDefault(),
DesktopComponentRuntimeRegistry.GetDefaultRegistrations()); DesktopComponentRuntimeRegistry.GetDefaultRegistrations());
var expected = AppearanceCornerRadiusTokenFactory.Create(globalScale).Component.TopLeft; var expected = AppearanceCornerRadiusTokenFactory.Create(style).Component.TopLeft;
foreach (var descriptor in registry.GetDesktopComponents()) 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); Assert.Equal(expected, resolved, 3);
} }
} }
@@ -31,13 +31,12 @@ public sealed class BuiltInDesktopHostCornerRadiusBaselineTests
private static ComponentChromeContext CreateChromeContext( private static ComponentChromeContext CreateChromeContext(
string componentId, string componentId,
double cellSize, double cellSize,
double globalScale) string style)
{ {
return new ComponentChromeContext( return new ComponentChromeContext(
componentId, componentId,
null, null,
cellSize, cellSize,
globalScale, AppearanceCornerRadiusTokenFactory.Create(style));
AppearanceCornerRadiusTokenFactory.Create(globalScale));
} }
} }

View File

@@ -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<string, object?>(),
"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;
}
}

View File

@@ -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<string, object?>(),
"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;
}
}

View File

@@ -10,7 +10,7 @@ namespace LanMountainDesktop.Tests;
public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests
{ {
[Fact] [Fact]
public void LegacyCellSizeResolver_AppliesGlobalCornerRadiusScale() public void LegacyCellSizeResolver_ReturnsUnscaledFixedValue()
{ {
var registration = new DesktopComponentRuntimeRegistration( var registration = new DesktopComponentRuntimeRegistration(
componentId: "test.component", componentId: "test.component",
@@ -19,41 +19,42 @@ public sealed class DesktopComponentRuntimeRegistrationCornerRadiusTests
cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.30, 10, 40)); cornerRadiusResolver: cellSize => Math.Clamp(cellSize * 0.30, 10, 40));
var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(registration.CornerRadiusResolver); var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(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] [Fact]
public void ChromeContextResolver_IsNotDoubleScaledByRegistrationWrapper() public void ChromeContextResolver_UsesTokenValue()
{ {
var registration = new DesktopComponentRuntimeRegistration( var registration = new DesktopComponentRuntimeRegistration(
componentId: "test.component", componentId: "test.component",
displayNameLocalizationKey: null, displayNameLocalizationKey: null,
controlFactory: _ => new Border(), controlFactory: _ => new Border(),
cornerRadiusResolver: chromeContext => chromeContext.CellSize + chromeContext.GlobalCornerRadiusScale); cornerRadiusResolver: chromeContext => chromeContext.CornerRadiusTokens.Component.TopLeft);
var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(registration.CornerRadiusResolver); var resolver = Assert.IsType<Func<ComponentChromeContext, double>>(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( return new ComponentChromeContext(
ComponentId: "test.component", ComponentId: "test.component",
PlacementId: null, PlacementId: null,
CellSize: cellSize, CellSize: cellSize,
GlobalCornerRadiusScale: globalScale,
CornerRadiusTokens: new AppearanceCornerRadiusTokens( CornerRadiusTokens: new AppearanceCornerRadiusTokens(
new CornerRadius(6), Micro: new CornerRadius(6),
new CornerRadius(12), Xs: new CornerRadius(12),
new CornerRadius(14), Sm: new CornerRadius(14),
new CornerRadius(20), Md: new CornerRadius(20),
new CornerRadius(28), Lg: new CornerRadius(28),
new CornerRadius(32), Xl: new CornerRadius(32),
new CornerRadius(36), Island: new CornerRadius(36),
new CornerRadius(8))); Component: new CornerRadius(24)));
} }
} }

View File

@@ -48,26 +48,27 @@ public sealed class InfoRecommendationHostCornerRadiusTests
registry.TryGetDescriptor(componentId, out var descriptor), registry.TryGetDescriptor(componentId, out var descriptor),
$"Missing runtime registration for '{componentId}'."); $"Missing runtime registration for '{componentId}'.");
var zero = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 0d)); var sharp = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Sharp"));
var unit = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 1d)); var balanced = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Balanced"));
var max = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, 2.5d)); var rounded = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Rounded"));
var open = descriptor.ResolveCornerRadius(CreateChromeContext(componentId, cellSize, "Open"));
Assert.Equal(0d, zero, 3); // All info widgets should resolve to the Component token in the new system
Assert.Equal(18d, unit, 3); Assert.Equal(20d, sharp, 3);
Assert.Equal(45d, max, 3); Assert.Equal(24d, balanced, 3);
Assert.True(zero <= unit && unit <= max); Assert.Equal(28d, rounded, 3);
Assert.Equal(32d, open, 3);
} }
private static ComponentChromeContext CreateChromeContext( private static ComponentChromeContext CreateChromeContext(
string componentId, string componentId,
double cellSize, double cellSize,
double globalScale) string style)
{ {
return new ComponentChromeContext( return new ComponentChromeContext(
componentId, componentId,
null, null,
cellSize, cellSize,
globalScale, AppearanceCornerRadiusTokenFactory.Create(style));
AppearanceCornerRadiusTokenFactory.Create(globalScale));
} }
} }

View File

@@ -664,7 +664,7 @@ public partial class App : Application
refreshAll || refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), 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) && (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) || changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) && (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&

View File

@@ -19,6 +19,8 @@ public sealed class AppSettingsSnapshot
public double GlobalCornerRadiusScale { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusScale; public double GlobalCornerRadiusScale { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusScale;
public string CornerRadiusStyle { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusStyle;
public string ThemeColorMode { get; set; } = "default_neutral"; public string ThemeColorMode { get; set; } = "default_neutral";
public string SystemMaterialMode { get; set; } = "none"; public string SystemMaterialMode { get; set; } = "none";

View File

@@ -44,7 +44,7 @@ public sealed record AppearanceThemeSnapshot(
string ThemeColorMode, string ThemeColorMode,
string? UserThemeColor, string? UserThemeColor,
string? SelectedWallpaperSeed, string? SelectedWallpaperSeed,
double GlobalCornerRadiusScale, string CornerRadiusStyle,
AppearanceCornerRadiusTokens CornerRadiusTokens, AppearanceCornerRadiusTokens CornerRadiusTokens,
string ResolvedSeedSource, string ResolvedSeedSource,
MonetPalette MonetPalette, MonetPalette MonetPalette,
@@ -551,7 +551,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
if (!refreshAll && if (!refreshAll &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) && !changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) && !changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) &&
!changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) && !changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) &&
!(respondsToThemeColor && !(respondsToThemeColor &&
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) && changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) &&
!(respondsToWallpaper && !(respondsToWallpaper &&
@@ -573,8 +573,8 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
bool queueWallpaperPaletteBuild) bool queueWallpaperPaletteBuild)
{ {
var availableModes = _windowMaterialService.GetAvailableModes(); var availableModes = _windowMaterialService.GetAvailableModes();
var globalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(themeState.GlobalCornerRadiusScale); var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(themeState.CornerRadiusStyle);
var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(globalCornerRadiusScale); var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(cornerRadiusStyle);
MonetPalette palette; MonetPalette palette;
IReadOnlyList<Color> wallpaperSeedCandidates; IReadOnlyList<Color> wallpaperSeedCandidates;
Color effectiveSeedColor; Color effectiveSeedColor;
@@ -614,7 +614,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
themeColorMode, themeColorMode,
themeState.ThemeColor, themeState.ThemeColor,
selectedWallpaperSeed, selectedWallpaperSeed,
globalCornerRadiusScale, cornerRadiusStyle,
cornerRadiusTokens, cornerRadiusTokens,
resolvedSeedSource, resolvedSeedSource,
palette, palette,

View File

@@ -129,7 +129,6 @@ public static class DesktopComponentRegistryFactory
settingsService); settingsService);
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
var pluginAppearance = new PluginAppearanceContext(new PluginAppearanceSnapshot( var pluginAppearance = new PluginAppearanceContext(new PluginAppearanceSnapshot(
GlobalCornerRadiusScale: appearanceSnapshot.GlobalCornerRadiusScale,
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(appearanceSnapshot.CornerRadiusTokens), CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(appearanceSnapshot.CornerRadiusTokens),
ThemeVariant: appearanceSnapshot.IsNightMode ? "Dark" : "Light")); ThemeVariant: appearanceSnapshot.IsNightMode ? "Dark" : "Light"));
var pluginContext = new PluginDesktopComponentContext( var pluginContext = new PluginDesktopComponentContext(
@@ -157,7 +156,6 @@ public static class DesktopComponentRegistryFactory
private static IPluginAppearanceContext CreatePluginAppearanceContext(ComponentChromeContext chromeContext) private static IPluginAppearanceContext CreatePluginAppearanceContext(ComponentChromeContext chromeContext)
{ {
return new PluginAppearanceContext(new PluginAppearanceSnapshot( return new PluginAppearanceContext(new PluginAppearanceSnapshot(
GlobalCornerRadiusScale: chromeContext.GlobalCornerRadiusScale,
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(chromeContext.CornerRadiusTokens), CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(chromeContext.CornerRadiusTokens),
ThemeVariant: "Unknown")); ThemeVariant: "Unknown"));
} }

View File

@@ -20,7 +20,6 @@ public sealed record ComponentLibraryCategoryEntry(
public sealed record ComponentLibraryCreateContext( public sealed record ComponentLibraryCreateContext(
double CellSize, double CellSize,
double GlobalCornerRadiusScale,
TimeZoneService TimeZoneService, TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService, IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService, IRecommendationInfoService RecommendationInfoService,

View File

@@ -30,7 +30,7 @@ public sealed record ThemeAppearanceSettingsState(
bool IsNightMode, bool IsNightMode,
string? ThemeColor, string? ThemeColor,
bool UseSystemChrome, bool UseSystemChrome,
double GlobalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale, string CornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle,
string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral, string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral,
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone, string SystemMaterialMode = ThemeAppearanceValues.MaterialNone,
string? SelectedWallpaperSeed = null); string? SelectedWallpaperSeed = null);

View File

@@ -254,11 +254,19 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
public ThemeAppearanceSettingsState Get() public ThemeAppearanceSettingsState Get()
{ {
var snapshot = _settingsService.Load(); 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( return new ThemeAppearanceSettingsState(
snapshot.IsNightMode ?? false, snapshot.IsNightMode ?? false,
snapshot.ThemeColor, snapshot.ThemeColor,
snapshot.UseSystemChrome, snapshot.UseSystemChrome,
GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale), cornerRadiusStyle,
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor), ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode), ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
snapshot.SelectedWallpaperSeed); snapshot.SelectedWallpaperSeed);
@@ -269,7 +277,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
var snapshot = _settingsService.Load(); var snapshot = _settingsService.Load();
var changedKeys = new List<string>(); var changedKeys = new List<string>();
var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor; 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 normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor);
var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode); var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode);
var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed) var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed)
@@ -294,10 +302,10 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome)); 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; snapshot.CornerRadiusStyle = normalizedCornerRadiusStyle;
changedKeys.Add(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale)); changedKeys.Add(nameof(AppSettingsSnapshot.CornerRadiusStyle));
} }
if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase))

View File

@@ -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 partial class MainWindowViewModel : ViewModelBase
{ {
public string Greeting { get; } = "A modern desktop shell powered by FluentAvalonia."; 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
});
}
}
} }

View File

@@ -614,10 +614,10 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
private string _systemMaterialLabel = string.Empty; private string _systemMaterialLabel = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _globalCornerRadiusLabel = string.Empty; private string _cornerRadiusStyleLabel = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _globalCornerRadiusDescription = string.Empty; private string _cornerRadiusStyleDescription = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _themeHeader = string.Empty; private string _themeHeader = string.Empty;
@@ -701,6 +701,15 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
public IBrush NeutralDarkPreviewBrush => NeutralDarkBrushValue; public IBrush NeutralDarkPreviewBrush => NeutralDarkBrushValue;
[ObservableProperty]
private string _cornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle;
[ObservableProperty]
private IReadOnlyList<SelectionOption> _cornerRadiusStyleOptions = [];
[ObservableProperty]
private SelectionOption? _selectedCornerRadiusStyle;
public void Load() public void Load()
{ {
var theme = _settingsFacade.Theme.Get(); var theme = _settingsFacade.Theme.Get();
@@ -740,29 +749,14 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
PersistCurrentState(restartRequired: false); PersistCurrentState(restartRequired: false);
} }
partial void OnGlobalCornerRadiusScaleChanged(double value) partial void OnSelectedCornerRadiusStyleChanged(SelectionOption? value)
{ {
if (_isInitializing) if (_isInitializing || value is null)
{ {
return; return;
} }
var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value); CornerRadiusStyle = value.Value;
if (Math.Abs(normalized - value) > 0.0001d)
{
_isInitializing = true;
try
{
GlobalCornerRadiusScale = normalized;
}
finally
{
_isInitializing = false;
}
return;
}
PersistCurrentState(restartRequired: false); PersistCurrentState(restartRequired: false);
} }
@@ -830,8 +824,12 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color"); ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source"); ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
SystemMaterialLabel = L("settings.appearance.system_material_label", "System material"); SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
GlobalCornerRadiusLabel = L("settings.appearance.corner_radius.label", "Global corner radius"); CornerRadiusStyleLabel = L("settings.appearance.corner_radius.label", "Global corner radius style");
GlobalCornerRadiusDescription = L("settings.appearance.corner_radius.description", "Adjust the shared radius scale used by cards, panels, and component containers."); 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"); ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet"); ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet"); ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
@@ -876,7 +874,10 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
IsNightMode = theme.IsNightMode; IsNightMode = theme.IsNightMode;
ThemeColor = theme.ThemeColor ?? string.Empty; ThemeColor = theme.ThemeColor ?? string.Empty;
UseSystemChrome = theme.UseSystemChrome; 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; _selectedWallpaperSeed = theme.SelectedWallpaperSeed;
SyncCustomSeedPickerWithSavedThemeColor(); SyncCustomSeedPickerWithSavedThemeColor();
@@ -926,7 +927,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
IsNightMode, IsNightMode,
themeColor, themeColor,
UseSystemChrome, UseSystemChrome,
GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale), GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
themeColorMode, themeColorMode,
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value), ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
_selectedWallpaperSeed); _selectedWallpaperSeed);
@@ -1070,20 +1071,22 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
private string _spacingPresetLabel = string.Empty; private string _spacingPresetLabel = string.Empty;
[ObservableProperty] [ObservableProperty]
private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale; private string _cornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle;
public double GlobalCornerRadiusMinimum => GlobalAppearanceSettings.MinimumCornerRadiusScale; [ObservableProperty]
private IReadOnlyList<SelectionOption> _cornerRadiusStyleOptions = [];
public double GlobalCornerRadiusMaximum => GlobalAppearanceSettings.MaximumCornerRadiusScale; [ObservableProperty]
private SelectionOption? _selectedCornerRadiusStyle;
[ObservableProperty] [ObservableProperty]
private string _componentRadiusHeader = string.Empty; private string _componentRadiusHeader = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _globalCornerRadiusLabel = string.Empty; private string _cornerRadiusStyleLabel = string.Empty;
[ObservableProperty] [ObservableProperty]
private string _globalCornerRadiusDescription = string.Empty; private string _cornerRadiusStyleDescription = string.Empty;
public void Load() public void Load()
{ {
@@ -1096,7 +1099,10 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
?? SpacingPresets[1]; ?? SpacingPresets[1];
var theme = _settingsFacade.Theme.Get(); 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) partial void OnShortSideCellsChanged(int value)
@@ -1129,29 +1135,14 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
SaveGrid(); SaveGrid();
} }
partial void OnGlobalCornerRadiusScaleChanged(double value) partial void OnSelectedCornerRadiusStyleChanged(SelectionOption? value)
{ {
if (_isInitializing) if (_isInitializing || value is null)
{ {
return; return;
} }
var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value); CornerRadiusStyle = value.Value;
if (Math.Abs(normalized - value) > 0.0001d)
{
_isInitializing = true;
try
{
GlobalCornerRadiusScale = normalized;
}
finally
{
_isInitializing = false;
}
return;
}
SaveComponentCornerRadius(); SaveComponentCornerRadius();
} }
@@ -1170,7 +1161,7 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
theme.IsNightMode, theme.IsNightMode,
theme.ThemeColor, theme.ThemeColor,
theme.UseSystemChrome, theme.UseSystemChrome,
GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale), GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
theme.ThemeColorMode, theme.ThemeColorMode,
theme.SystemMaterialMode, theme.SystemMaterialMode,
theme.SelectedWallpaperSeed)); theme.SelectedWallpaperSeed));
@@ -1194,10 +1185,14 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
EdgeInsetPercentLabel = L("settings.components.edge_inset_label", "Screen Inset"); EdgeInsetPercentLabel = L("settings.components.edge_inset_label", "Screen Inset");
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"); CornerRadiusStyleLabel = L("settings.components.corner_radius.label", "Component Corner Radius Style");
GlobalCornerRadiusDescription = L( CornerRadiusStyleDescription = L(
"settings.components.corner_radius.description", "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) private string L(string key, string fallback)

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -552,7 +552,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
{ {
Width = 160, Width = 160,
Height = 90, Height = 90,
CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16, 8, 22), CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16, 8, 22),
ClipToBounds = true, ClipToBounds = true,
Background = new SolidColorBrush(Color.Parse("#E6E6E6")), Background = new SolidColorBrush(Color.Parse("#E6E6E6")),
IsHitTestVisible = false IsHitTestVisible = false
@@ -647,8 +647,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
News1ImageHost.Height = imageHeight; News1ImageHost.Height = imageHeight;
News2ImageHost.Width = imageWidth; News2ImageHost.Width = imageWidth;
News2ImageHost.Height = imageHeight; News2ImageHost.Height = imageHeight;
News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22);
News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.ScaleRadius(16 * scale, 8, 22);
News1ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6")); News1ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
News2ImageHost.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.Width = imageWidth;
row.ImageHost.Height = imageHeight; 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.ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
row.TitleTextBlock.MaxWidth = availableTextWidth; row.TitleTextBlock.MaxWidth = availableTextWidth;

View File

@@ -3,13 +3,14 @@ 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.Services.Settings;
using LanMountainDesktop.Settings.Core; using LanMountainDesktop.Settings.Core;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
internal static class ComponentChromeCornerRadiusHelper 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) if (chromeContext is not null)
{ {
@@ -20,7 +21,7 @@ internal static class ComponentChromeCornerRadiusHelper
var resolved = snapshot.CornerRadiusTokens.Component.TopLeft; var resolved = snapshot.CornerRadiusTokens.Component.TopLeft;
return double.IsFinite(resolved) return double.IsFinite(resolved)
? Math.Max(0d, 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) public static CornerRadius ResolveMainRectangleRadius(ComponentChromeContext? chromeContext = null, double fallback = 24d)
@@ -28,24 +29,6 @@ internal static class ComponentChromeCornerRadiusHelper
return new CornerRadius(ResolveMainRectangleRadiusValue(chromeContext, fallback)); 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) public static void Apply(CornerRadius radius, params Border?[] chromeLayers)
{ {
foreach (var chromeLayer in chromeLayers) foreach (var chromeLayer in chromeLayers)
@@ -67,28 +50,57 @@ internal static class ComponentChromeCornerRadiusHelper
: new CornerRadius(fallback); : 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( public static double Scale(double value, double min, double max, ComponentChromeContext? context = null)
ComponentChromeContext? chromeContext = null,
double responsiveness = 0.45d)
{ {
var scale = ResolveScale(chromeContext); _ = context;
var normalizedResponsiveness = Math.Clamp(responsiveness, 0d, 1d); return Math.Clamp(value, min, max);
return 1d + ((scale - 1d) * normalizedResponsiveness);
} }
public static double SafeValue( public static CornerRadius SafeRadius(double value, double min, double max, ComponentChromeContext? context = null)
double baseValue,
double min,
double max,
ComponentChromeContext? chromeContext = null,
double responsiveness = 0.45d)
{ {
var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness); _ = context;
return Math.Clamp(baseValue * safetyScale, min * safetyScale, max * safetyScale); 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;
} }
} }

View File

@@ -39,8 +39,7 @@ public sealed class DesktopComponentRuntimeRegistration
_ => controlFactory(), _ => controlFactory(),
cornerRadiusResolver is null cornerRadiusResolver is null
? null ? null
: chromeContext => cornerRadiusResolver(chromeContext.CellSize) * : chromeContext => cornerRadiusResolver(chromeContext.CellSize))
Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale))
{ {
} }
@@ -55,8 +54,7 @@ public sealed class DesktopComponentRuntimeRegistration
controlFactory, controlFactory,
cornerRadiusResolver is null cornerRadiusResolver is null
? null ? null
: chromeContext => cornerRadiusResolver(chromeContext.CellSize) * : chromeContext => cornerRadiusResolver(chromeContext.CellSize))
Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, chromeContext.GlobalCornerRadiusScale))
{ {
} }
@@ -131,7 +129,6 @@ public sealed class DesktopComponentRuntimeDescriptor
Definition.Id, Definition.Id,
placementId, placementId,
cellSize, cellSize,
appearanceSnapshot.GlobalCornerRadiusScale,
appearanceSnapshot.CornerRadiusTokens); appearanceSnapshot.CornerRadiusTokens);
var control = _controlFactory(new DesktopComponentControlFactoryContext( var control = _controlFactory(new DesktopComponentControlFactoryContext(
Definition, Definition,
@@ -226,8 +223,7 @@ public sealed class DesktopComponentRuntimeDescriptor
Definition.Id, Definition.Id,
null, null,
Math.Max(1, cellSize), Math.Max(1, cellSize),
1d, AppearanceCornerRadiusTokenFactory.Create(GlobalAppearanceSettings.DefaultCornerRadiusStyle)));
AppearanceCornerRadiusTokenFactory.Create(1d)));
} }
private static void ApplySettingsDependencies( private static void ApplySettingsDependencies(

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -730,7 +730,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
_imageHost.Width = imageWidth; _imageHost.Width = imageWidth;
_imageHost.Height = imageHeight; _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); var textWidth = Math.Max(84, innerWidth - imageWidth - columnGap);
_titleTextBlock.MaxWidth = textWidth; _titleTextBlock.MaxWidth = textWidth;

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@@ -638,7 +638,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
foreach (var visual in _itemVisuals) 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.Host.Padding = new Thickness(rowPaddingHorizontal, rowPaddingVertical);
visual.RowGrid.ColumnSpacing = Math.Clamp(8 * softScale, 4, 12); visual.RowGrid.ColumnSpacing = Math.Clamp(8 * softScale, 4, 12);

View File

@@ -295,7 +295,7 @@ public partial class MainWindow
var renderScale = RenderScaling > 0 ? RenderScaling : 1d; var renderScale = RenderScaling > 0 ? RenderScaling : 1d;
return string.Create( return string.Create(
CultureInfo.InvariantCulture, 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) private ComponentPreviewKey CreateComponentTypePreviewKey(string componentId, int widthCells, int heightCells)

View File

@@ -1548,7 +1548,6 @@ public partial class MainWindow
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
return new ComponentLibraryCreateContext( return new ComponentLibraryCreateContext(
cellSize, cellSize,
appearanceSnapshot.GlobalCornerRadiusScale,
_timeZoneService, _timeZoneService,
_weatherDataService, _weatherDataService,
_recommendationInfoService, _recommendationInfoService,
@@ -2552,12 +2551,10 @@ public partial class MainWindow
componentId, componentId,
null, null,
_currentDesktopCellSize, _currentDesktopCellSize,
appearanceSnapshot.GlobalCornerRadiusScale,
appearanceSnapshot.CornerRadiusTokens)); appearanceSnapshot.CornerRadiusTokens));
} }
var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, appearanceSnapshot.GlobalCornerRadiusScale); return Math.Max(0d, appearanceSnapshot.CornerRadiusTokens.Component.TopLeft);
return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18) * scale;
} }
private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells) private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells)
@@ -2809,7 +2806,6 @@ public partial class MainWindow
var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent();
var createContext = new ComponentLibraryCreateContext( var createContext = new ComponentLibraryCreateContext(
cellSize, cellSize,
appearanceSnapshot.GlobalCornerRadiusScale,
_timeZoneService, _timeZoneService,
_weatherDataService, _weatherDataService,
_recommendationInfoService, _recommendationInfoService,

View File

@@ -51,6 +51,7 @@ public partial class MainWindow
string.Equals(key, nameof(AppSettingsSnapshot.ThemeColorMode), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.ThemeColorMode), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.SelectedWallpaperSeed), 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.LastUpdateCheckUtcMs), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateInstallerPath), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateInstallerPath), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateVersion), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.PendingUpdateVersion), StringComparison.OrdinalIgnoreCase) ||
@@ -611,7 +612,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, CornerRadiusStyle = latestThemeState.CornerRadiusStyle,
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

@@ -73,28 +73,27 @@
Text="{Binding ComponentRadiusHeader}" Text="{Binding ComponentRadiusHeader}"
Margin="0,12,0,4" /> Margin="0,12,0,4" />
<ui:SettingsExpander Header="{Binding GlobalCornerRadiusLabel}" <ui:SettingsExpander Header="{Binding CornerRadiusStyleLabel}"
Description="{Binding GlobalCornerRadiusDescription}"> Description="{Binding CornerRadiusStyleDescription}">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ShapeOrganic" /> <fi:SymbolIconSource Symbol="ShapeOrganic" />
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem> <ui:SettingsExpander.Footer>
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="16"> <StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding GlobalCornerRadiusLabel}" <ComboBox Width="200"
VerticalAlignment="Center" /> ItemsSource="{Binding CornerRadiusStyleOptions}"
<Slider Grid.Column="1" SelectedItem="{Binding SelectedCornerRadiusStyle}">
Minimum="{Binding GlobalCornerRadiusMinimum}" <ComboBox.ItemTemplate>
Maximum="{Binding GlobalCornerRadiusMaximum}" <DataTemplate x:DataType="vm:SelectionOption">
SmallChange="0.01" <TextBlock Text="{Binding Label}" />
LargeChange="0.1" </DataTemplate>
Value="{Binding GlobalCornerRadiusScale}" /> </ComboBox.ItemTemplate>
<TextBlock Grid.Column="2" </ComboBox>
Width="56" <Button Classes="AppBarButton" ToolTip.Tip="View Corner Radius Specification" Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).OpenDesignSpecCommand}" CommandParameter="CORNER_RADIUS_SPEC.md">
Text="{Binding GlobalCornerRadiusScale, StringFormat={}{0:F2}x}" <fi:SymbolIcon Symbol="QuestionCircle" />
VerticalAlignment="Center" </Button>
HorizontalAlignment="Right" /> </StackPanel>
</Grid> </ui:SettingsExpander.Footer>
</ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>

View File

@@ -339,8 +339,7 @@ public sealed class PluginLoader
private static PluginAppearanceSnapshot BuildAppearanceSnapshot(IServiceProvider? hostServices) private static PluginAppearanceSnapshot BuildAppearanceSnapshot(IServiceProvider? hostServices)
{ {
var defaultSnapshot = new PluginAppearanceSnapshot( var defaultSnapshot = new PluginAppearanceSnapshot(
GlobalCornerRadiusScale: 1d, CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 24),
CornerRadiusTokens: new PluginCornerRadiusTokens(6, 12, 14, 20, 28, 32, 36, 18),
ThemeVariant: "Unknown"); ThemeVariant: "Unknown");
if (hostServices?.GetService(typeof(IAppearanceThemeService)) is not IAppearanceThemeService appearanceThemeService) if (hostServices?.GetService(typeof(IAppearanceThemeService)) is not IAppearanceThemeService appearanceThemeService)
@@ -352,7 +351,6 @@ public sealed class PluginLoader
{ {
var hostSnapshot = appearanceThemeService.GetCurrent(); var hostSnapshot = appearanceThemeService.GetCurrent();
return new PluginAppearanceSnapshot( return new PluginAppearanceSnapshot(
GlobalCornerRadiusScale: Math.Max(0d, hostSnapshot.GlobalCornerRadiusScale),
CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(hostSnapshot.CornerRadiusTokens), CornerRadiusTokens: PluginCornerRadiusTokens.FromShared(hostSnapshot.CornerRadiusTokens),
ThemeVariant: hostSnapshot.IsNightMode ? "Dark" : "Light"); ThemeVariant: hostSnapshot.IsNightMode ? "Dark" : "Light");
} }

View File

@@ -1,39 +1,59 @@
# 圆角设计规范 # 圆角设计规范 (LanMountain Desktop Corner Radius Spec)
## 中文 ## 核心理念 (Core Philosophy)
本规范用于统一阑山桌面不同层级容器和控件的圆角尺度 为了确保桌面组件在不同尺寸、缩放比例下都能保持视觉一致性和美感,阑山桌面采用了 **固定圆角风格预设 (Fixed Corner Radius Styles)**全面参考小米澎湃OS (Xiaomi HyperOS) 的设计语言
### 基础层级 所有的组件和容器必须使用统一的资源键,禁止在 XAML 或代码中使用硬编码的像素值。
- Level 112px小元素和图标容器 ## 预设风格 (Preset Styles)
- Level 216px小型色块和紧凑控件
- Level 320px普通按钮
- Level 424px输入面板和小型容器
- Component18px桌面组件的标准圆角默认值
- Level 528px普通玻璃面板
- Level 632px强化容器
- Level 736px大容器、窗口、任务栏
### 使用建议 用户可以在设置中选择以下四种风格之一。系统会自动根据选中的风格动态映射全局圆角 Token。
- 同层级元素保持相同圆角。 | 风格 (ID) | 名称 (Local) | 组件圆角 (Component) | 设计语义 |
- 大容器的圆角大于内部子面板。 | :--- | :--- | :--- | :--- |
- 动态尺寸组件可按 `cellSize` 计算圆角,但仍要落在统一范围内。 | **Sharp** | 锐利 | 20px | 紧凑、精确、利落 |
| **Balanced** | 平衡 | 24px | **默认值**。和谐、自然、普适 |
| **Rounded** | 圆润 | 28px | 保守、柔和、亲切 |
| **Open** | 开放 | 32px | 现代、沉浸、夸张 |
### 动态圆角建议 ## Token 阶梯映射 (Token Step Mapping)
```csharp 每个风格都定义了一套完整的圆角阶梯,以确保在大容器包裹小元素时满足 **圆角嵌套一致性 (Nesting Consistency)**
var cornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
```
## 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 > [!TIP]
- 20px for common buttons > **2. 圆角嵌套规则**
- 28px for normal glass panels > 当一个容器包裹另一个元素时,外层圆角应比内层圆角大一个阶梯。例如:
- 36px for large containers and windows > - 外部使用 `DesignCornerRadiusLg`
> - 内部紧贴边缘的内容应使用 `DesignCornerRadiusMd`
> 这样可以保证两条圆弧的圆心趋于重合,视觉重心更稳固。
> [!CAUTION]
> **3. 禁止硬编码 (No Hardcoding)**
> 禁止写死数字(如 `CornerRadius="24"`)或私有资源。如果现有 Token 无法满足需求,应优先考虑使用 `SafeValue` 辅助方法封装,但必须声明理由。
## 常用资源键 (Common Resource Keys)
- `DesignCornerRadiusComponent` (最常用)
- `DesignCornerRadiusMicro`
- `DesignCornerRadiusSm`
- `DesignCornerRadiusMd`
- `DesignCornerRadiusLg`
- `DesignCornerRadiusXl`

62
docs/TYPOGRAPHY_SPEC.md Normal file
View File

@@ -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.

View File

@@ -25,6 +25,12 @@
- `glass-strong`:主要大容器 - `glass-strong`:主要大容器
- `glass-panel`:子区域、小面板、卡片 - `glass-panel`:子区域、小面板、卡片
### 形状与圆角 (Shape & Corner Radius)
- **全局统一**:所有 UI 元素的圆角必须遵循 [圆角设计规范](file:///c:/Users/USER154971/Documents/GitHub/LanMountainDesktop/docs/CORNER_RADIUS_SPEC.md)。
- **禁止硬编码**:严禁在资源库以外的地方硬编码 `CornerRadius` 数值。
- **动态适配**:桌面组件必须使用 `DesignCornerRadiusComponent` 动态资源,以支持用户在设置中全局切换“锐利/平衡/圆润/开放”风格。
### 可访问性 ### 可访问性
- 正文对比度目标不低于 `4.5:1` - 正文对比度目标不低于 `4.5:1`