From 35976c3f3df0320014bf3ec6c2d32b13cd6b0213 Mon Sep 17 00:00:00 2001 From: lincube Date: Fri, 3 Apr 2026 01:17:47 +0800 Subject: [PATCH] =?UTF-8?q?fead.=E5=81=9A=E6=A1=8C=E9=9D=A2=E7=BB=84?= =?UTF-8?q?=E4=BB=B6ing=EF=BC=8C=E6=99=BA=E6=95=99hub=E5=8A=A0=E4=BA=86rin?= =?UTF-8?q?shub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LanMountainDesktop/App.axaml.cs | 8 +- .../Models/ComponentSettingsSnapshot.cs | 2 + .../Services/IRecommendationDataService.cs | 2 + .../Services/RecommendationDataService.cs | 1 + .../Services/WindowPassthroughService.cs | 41 +++- .../ZhiJiaoHubComponentEditor.axaml | 3 + .../ZhiJiaoHubComponentEditor.axaml.cs | 4 +- .../FusedDesktopComponentLibraryControl.axaml | 129 +++++++++-- ...sedDesktopComponentLibraryControl.axaml.cs | 214 +++++++++++++----- .../FusedDesktopComponentLibraryWindow.axaml | 84 ++++--- ...usedDesktopComponentLibraryWindow.axaml.cs | 53 ++++- .../Views/TransparentOverlayWindow.axaml.cs | 151 +++++++++++- 12 files changed, 559 insertions(+), 133 deletions(-) diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index cdbba1c..84396e4 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -227,7 +227,7 @@ public partial class App : Application return; } - // 确保透明覆盖层窗口存在 + // 确保透明覆盖层窗口存在并显示 EnsureTransparentOverlayWindow(); // 打开融合桌面组件库窗口 @@ -235,6 +235,12 @@ public partial class App : Application { try { + // 确保覆盖层窗口已显示(组件要渲染在上面,必须先 Show) + if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible) + { + _transparentOverlayWindow.Show(); + } + var window = new FusedDesktopComponentLibraryWindow(); if (_transparentOverlayWindow is not null) diff --git a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs index e91102c..570790a 100644 --- a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs @@ -124,12 +124,14 @@ public static class ZhiJiaoHubSources { public const string ClassIsland = "classisland"; public const string Sectl = "sectl"; + public const string RinLit = "rinlit"; public static string Normalize(string? value) { return value?.ToLowerInvariant() switch { "sectl" => Sectl, + "rinlit" => RinLit, _ => ClassIsland }; } diff --git a/LanMountainDesktop/Services/IRecommendationDataService.cs b/LanMountainDesktop/Services/IRecommendationDataService.cs index 7ef61f6..e0822fa 100644 --- a/LanMountainDesktop/Services/IRecommendationDataService.cs +++ b/LanMountainDesktop/Services/IRecommendationDataService.cs @@ -322,6 +322,8 @@ public sealed record RecommendationApiOptions public string ClassIslandHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/ClassIsland/classisland-hub/main/images/{0}"; public string SectlHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/SECTL/SECTL-hub/main/images/{0}"; + + public string RinLitHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/RinLit-233-shiroko/Rin-sHub/main/images/{0}"; } public interface IRecommendationInfoService diff --git a/LanMountainDesktop/Services/RecommendationDataService.cs b/LanMountainDesktop/Services/RecommendationDataService.cs index 78b0270..5e3252e 100644 --- a/LanMountainDesktop/Services/RecommendationDataService.cs +++ b/LanMountainDesktop/Services/RecommendationDataService.cs @@ -3247,6 +3247,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis var (owner, repo, path) = source switch { ZhiJiaoHubSources.Sectl => ("SECTL", "SECTL-hub", "docs/.vuepress/public/images"), + ZhiJiaoHubSources.RinLit => ("RinLit-233-shiroko", "Rin-sHub", "images"), _ => ("ClassIsland", "classisland-hub", "images") }; diff --git a/LanMountainDesktop/Services/WindowPassthroughService.cs b/LanMountainDesktop/Services/WindowPassthroughService.cs index 5fff130..cbac9c1 100644 --- a/LanMountainDesktop/Services/WindowPassthroughService.cs +++ b/LanMountainDesktop/Services/WindowPassthroughService.cs @@ -100,6 +100,9 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService private static readonly Dictionary _bottomMostWindows = new(); private static readonly Dictionary _originalWndProcs = new(); private static readonly Dictionary> _interactiveRegions = new(); + + // 记录每个窗口的屏幕原点(窗口左上角的屏幕坐标),用于将 WM_NCHITTEST 屏幕坐标转成窗口相对坐标 + private static readonly Dictionary _windowScreenOrigins = new(); private static readonly object _staticLock = new(); public bool IsBottomMostSupported => true; @@ -121,11 +124,12 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService // 设置为桌面子窗口 SetAsDesktopChild(handle); - // 注册置底状态 + // 注册置底状态 & 记录窗口屏幕原点 lock (_staticLock) { _bottomMostWindows[handle] = true; _interactiveRegions[handle] = []; + UpdateWindowScreenOrigin(handle); } // 注入消息钩子 @@ -147,6 +151,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService _bottomMostWindows.Remove(handle); _originalWndProcs.Remove(handle); _interactiveRegions.Remove(handle); + _windowScreenOrigins.Remove(handle); } } }; @@ -220,15 +225,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService // 处理 WM_NCHITTEST - 区域级穿透 if (msg == WM_NCHITTEST) { - // 从 lParam 解析坐标(低字为 X,高字为 Y) - var x = (short)(wParam.ToInt32() & 0xFFFF); - var y = (short)((wParam.ToInt32() >> 16) & 0xFFFF); - var point = new Point(x, y); + // WM_NCHITTEST 的鼠标坐标在 lParam(低16位=X,高16位=Y),且为屏幕坐标 + var screenX = (short)(lParam.ToInt64() & 0xFFFF); + var screenY = (short)((lParam.ToInt64() >> 16) & 0xFFFF); lock (_staticLock) { - if (_interactiveRegions.TryGetValue(hWnd, out var regions)) + if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0) { + // 将屏幕坐标转为窗口相对坐标(_interactiveRegions 存的是窗口内坐标) + _windowScreenOrigins.TryGetValue(hWnd, out var origin); + var clientX = screenX - origin.X; + var clientY = screenY - origin.Y; + var point = new Point(clientX, clientY); + foreach (var region in regions) { if (region.Contains(point)) @@ -265,9 +275,28 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService lock (_staticLock) { _interactiveRegions[handle] = regions; + // 同步刷新屏幕原点(DPI 缩放可能影响坐标,每次更新区域时一并刷新) + UpdateWindowScreenOrigin(handle); } } + /// + /// 更新指定窗口的屏幕左上角坐标缓存(用于将 WM_NCHITTEST 屏幕坐标转为窗口相对坐标) + /// + private static void UpdateWindowScreenOrigin(IntPtr handle) + { + if (GetWindowRect(handle, out var rect)) + { + _windowScreenOrigins[handle] = new Point(rect.Left, rect.Top); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct RECT { public int Left, Top, Right, Bottom; } + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + private delegate IntPtr WndProcDelegate(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] diff --git a/LanMountainDesktop/Views/ComponentEditors/ZhiJiaoHubComponentEditor.axaml b/LanMountainDesktop/Views/ComponentEditors/ZhiJiaoHubComponentEditor.axaml index 146a9cb..acb11f8 100644 --- a/LanMountainDesktop/Views/ComponentEditors/ZhiJiaoHubComponentEditor.axaml +++ b/LanMountainDesktop/Views/ComponentEditors/ZhiJiaoHubComponentEditor.axaml @@ -21,6 +21,9 @@ + SectlItem, + ZhiJiaoHubSources.RinLit => RinLitItem, _ => ClassIslandItem }; diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml index de6e051..f6ab62c 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml @@ -1,30 +1,113 @@ - - - - - - - - + xmlns:fi="using:FluentIcons.Avalonia" + xmlns:local="using:LanMountainDesktop.Views" + x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"> + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs index 2a46d01..fdaa61c 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Input; @@ -7,72 +10,167 @@ using Avalonia.Layout; using Avalonia.Media; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.Views.Components; +using LanMountainDesktop.Models; namespace LanMountainDesktop.Views; -/// -/// 融合桌面组件库控件 - 专门用于添加组件到系统桌面(负一屏) -/// public partial class FusedDesktopComponentLibraryControl : UserControl { - /// - /// 添加组件到融合桌面事件 - /// public event EventHandler? AddComponentRequested; + + private readonly ObservableCollection _categories = new(); + private readonly ObservableCollection _components = new(); + private List _allDefinitions = new(); + private ComponentRegistry? _componentRegistry; + private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry; + private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); + private readonly IWeatherInfoService _weatherDataService; + private readonly TimeZoneService _timeZoneService; + private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService(); + private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService(); + public FusedDesktopComponentLibraryControl() { InitializeComponent(); - LoadComponents(); - } - - /// - /// 加载可用组件列表 - /// - private void LoadComponents() - { - var registry = ComponentRegistry.CreateDefault(); + _weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); + _timeZoneService = _settingsFacade.Region.GetTimeZoneService(); - foreach (var definition in registry.GetAll()) + CategoryListBox.ItemsSource = _categories; + ComponentItemsControl.ItemsSource = _components; + + LoadRegistry(); + LoadCategories(); + SearchBox.KeyUp += (s, e) => FilterComponents(); + + // 默认选择第一个分类 + if (_categories.Count > 0) { - if (!definition.AllowDesktopPlacement) - { - continue; - } - - var button = new Button - { - Width = 100, - Height = 100, - Margin = new Thickness(4), - Padding = new Thickness(8), - CornerRadius = new CornerRadius(12), - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - Tag = definition.Id - }; - - var textBlock = new TextBlock - { - Text = definition.DisplayName, - FontSize = 11, - TextAlignment = TextAlignment.Center, - TextTrimming = TextTrimming.CharacterEllipsis, - MaxLines = 2, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - - button.Content = textBlock; - button.Click += OnAddComponentClick; - - ComponentPanel.Children.Add(button); + CategoryListBox.SelectedIndex = 0; } } - - /// - /// 添加组件按钮点击 - /// + + private void LoadRegistry() + { + var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService; + _componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService); + _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( + _componentRegistry, + pluginRuntimeService, + _settingsFacade); + + _allDefinitions = _componentRegistry.GetAll() + .Where(d => d.AllowDesktopPlacement) + .ToList(); + } + + private void LoadCategories() + { + _categories.Clear(); + _categories.Add(new LibraryCategoryItem("all", "全部组件", "Apps")); + + var categoryMap = new Dictionary + { + { "clock", ("时钟", "Clock") }, + { "date", ("日历", "Calendar") }, + { "weather", ("天气", "WeatherCloudy") }, + { "info", ("资讯", "News") }, + { "calculator", ("工具", "Calculator") }, + { "study", ("学习", "Book") }, + { "file", ("文件", "Document") } + }; + + var usedCategories = _allDefinitions + .Select(d => d.Category) + .Distinct() + .Where(c => !string.IsNullOrEmpty(c)); + + foreach (var cat in usedCategories) + { + if (categoryMap.TryGetValue(cat.ToLower(), out var info)) + { + _categories.Add(new LibraryCategoryItem(cat, info.Display, info.Icon)); + } + else + { + _categories.Add(new LibraryCategoryItem(cat, cat, "Cube")); + } + } + } + + private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e) + { + FilterComponents(); + } + + private void FilterComponents() + { + var selectedCategory = (CategoryListBox.SelectedItem as LibraryCategoryItem)?.Id; + var searchText = SearchBox.Text?.ToLower() ?? ""; + + var filtered = _allDefinitions.Where(d => + { + var matchesCategory = selectedCategory == "all" || string.Equals(d.Category, selectedCategory, StringComparison.OrdinalIgnoreCase); + var matchesSearch = string.IsNullOrEmpty(searchText) || d.DisplayName.ToLower().Contains(searchText) || d.Id.ToLower().Contains(searchText); + return matchesCategory && matchesSearch; + }); + + _components.Clear(); + foreach (var def in filtered) + { + _components.Add(new LibraryComponentItem + { + Id = def.Id, + DisplayName = def.DisplayName, + Description = GetDescription(def.Id), + PreviewContent = CreatePreview(def.Id) + }); + } + } + + private string GetDescription(string id) + { + // 简单映射描述信息 + return id.Contains("clock") ? "实时显示当前时间与日期。" : + id.Contains("weather") ? "为您提供精准的天气预报。" : + "多功能桌面组件,提升您的操作效率。"; + } + + private Control? CreatePreview(string id) + { + if (_componentRuntimeRegistry == null || !_componentRuntimeRegistry.TryGetDescriptor(id, out var descriptor)) + { + return null; + } + + try + { + var control = descriptor.CreateControl( + 100, // Previews assume 100px base + _timeZoneService, + _weatherDataService, + _recommendationInfoService, + _calculatorDataService, + _settingsFacade, + "preview_" + id); + + control.IsHitTestVisible = false; + + return new Viewbox + { + Child = control, + Stretch = Stretch.Uniform, + Margin = new Thickness(12) + }; + } + catch (Exception ex) + { + return new TextBlock { Text = "无法预览", VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center }; + } + } + private void OnAddComponentClick(object? sender, RoutedEventArgs e) { if (sender is Button button && button.Tag is string componentId) @@ -81,3 +179,15 @@ public partial class FusedDesktopComponentLibraryControl : UserControl } } } + +public record LibraryCategoryItem(string Id, string DisplayName, string Icon); + +public class LibraryComponentItem +{ + public string Id { get; set; } = ""; + public string DisplayName { get; set; } = ""; + public string Description { get; set; } = ""; + public Control? PreviewContent { get; set; } + public bool HasPreview => PreviewContent != null; +} + diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml index 1325811..77980b9 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml @@ -1,41 +1,57 @@ - - - - - - - - - - - + SystemDecorations="Full" + ExtendClientAreaToDecorationsHint="True" + ExtendClientAreaChromeHints="NoChrome" + ExtendClientAreaTitleBarHeightHint="-1" + Background="Transparent" + TransparencyLevelHint="Mica" + Title="融合桌面组件库"> + + + + - - - + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs index 631f8a7..dd88982 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs @@ -1,7 +1,9 @@ using System; using Avalonia.Controls; using Avalonia.Interactivity; +using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Views; @@ -13,8 +15,12 @@ namespace LanMountainDesktop.Views; public partial class FusedDesktopComponentLibraryWindow : Window { private readonly IFusedDesktopLayoutService _layoutService = FusedDesktopLayoutServiceProvider.GetOrCreate(); + private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); private TransparentOverlayWindow? _overlayWindow; + // 与 TransparentOverlayWindow 保持一致的默认 cellSize + private const double DefaultCellSize = 100; + public FusedDesktopComponentLibraryWindow() { InitializeComponent(); @@ -31,7 +37,7 @@ public partial class FusedDesktopComponentLibraryWindow : Window } /// - /// 添加组件请求处理 + /// 添加组件请求处理 - 将组件放置在屏幕(覆盖层画布)中央 /// private void OnAddComponentRequested(object? sender, string componentId) { @@ -41,19 +47,52 @@ public partial class FusedDesktopComponentLibraryWindow : Window return; } - // 在屏幕中央添加组件 - var screenBounds = _overlayWindow.Bounds; - var x = screenBounds.Width / 2 - 100; // 居中 - var y = screenBounds.Height / 2 - 100; + // 计算组件的像素尺寸 + var (componentWidth, componentHeight) = ResolveComponentSize(componentId); - _overlayWindow.AddComponent(componentId, x, y, 200, 200); + // 取覆盖层画布的中心点,减去组件半尺寸,使组件出现在屏幕正中央 + var overlayBounds = _overlayWindow.Bounds; + var centerX = overlayBounds.Width / 2.0 - componentWidth / 2.0; + var centerY = overlayBounds.Height / 2.0 - componentHeight / 2.0; - AppLogger.Info("FusedDesktopLibrary", $"Added component {componentId} to fused desktop."); + // 边界保护:确保组件不超出屏幕边界 + centerX = Math.Max(0, Math.Min(centerX, overlayBounds.Width - componentWidth)); + centerY = Math.Max(0, Math.Min(centerY, overlayBounds.Height - componentHeight)); + + _overlayWindow.AddComponent(componentId, centerX, centerY, componentWidth, componentHeight); + + AppLogger.Info("FusedDesktopLibrary", + $"Added component '{componentId}' at center ({centerX:F0}, {centerY:F0}) size ({componentWidth}x{componentHeight})."); // 关闭窗口 Close(); } + /// + /// 解析组件的默认像素尺寸(基于组件定义的 MinCells * DefaultCellSize) + /// + private (double Width, double Height) ResolveComponentSize(string componentId) + { + try + { + var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService; + var registry = DesktopComponentRegistryFactory.Create(pluginRuntimeService); + if (registry.TryGetDefinition(componentId, out var definition)) + { + var w = Math.Max(1, definition.MinWidthCells) * DefaultCellSize; + var h = Math.Max(1, definition.MinHeightCells) * DefaultCellSize; + return (w, h); + } + } + catch (Exception ex) + { + AppLogger.Warn("FusedDesktopLibrary", $"Failed to resolve component size for '{componentId}'.", ex); + } + + // 回退为 2×2 格子的默认尺寸 + return (DefaultCellSize * 2, DefaultCellSize * 2); + } + private void OnCloseClick(object? sender, RoutedEventArgs e) { Close(); diff --git a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs index b53646a..730bc1b 100644 --- a/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs +++ b/LanMountainDesktop/Views/TransparentOverlayWindow.axaml.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; @@ -10,6 +11,8 @@ using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views; @@ -41,6 +44,17 @@ public partial class TransparentOverlayWindow : Window private readonly Dictionary _componentHosts = []; private readonly List _interactiveRegions = []; private FusedDesktopLayoutSnapshot _layout = new(); + private ComponentRegistry? _componentRegistry; + private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry; + + // 基础服务 + private readonly IWeatherInfoService _weatherDataService; + private readonly TimeZoneService _timeZoneService; + private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService(); + private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService(); + + // 渲染参数 + private const double DefaultCellSize = 100; // 拖拽状态 private bool _isDragging; @@ -53,6 +67,8 @@ public partial class TransparentOverlayWindow : Window public TransparentOverlayWindow() { InitializeComponent(); + _weatherDataService = _settingsFacade.Weather.GetWeatherInfoService(); + _timeZoneService = _settingsFacade.Region.GetTimeZoneService(); // 仅在 Windows 上启用置底功能 if (OperatingSystem.IsWindows()) @@ -70,12 +86,54 @@ public partial class TransparentOverlayWindow : Window _bottomMostService.SendToBottom(this); } - // 加载布局 + // 确保注册表已初始化 + EnsureRegistries(); + + // 加载布局并渲染 _layout = _layoutService.Load(); + RenderAllComponents(); - // TODO: 渲染组件(需要从 MainWindow 获取组件注册表) + AppLogger.Info("TransparentOverlay", $"Opened with {_layout.ComponentPlacements.Count} components."); + } + + /// + /// 确保组件运行时注册表已初始化 + /// + private void EnsureRegistries() + { + if (_componentRuntimeRegistry is not null) return; - AppLogger.Info("TransparentOverlay", "Transparent overlay window opened."); + var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService; + _componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService); + _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( + _componentRegistry, + pluginRuntimeService, + _settingsFacade); + } + + /// + /// 渲染所有布局中的组件 + /// + private void RenderAllComponents() + { + if (Content is not Canvas canvas) return; + + canvas.Children.Clear(); + _componentHosts.Clear(); + + foreach (var placement in _layout.ComponentPlacements) + { + try + { + RenderComponentInternal(placement); + } + catch (Exception ex) + { + AppLogger.Warn("TransparentOverlay", $"Failed to render component {placement.ComponentId}", ex); + } + } + + UpdateInteractiveRegions(); } protected override void OnClosed(EventArgs e) @@ -112,8 +170,20 @@ public partial class TransparentOverlayWindow : Window /// /// 添加组件(供外部调用) /// - public void AddComponent(string componentId, double x, double y, double width = 200, double height = 200) + public void AddComponent(string componentId, double x, double y, double? width = null, double? height = null) { + EnsureRegistries(); + + if (_componentRegistry == null || !_componentRegistry.TryGetDefinition(componentId, out var definition)) + { + AppLogger.Warn("TransparentOverlay", $"Cannot add unknown component: {componentId}"); + return; + } + + // 解析尺寸:如果未提供,则使用组件定义的最小尺寸 * 100 + var finalWidth = width ?? (definition.MinWidthCells * DefaultCellSize); + var finalHeight = height ?? (definition.MinHeightCells * DefaultCellSize); + var placementId = Guid.NewGuid().ToString("N"); var placement = new FusedDesktopComponentPlacementSnapshot { @@ -121,16 +191,49 @@ public partial class TransparentOverlayWindow : Window ComponentId = componentId, X = x, Y = y, - Width = width, - Height = height, + Width = finalWidth, + Height = finalHeight, ZIndex = _layout.ComponentPlacements.Count }; _layout.ComponentPlacements.Add(placement); - UpdateInteractiveRegions(); - SaveLayout(); - AppLogger.Info("TransparentOverlay", $"Added component: {componentId} at ({x}, {y})"); + // 立即渲染 + try + { + RenderComponentInternal(placement); + UpdateInteractiveRegions(); + SaveLayout(); + AppLogger.Info("TransparentOverlay", $"Added component: {componentId} at ({x}, {y}) size ({finalWidth}x{finalHeight})"); + } + catch (Exception ex) + { + AppLogger.Warn("TransparentOverlay", $"Failed to add component {componentId}", ex); + _layout.ComponentPlacements.Remove(placement); + } + } + + /// + /// 内部渲染单个组件 + /// + private void RenderComponentInternal(FusedDesktopComponentPlacementSnapshot placement) + { + if (_componentRuntimeRegistry is null || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor)) + { + AppLogger.Warn("TransparentOverlay", $"Unknown component: {placement.ComponentId}"); + return; + } + + var control = descriptor.CreateControl( + DefaultCellSize, + _timeZoneService, + _weatherDataService, + _recommendationInfoService, + _calculatorDataService, + _settingsFacade, + placement.PlacementId); + + RenderComponent(placement.PlacementId, control, placement.X, placement.Y, placement.Width, placement.Height); } /// @@ -176,6 +279,9 @@ public partial class TransparentOverlayWindow : Window host.PointerMoved += OnComponentPointerMoved; host.PointerReleased += OnComponentPointerReleased; + // 右键上下文菜单(删除组件) + host.ContextRequested += OnComponentContextRequested; + if (Content is Canvas canvas) { canvas.Children.Add(host); @@ -185,6 +291,33 @@ public partial class TransparentOverlayWindow : Window UpdateInteractiveRegions(); } + // 组件右键上下文菜单(删除) + private void OnComponentContextRequested(object? sender, ContextRequestedEventArgs e) + { + if (sender is not Border host || host.Tag is not string placementId) return; + + // 构建上下文菜单 + var deleteItem = new MenuItem + { + Header = "移除组件", + Icon = new Avalonia.Controls.TextBlock { Text = "🗑" } + }; + deleteItem.Click += (_, _) => + { + RemoveComponent(placementId); + AppLogger.Info("TransparentOverlay", $"Component removed via context menu: {placementId}"); + }; + + var menu = new ContextMenu + { + Items = { deleteItem } + }; + + // 显示在当前控件上 + menu.Open(host); + e.Handled = true; + } + // 组件拖拽处理 private void OnComponentPointerPressed(object? sender, PointerPressedEventArgs e) {