mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
Compare commits
3 Commits
v0.8.2.1
...
e69bbf8b19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e69bbf8b19 | ||
|
|
d30af21317 | ||
|
|
8583465a67 |
@@ -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`
|
||||||
|
|
||||||
|
|||||||
82
CHANGELOG.md
Normal file
82
CHANGELOG.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# 更新日志 / Changelog
|
||||||
|
|
||||||
|
所有重要的更改都将记录在此文件中。
|
||||||
|
|
||||||
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||||
|
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 新增 (Added)
|
||||||
|
- 待发布的新功能
|
||||||
|
|
||||||
|
### 变更 (Changed)
|
||||||
|
- 待发布的变更
|
||||||
|
|
||||||
|
### 修复 (Fixed)
|
||||||
|
- 待发布的修复
|
||||||
|
|
||||||
|
### 移除 (Removed)
|
||||||
|
- 待发布的移除项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.8.3.1] - 2026-04-08
|
||||||
|
|
||||||
|
### 新增 (Added)
|
||||||
|
- ✨ **快捷方式组件**: 新增快捷方式组件,可在阑山桌面内便捷打开系统应用与文件
|
||||||
|
- 支持创建快捷方式,统一管理应用和文件
|
||||||
|
- 提供单击打开和双击打开两种交互模式
|
||||||
|
- 支持配置是否显示背景
|
||||||
|
- 📝 初始化更新日志文档,为后续版本发布建立基础
|
||||||
|
|
||||||
|
### 变更 (Changed)
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 修复 (Fixed)
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 移除 (Removed)
|
||||||
|
- 无
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本说明
|
||||||
|
|
||||||
|
### 版本号规则
|
||||||
|
|
||||||
|
本项目采用语义化版本号 `MAJOR.MINOR.PATCH.BUILD`:
|
||||||
|
|
||||||
|
- **MAJOR (主版本号)**: 不兼容的 API 修改
|
||||||
|
- **MINOR (次版本号)**: 向下兼容的功能性新增
|
||||||
|
- **PATCH (修订号)**: 向下兼容的问题修正
|
||||||
|
- **BUILD (构建号)**: 内部构建版本,用于区分同一 PATCH 版本的不同构建
|
||||||
|
|
||||||
|
### 分类说明
|
||||||
|
|
||||||
|
- **新增 (Added)**: 新功能、新特性
|
||||||
|
- **变更 (Changed)**: 对现有功能的变更
|
||||||
|
- **修复 (Fixed)**: Bug 修复
|
||||||
|
- **移除 (Removed)**: 移除的功能或特性
|
||||||
|
|
||||||
|
### 图例
|
||||||
|
|
||||||
|
- 🎉 **重大更新**: 重要功能或里程碑
|
||||||
|
- ✨ **新功能**: 新增功能特性
|
||||||
|
- 🐛 **Bug修复**: 问题修复
|
||||||
|
- 🔧 **配置**: 配置相关变更
|
||||||
|
- 📝 **文档**: 文档更新
|
||||||
|
- 🎨 **样式**: UI/UX 改进
|
||||||
|
- ♻️ **重构**: 代码重构
|
||||||
|
- ⚡ **性能**: 性能优化
|
||||||
|
- 🔒 **安全**: 安全相关
|
||||||
|
- 🌐 **国际化**: 国际化/本地化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/yourorg/LanMountainDesktop/compare/v0.8.3.1...HEAD
|
||||||
|
[0.8.3.1]: https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.1
|
||||||
@@ -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))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) &&
|
||||||
|
|||||||
@@ -46,4 +46,5 @@ public static class BuiltInComponentIds
|
|||||||
public const string DesktopZhiJiaoHub = "DesktopZhiJiaoHub";
|
public const string DesktopZhiJiaoHub = "DesktopZhiJiaoHub";
|
||||||
public const string DesktopFileManager = "DesktopFileManager";
|
public const string DesktopFileManager = "DesktopFileManager";
|
||||||
public const string DesktopNotificationBox = "DesktopNotificationBox";
|
public const string DesktopNotificationBox = "DesktopNotificationBox";
|
||||||
|
public const string DesktopShortcut = "DesktopShortcut";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,6 +420,16 @@ public sealed class ComponentRegistry
|
|||||||
MinHeightCells: 2,
|
MinHeightCells: 2,
|
||||||
AllowStatusBarPlacement: false,
|
AllowStatusBarPlacement: false,
|
||||||
AllowDesktopPlacement: true,
|
AllowDesktopPlacement: true,
|
||||||
|
ResizeMode: DesktopComponentResizeMode.Free),
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
BuiltInComponentIds.DesktopShortcut,
|
||||||
|
"快捷方式",
|
||||||
|
"App",
|
||||||
|
"Launcher",
|
||||||
|
MinWidthCells: 1,
|
||||||
|
MinHeightCells: 1,
|
||||||
|
AllowStatusBarPlacement: false,
|
||||||
|
AllowDesktopPlacement: true,
|
||||||
ResizeMode: DesktopComponentResizeMode.Free)
|
ResizeMode: DesktopComponentResizeMode.Free)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -123,6 +123,25 @@ public sealed class ComponentSettingsSnapshot
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Shortcut Component Settings (快捷方式组件设置)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快捷方式目标路径
|
||||||
|
/// </summary>
|
||||||
|
public string? ShortcutTargetPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 点击模式:Single(单击打开) 或 Double(双击打开)
|
||||||
|
/// </summary>
|
||||||
|
public string ShortcutClickMode { get; set; } = "Double";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否透明背景
|
||||||
|
/// </summary>
|
||||||
|
public bool ShortcutTransparentBackground { get; set; } = false;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public ComponentSettingsSnapshot Clone()
|
public ComponentSettingsSnapshot Clone()
|
||||||
{
|
{
|
||||||
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -272,7 +272,12 @@ public static class DesktopComponentEditorRegistryFactory
|
|||||||
BuiltInComponentIds.DesktopNotificationBox,
|
BuiltInComponentIds.DesktopNotificationBox,
|
||||||
context => new NotificationBoxComponentEditor(context),
|
context => new NotificationBoxComponentEditor(context),
|
||||||
preferredWidth: 480d,
|
preferredWidth: 480d,
|
||||||
preferredHeight: 520d)
|
preferredHeight: 520d),
|
||||||
|
[BuiltInComponentIds.DesktopShortcut] = new(
|
||||||
|
BuiltInComponentIds.DesktopShortcut,
|
||||||
|
context => new ShortcutComponentEditor(context),
|
||||||
|
preferredWidth: 420d,
|
||||||
|
preferredHeight: 400d)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var componentId in GetBuiltInDesktopComponentIds(componentRegistry))
|
foreach (var componentId in GetBuiltInDesktopComponentIds(componentRegistry))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||||
|
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="360"
|
||||||
|
d:DesignHeight="400"
|
||||||
|
x:Class="LanMountainDesktop.Views.ComponentEditors.ShortcutComponentEditor">
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="16" Margin="20">
|
||||||
|
|
||||||
|
<TextBlock x:Name="HeadlineTextBlock"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold" />
|
||||||
|
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Opacity="0.75"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<ui:SettingsExpander x:Name="TargetPathExpander">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="Folder" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<Button x:Name="BrowseButton"
|
||||||
|
Content="浏览..."
|
||||||
|
Click="OnBrowseClick" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<TextBox x:Name="TargetPathTextBox"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Watermark="未选择目标"
|
||||||
|
MinWidth="200" />
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<Button x:Name="ClearButton"
|
||||||
|
Content="清除"
|
||||||
|
Click="OnClearClick"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<ui:SettingsExpander>
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="CursorClick" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<RadioButton x:Name="SingleClickRadio"
|
||||||
|
Content="单击打开"
|
||||||
|
GroupName="ClickModeGroup" />
|
||||||
|
<RadioButton x:Name="DoubleClickRadio"
|
||||||
|
Content="双击打开"
|
||||||
|
GroupName="ClickModeGroup"
|
||||||
|
IsChecked="True" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<ui:SettingsExpander>
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="ColorBackground" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<ToggleSwitch x:Name="BackgroundToggle"
|
||||||
|
OnContent=""
|
||||||
|
OffContent=""
|
||||||
|
IsChecked="True" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
<ui:SettingsExpanderItem>
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock x:Name="BackgroundLabel" />
|
||||||
|
<TextBlock x:Name="BackgroundDescription"
|
||||||
|
Opacity="0.75"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:SettingsExpanderItem>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||||
|
|
||||||
|
public partial class ShortcutComponentEditor : ComponentEditorViewBase
|
||||||
|
{
|
||||||
|
private bool _suppressEvents;
|
||||||
|
|
||||||
|
public ShortcutComponentEditor()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShortcutComponentEditor(DesktopComponentEditorContext? context)
|
||||||
|
: base(context)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ApplyLocalizedText();
|
||||||
|
ApplyState();
|
||||||
|
AttachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalizedText()
|
||||||
|
{
|
||||||
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "快捷方式";
|
||||||
|
DescriptionTextBlock.Text = L(
|
||||||
|
"shortcut.settings.desc",
|
||||||
|
"配置快捷方式的目标路径和打开方式。");
|
||||||
|
|
||||||
|
BackgroundLabel.Text = L("shortcut.settings.show_background", "显示背景");
|
||||||
|
BackgroundDescription.Text = L(
|
||||||
|
"shortcut.settings.show_background.desc",
|
||||||
|
"关闭后组件背景将变为透明。");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyState()
|
||||||
|
{
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
var targetPath = snapshot.ShortcutTargetPath;
|
||||||
|
var clickMode = snapshot.ShortcutClickMode;
|
||||||
|
var transparentBackground = snapshot.ShortcutTransparentBackground;
|
||||||
|
|
||||||
|
_suppressEvents = true;
|
||||||
|
TargetPathTextBox.Text = targetPath ?? string.Empty;
|
||||||
|
SingleClickRadio.IsChecked = string.Equals(clickMode, "Single", StringComparison.OrdinalIgnoreCase);
|
||||||
|
DoubleClickRadio.IsChecked = !SingleClickRadio.IsChecked;
|
||||||
|
BackgroundToggle.IsChecked = !transparentBackground;
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachEventHandlers()
|
||||||
|
{
|
||||||
|
BackgroundToggle.IsCheckedChanged += OnBackgroundToggleChanged;
|
||||||
|
SingleClickRadio.IsCheckedChanged += OnClickModeChanged;
|
||||||
|
DoubleClickRadio.IsCheckedChanged += OnClickModeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnBrowseClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel?.StorageProvider is not { } storageProvider)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = L("shortcut.settings.picker_title", "选择目标文件或文件夹"),
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter =
|
||||||
|
[
|
||||||
|
new FilePickerFileType(L("shortcut.settings.picker_type.executable", "可执行文件"))
|
||||||
|
{
|
||||||
|
Patterns = ["*.exe", "*.lnk", "*.bat", "*.cmd"]
|
||||||
|
},
|
||||||
|
new FilePickerFileType(L("shortcut.settings.picker_type.all", "所有文件"))
|
||||||
|
{
|
||||||
|
Patterns = ["*.*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var files = await storageProvider.OpenFilePickerAsync(options);
|
||||||
|
var file = files.FirstOrDefault();
|
||||||
|
var localPath = file?.TryGetLocalPath();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(localPath))
|
||||||
|
{
|
||||||
|
var folderOptions = new FolderPickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = L("shortcut.settings.picker_title_folder", "选择目标文件夹"),
|
||||||
|
AllowMultiple = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var folders = await storageProvider.OpenFolderPickerAsync(folderOptions);
|
||||||
|
localPath = folders.FirstOrDefault()?.TryGetLocalPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(localPath))
|
||||||
|
{
|
||||||
|
TargetPathTextBox.Text = localPath;
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutTargetPath = localPath;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutTargetPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClearClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
TargetPathTextBox.Text = string.Empty;
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutTargetPath = null;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutTargetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClickModeChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clickMode = SingleClickRadio.IsChecked == true ? "Single" : "Double";
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutClickMode = clickMode;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutClickMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBackgroundToggleChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transparentBackground = BackgroundToggle.IsChecked != true;
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
snapshot.ShortcutTransparentBackground = transparentBackground;
|
||||||
|
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ShortcutTransparentBackground));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
@@ -483,7 +479,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
|||||||
new DesktopComponentRuntimeRegistration(
|
new DesktopComponentRuntimeRegistration(
|
||||||
BuiltInComponentIds.DesktopNotificationBox,
|
BuiltInComponentIds.DesktopNotificationBox,
|
||||||
"component.notification_box",
|
"component.notification_box",
|
||||||
() => new NotificationBoxWidget())
|
() => new NotificationBoxWidget()),
|
||||||
|
new DesktopComponentRuntimeRegistration(
|
||||||
|
BuiltInComponentIds.DesktopShortcut,
|
||||||
|
"component.shortcut",
|
||||||
|
() => new ShortcutWidget())
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
38
LanMountainDesktop/Views/Components/ShortcutWidget.axaml
Normal file
38
LanMountainDesktop/Views/Components/ShortcutWidget.axaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="96"
|
||||||
|
d:DesignHeight="96"
|
||||||
|
x:Class="LanMountainDesktop.Views.Components.ShortcutWidget">
|
||||||
|
|
||||||
|
<Border x:Name="RootBorder"
|
||||||
|
Classes="glass-panel"
|
||||||
|
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<Grid RowDefinitions="*,Auto"
|
||||||
|
x:Name="ContentGrid">
|
||||||
|
|
||||||
|
<Border x:Name="IconHost"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Image x:Name="IconImage"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock x:Name="NameTextBlock"
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
MaxLines="2"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="4,0,4,4"
|
||||||
|
FontSize="11" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
369
LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs
Normal file
369
LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using FluentIcons.Avalonia;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class ShortcutWidget : UserControl, IDesktopComponentWidget, IComponentPlacementContextAware, IDisposable
|
||||||
|
{
|
||||||
|
private string _componentId = BuiltInComponentIds.DesktopShortcut;
|
||||||
|
private string _placementId = string.Empty;
|
||||||
|
private string? _targetPath;
|
||||||
|
private string _clickMode = "Double";
|
||||||
|
private bool _transparentBackground;
|
||||||
|
private double _currentCellSize = 48;
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
private const double TapMovementThreshold = 10;
|
||||||
|
private const long TapTimeThresholdMs = 500;
|
||||||
|
|
||||||
|
private readonly Dictionary<int, PointerGestureState> _gestureStates = new();
|
||||||
|
|
||||||
|
private record PointerGestureState(
|
||||||
|
Point StartPosition,
|
||||||
|
long StartTime
|
||||||
|
);
|
||||||
|
|
||||||
|
public ShortcutWidget()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DoubleTapped += OnDoubleTapped;
|
||||||
|
UpdateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetComponentPlacementContext(string componentId, string? placementId)
|
||||||
|
{
|
||||||
|
_componentId = string.IsNullOrWhiteSpace(componentId)
|
||||||
|
? BuiltInComponentIds.DesktopShortcut
|
||||||
|
: componentId.Trim();
|
||||||
|
_placementId = placementId?.Trim() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplySettings(ComponentSettingsSnapshot snapshot)
|
||||||
|
{
|
||||||
|
_targetPath = snapshot.ShortcutTargetPath;
|
||||||
|
_clickMode = string.Equals(snapshot.ShortcutClickMode, "Single", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "Single"
|
||||||
|
: "Double";
|
||||||
|
_transparentBackground = snapshot.ShortcutTransparentBackground;
|
||||||
|
UpdateDisplay();
|
||||||
|
ApplyChrome();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyCellSize(double cellSize)
|
||||||
|
{
|
||||||
|
_currentCellSize = cellSize;
|
||||||
|
var iconSize = Math.Clamp(cellSize * 0.5, 24, 64);
|
||||||
|
IconImage.Width = iconSize;
|
||||||
|
IconImage.Height = iconSize;
|
||||||
|
|
||||||
|
var fontSize = Math.Clamp(cellSize * 0.18, 10, 16);
|
||||||
|
NameTextBlock.FontSize = fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDisplay()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
ShowEmptyState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var name = GetDisplayName(_targetPath);
|
||||||
|
NameTextBlock.Text = name;
|
||||||
|
NameTextBlock.Foreground = this.FindResource("AdaptiveTextPrimaryBrush") as IBrush ?? new SolidColorBrush(Colors.White);
|
||||||
|
|
||||||
|
LoadIcon(_targetPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ShowEmptyState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowEmptyState()
|
||||||
|
{
|
||||||
|
NameTextBlock.Text = "添加快捷方式";
|
||||||
|
NameTextBlock.Foreground = this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray);
|
||||||
|
|
||||||
|
var iconBrush = this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray);
|
||||||
|
IconImage.Source = null;
|
||||||
|
|
||||||
|
var iconHostContent = new SymbolIcon
|
||||||
|
{
|
||||||
|
Symbol = FluentIcons.Common.Symbol.Add,
|
||||||
|
FontSize = 32,
|
||||||
|
Foreground = iconBrush,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
IconHost.Child = iconHostContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDisplayName(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return "快捷方式";
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return Path.GetFileName(path.TrimEnd('\\', '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||||
|
return string.IsNullOrWhiteSpace(fileName) ? path : fileName;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadIcon(string path)
|
||||||
|
{
|
||||||
|
byte[]? pngBytes = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = WindowsIconService.TryGetSystemFolderIconPngBytes();
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = WindowsIconService.TryGetIconPngBytes(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = LinuxIconService.TryGetSystemFolderIconPngBytes();
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = LinuxIconService.TryGetIconPngBytes(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = MacIconService.TryGetSystemFolderIconPngBytes();
|
||||||
|
}
|
||||||
|
else if (File.Exists(path))
|
||||||
|
{
|
||||||
|
pngBytes = MacIconService.TryGetIconPngBytes(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
pngBytes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pngBytes is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(pngBytes);
|
||||||
|
IconImage.Source = new Bitmap(stream);
|
||||||
|
IconHost.Child = IconImage;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadFallbackIcon(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFallbackIcon(string path)
|
||||||
|
{
|
||||||
|
var symbol = Directory.Exists(path)
|
||||||
|
? FluentIcons.Common.Symbol.Folder
|
||||||
|
: FluentIcons.Common.Symbol.Document;
|
||||||
|
|
||||||
|
var iconBrush = this.FindResource("AdaptiveAccentBrush") as IBrush ?? new SolidColorBrush(Colors.DodgerBlue);
|
||||||
|
|
||||||
|
IconImage.Source = null;
|
||||||
|
var iconHostContent = new SymbolIcon
|
||||||
|
{
|
||||||
|
Symbol = symbol,
|
||||||
|
FontSize = 32,
|
||||||
|
Foreground = iconBrush,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||||
|
};
|
||||||
|
IconHost.Child = iconHostContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyChrome()
|
||||||
|
{
|
||||||
|
if (_transparentBackground)
|
||||||
|
{
|
||||||
|
RootBorder.Classes.Remove("glass-panel");
|
||||||
|
RootBorder.Background = Brushes.Transparent;
|
||||||
|
RootBorder.BorderBrush = Brushes.Transparent;
|
||||||
|
RootBorder.BorderThickness = new Thickness(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RootBorder.Classes.Contains("glass-panel"))
|
||||||
|
{
|
||||||
|
RootBorder.Classes.Add("glass-panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
RootBorder.ClearValue(Border.BackgroundProperty);
|
||||||
|
RootBorder.ClearValue(Border.BorderBrushProperty);
|
||||||
|
RootBorder.ClearValue(Border.BorderThicknessProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerPressed(e);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pointer = e.GetCurrentPoint(this);
|
||||||
|
var pointerId = e.Pointer.Id;
|
||||||
|
var position = pointer.Position;
|
||||||
|
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
_gestureStates[pointerId] = new PointerGestureState(position, timestamp);
|
||||||
|
e.Pointer.Capture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerMoved(PointerEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerMoved(e);
|
||||||
|
|
||||||
|
var pointerId = e.Pointer.Id;
|
||||||
|
if (!_gestureStates.TryGetValue(pointerId, out var state))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPoint = e.GetCurrentPoint(this);
|
||||||
|
var distance = Math.Sqrt(
|
||||||
|
Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) +
|
||||||
|
Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distance > TapMovementThreshold)
|
||||||
|
{
|
||||||
|
_gestureStates.Remove(pointerId);
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPointerReleased(e);
|
||||||
|
|
||||||
|
var pointerId = e.Pointer.Id;
|
||||||
|
if (!_gestureStates.Remove(pointerId, out var state))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
|
||||||
|
var currentPoint = e.GetCurrentPoint(this);
|
||||||
|
var distance = Math.Sqrt(
|
||||||
|
Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) +
|
||||||
|
Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - state.StartTime;
|
||||||
|
|
||||||
|
if (distance > TapMovementThreshold || elapsed > TapTimeThresholdMs)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_clickMode == "Single")
|
||||||
|
{
|
||||||
|
OpenTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDoubleTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_clickMode == "Double")
|
||||||
|
{
|
||||||
|
OpenTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenTarget()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_targetPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(_targetPath)
|
||||||
|
{
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
Process.Start("xdg-open", _targetPath);
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
Process.Start("open", _targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("ShortcutWidget", $"Failed to open target: {_targetPath}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
_gestureStates.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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