This commit is contained in:
lincube
2026-02-28 12:30:16 +08:00
parent 310c0f224f
commit 473a84e47b
13 changed files with 750 additions and 327 deletions

View File

@@ -0,0 +1,6 @@
namespace LanMontainDesktop.ComponentSystem;
public static class BuiltInComponentIds
{
public const string Clock = "Clock";
}

View File

@@ -0,0 +1,28 @@
using System;
namespace LanMontainDesktop.ComponentSystem;
public static class ComponentPlacementRules
{
public static (int WidthCells, int HeightCells) EnsureMinimumSize(
DesktopComponentDefinition definition,
int requestedWidthCells,
int requestedHeightCells)
{
var width = Math.Max(definition.MinWidthCells, requestedWidthCells);
var height = Math.Max(definition.MinHeightCells, requestedHeightCells);
return (Math.Max(1, width), Math.Max(1, height));
}
public static bool CanPlaceInStatusBar(DesktopComponentDefinition definition, int requestedHeightCells)
{
return definition.AllowStatusBarPlacement && requestedHeightCells == 1;
}
public static (int Column, int Row) ClampToGrid(int requestedColumn, int requestedRow, int maxColumns, int maxRows)
{
var clampedColumn = Math.Clamp(requestedColumn, 0, Math.Max(0, maxColumns - 1));
var clampedRow = Math.Clamp(requestedRow, 0, Math.Max(0, maxRows - 1));
return (clampedColumn, clampedRow);
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LanMontainDesktop.ComponentSystem.Extensions;
namespace LanMontainDesktop.ComponentSystem;
public sealed class ComponentRegistry
{
private readonly Dictionary<string, DesktopComponentDefinition> _definitions;
public ComponentRegistry(IEnumerable<DesktopComponentDefinition> definitions)
{
_definitions = definitions
.Where(d => !string.IsNullOrWhiteSpace(d.Id))
.GroupBy(d => d.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);
}
public static ComponentRegistry CreateDefault()
{
var builtIn = new[]
{
new DesktopComponentDefinition(
BuiltInComponentIds.Clock,
"Clock",
"Clock",
"Status",
MinWidthCells: 1,
MinHeightCells: 1,
AllowStatusBarPlacement: true,
AllowDesktopPlacement: true)
};
return new ComponentRegistry(builtIn);
}
public ComponentRegistry RegisterExtensions(IEnumerable<IComponentExtensionProvider> providers)
{
var merged = _definitions.Values.ToList();
foreach (var provider in providers)
{
var externalDefinitions = provider.GetComponents();
if (externalDefinitions is null)
{
continue;
}
merged.AddRange(externalDefinitions);
}
return new ComponentRegistry(merged);
}
public bool TryGetDefinition(string componentId, out DesktopComponentDefinition definition)
{
return _definitions.TryGetValue(componentId, out definition!);
}
public bool IsKnownComponent(string componentId)
{
return _definitions.ContainsKey(componentId);
}
public bool AllowsStatusBarPlacement(string componentId)
{
return _definitions.TryGetValue(componentId, out var definition) && definition.AllowStatusBarPlacement;
}
public IReadOnlyList<DesktopComponentDefinition> GetAll()
{
return _definitions.Values.OrderBy(d => d.Category).ThenBy(d => d.DisplayName).ToList();
}
}

View File

@@ -0,0 +1,11 @@
namespace LanMontainDesktop.ComponentSystem;
public sealed record DesktopComponentDefinition(
string Id,
string DisplayName,
string IconKey,
string Category,
int MinWidthCells,
int MinHeightCells,
bool AllowStatusBarPlacement,
bool AllowDesktopPlacement);

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace LanMontainDesktop.ComponentSystem.Extensions;
public interface IComponentExtensionProvider
{
IReadOnlyList<DesktopComponentDefinition> GetComponents();
}

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace LanMontainDesktop.ComponentSystem.Extensions;
public sealed class JsonComponentExtensionProvider : IComponentExtensionProvider
{
private readonly IReadOnlyList<DesktopComponentDefinition> _definitions;
private JsonComponentExtensionProvider(IReadOnlyList<DesktopComponentDefinition> definitions)
{
_definitions = definitions;
}
public IReadOnlyList<DesktopComponentDefinition> GetComponents()
{
return _definitions;
}
public static IReadOnlyList<IComponentExtensionProvider> LoadProvidersFromDirectory(string directoryPath)
{
if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath))
{
return Array.Empty<IComponentExtensionProvider>();
}
var providers = new List<IComponentExtensionProvider>();
foreach (var filePath in Directory.GetFiles(directoryPath, "*.json", SearchOption.TopDirectoryOnly))
{
var provider = TryLoadFromFile(filePath);
if (provider is not null)
{
providers.Add(provider);
}
}
return providers;
}
private static JsonComponentExtensionProvider? TryLoadFromFile(string filePath)
{
try
{
var json = File.ReadAllText(filePath);
var entries = JsonSerializer.Deserialize<List<ComponentExtensionEntry>>(json);
if (entries is null || entries.Count == 0)
{
return null;
}
var definitions = new List<DesktopComponentDefinition>();
foreach (var entry in entries)
{
if (string.IsNullOrWhiteSpace(entry.Id) ||
string.IsNullOrWhiteSpace(entry.DisplayName))
{
continue;
}
definitions.Add(new DesktopComponentDefinition(
entry.Id.Trim(),
entry.DisplayName.Trim(),
string.IsNullOrWhiteSpace(entry.IconKey) ? "PuzzlePiece" : entry.IconKey,
string.IsNullOrWhiteSpace(entry.Category) ? "Extensions" : entry.Category,
MinWidthCells: Math.Max(1, entry.MinWidthCells),
MinHeightCells: Math.Max(1, entry.MinHeightCells),
AllowStatusBarPlacement: entry.AllowStatusBarPlacement,
AllowDesktopPlacement: entry.AllowDesktopPlacement));
}
return definitions.Count == 0 ? null : new JsonComponentExtensionProvider(definitions);
}
catch
{
return null;
}
}
private sealed class ComponentExtensionEntry
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string IconKey { get; set; } = string.Empty;
public string Category { get; set; } = "Extensions";
public int MinWidthCells { get; set; } = 1;
public int MinHeightCells { get; set; } = 1;
public bool AllowStatusBarPlacement { get; set; }
public bool AllowDesktopPlacement { get; set; } = true;
}
}

View File

@@ -0,0 +1,77 @@
# 组件系统模块Component System Module
本目录提供组件系统的模块化基础,用于支持内置组件管理与第三方扩展接入。
This directory provides the modular foundation for built-in component management and third-party extension integration.
## 核心文件职责Core Files
- `BuiltInComponentIds.cs`:内置组件 ID 常量(例如 `Clock`)。
Built-in component ID constants (for example `Clock`).
- `DesktopComponentDefinition.cs`:组件元数据定义(名称、类别、最小尺寸、可放置区域等)。
Component metadata model (name, category, minimum size, placement permissions).
- `ComponentPlacementRules.cs`:组件放置规则(最小尺寸、状态栏高度限制、网格边界约束)。
Placement rules (minimum size, status-bar height rule, grid clamping).
- `ComponentRegistry.cs`:组件注册中心,负责内置组件与扩展组件合并。
Registry that merges built-in and extension components.
- `Extensions/IComponentExtensionProvider.cs`:扩展提供者接口契约。
Extension provider interface contract.
- `Extensions/JsonComponentExtensionProvider.cs`:基于 JSON 的扩展加载器。
JSON-based extension loader.
## 第三方扩展契约Extension Contract
- 第三方可通过实现 `IComponentExtensionProvider` 提供组件定义。
Third parties can provide component definitions via `IComponentExtensionProvider`.
- 当前内置了 JSON 提供者,运行时扫描目录:
Built-in JSON provider scans at runtime:
- `Extensions/Components/*.json`(相对应用输出目录)
`Extensions/Components/*.json` (relative to app output directory)
## 加载流程Load Flow
1. `ComponentRegistry.CreateDefault()` 先注册内置组件。
Register built-in components first via `ComponentRegistry.CreateDefault()`.
2. 调用 `.RegisterExtensions(...)` 合并扩展组件。
Merge extension components via `.RegisterExtensions(...)`.
3. 主窗口通过注册中心校验组件合法性与放置权限。
Main window validates component identity and placement permission through the registry.
## JSON 清单格式Manifest Schema
JSON 文件为数组,每一项代表一个组件定义。
The JSON file is an array, where each item represents one component definition.
```json
[
{
"id": "Weather",
"displayName": "Weather",
"iconKey": "WeatherSunny",
"category": "Status",
"minWidthCells": 1,
"minHeightCells": 1,
"allowStatusBarPlacement": true,
"allowDesktopPlacement": true
}
]
```
字段说明Field notes
- `id`:组件唯一 ID建议英文、稳定不变
Unique component ID (prefer stable English key).
- `displayName`:显示名。
Display name.
- `iconKey`:图标键(由上层 UI 解释)。
Icon key resolved by UI layer.
- `category`:组件分类。
Component category.
- `minWidthCells` / `minHeightCells`:最小占格,必须满足 `>= 1`
Minimum cell size, must satisfy `>= 1`.
- `allowStatusBarPlacement`:是否允许放到顶部状态栏。
Whether placing in top status bar is allowed.
- `allowDesktopPlacement`:是否允许放到桌面区域。
Whether placing in desktop area is allowed.
## 放置规则摘要Placement Rules Summary
- 最小尺寸约束:`minWidthCells >= 1``minHeightCells >= 1`
Minimum size constraint: `minWidthCells >= 1` and `minHeightCells >= 1`.
- 状态栏约束:状态栏组件高度必须为 `1` 格。
Status bar constraint: component height must be exactly `1` cell.
- 越界约束所有组件坐标会被网格边界钳制clamp
Out-of-bounds constraint: component coordinates are clamped to grid bounds.