mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 15:44:25 +08:00
fead.圆角,终于统一
This commit is contained in:
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
GlobalAppearanceSettings.CornerRadiusStyleSharp => new AppearanceCornerRadiusTokens(
|
||||||
Radius(14, normalizedScale),
|
Micro: new CornerRadius(4),
|
||||||
Radius(20, normalizedScale),
|
Xs: new CornerRadius(8),
|
||||||
Radius(28, normalizedScale),
|
Sm: new CornerRadius(10),
|
||||||
Radius(32, normalizedScale),
|
Md: new CornerRadius(14),
|
||||||
Radius(36, normalizedScale),
|
Lg: new CornerRadius(20),
|
||||||
Radius(18, normalizedScale));
|
Xl: new CornerRadius(24),
|
||||||
}
|
Island: new CornerRadius(28),
|
||||||
|
Component: new CornerRadius(20)),
|
||||||
private static CornerRadius Radius(double value, double scale)
|
GlobalAppearanceSettings.CornerRadiusStyleRounded => new AppearanceCornerRadiusTokens(
|
||||||
{
|
Micro: new CornerRadius(8),
|
||||||
var scaled = Math.Round(value * scale * 2, MidpointRounding.AwayFromZero) / 2d;
|
Xs: new CornerRadius(14),
|
||||||
return new CornerRadius(scaled);
|
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))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
71
LanMountainDesktop.Tests/CornerRadiusStyleTests.cs
Normal file
71
LanMountainDesktop.Tests/CornerRadiusStyleTests.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) &&
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,59 @@
|
|||||||
# 圆角设计规范
|
# 圆角设计规范 (LanMountain Desktop Corner Radius Spec)
|
||||||
|
|
||||||
## 中文
|
## 核心理念 (Core Philosophy)
|
||||||
|
|
||||||
本规范用于统一阑山桌面不同层级容器和控件的圆角尺度。
|
为了确保桌面组件在不同尺寸、缩放比例下都能保持视觉一致性和美感,阑山桌面采用了 **固定圆角风格预设 (Fixed Corner Radius Styles)**,全面参考小米澎湃OS (Xiaomi HyperOS) 的设计语言。
|
||||||
|
|
||||||
### 基础层级
|
所有的组件和容器必须使用统一的资源键,禁止在 XAML 或代码中使用硬编码的像素值。
|
||||||
|
|
||||||
- Level 1:12px,小元素和图标容器
|
## 预设风格 (Preset Styles)
|
||||||
- Level 2:16px,小型色块和紧凑控件
|
|
||||||
- Level 3:20px,普通按钮
|
|
||||||
- Level 4:24px,输入面板和小型容器
|
|
||||||
- Component:18px,桌面组件的标准圆角(默认值)
|
|
||||||
- Level 5:28px,普通玻璃面板
|
|
||||||
- Level 6:32px,强化容器
|
|
||||||
- Level 7:36px,大容器、窗口、任务栏
|
|
||||||
|
|
||||||
### 使用建议
|
用户可以在设置中选择以下四种风格之一。系统会自动根据选中的风格动态映射全局圆角 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
62
docs/TYPOGRAPHY_SPEC.md
Normal 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.
|
||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user