using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using FluentIcons.Avalonia; using FluentIcons.Common; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.DesktopEditing; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Settings.Core; using LanMountainDesktop.Theme; using LanMountainDesktop.Views.Components; using PathShape = Avalonia.Controls.Shapes.Path; using Symbol = FluentIcons.Common.Symbol; using SymbolIcon = FluentIcons.Avalonia.SymbolIcon; namespace LanMountainDesktop.Views; public partial class MainWindow { private readonly List _desktopComponentPlacements = []; private readonly Dictionary _desktopPageComponentGrids = new(); private const string DesktopComponentClass = "desktop-component"; private const string DesktopComponentHostClass = "desktop-component-host"; private const string DesktopComponentContentHostTag = "desktop-component-content-host"; private const string DesktopComponentResizeHandleTag = "desktop-component-resize-handle"; private string? _componentLibraryActiveCategoryId; private int _componentLibraryCategoryIndex; private int _componentLibraryComponentIndex; private double _componentLibraryCategoryPageWidth; private double _componentLibraryComponentPageWidth; private TranslateTransform? _componentLibraryCategoryHostTransform; private TranslateTransform? _componentLibraryComponentHostTransform; private IReadOnlyList _componentLibraryCategories = Array.Empty(); private IReadOnlyList _componentLibraryActiveComponents = Array.Empty(); private bool _isComponentLibraryCategoryGestureActive; private bool _isComponentLibraryComponentGestureActive; private Point _componentLibraryCategoryGestureStartPoint; private Point _componentLibraryCategoryGestureCurrentPoint; private double _componentLibraryCategoryGestureBaseOffset; private Point _componentLibraryComponentGestureStartPoint; private Point _componentLibraryComponentGestureCurrentPoint; private double _componentLibraryComponentGestureBaseOffset; private sealed record ComponentLibraryCategory( string Id, Symbol Icon, string Title, IReadOnlyList Components); private readonly record struct ComponentScaleRule(int WidthUnit, int HeightUnit, int MinScale); private readonly record struct TaskbarProfilePopupMaterialPalette( Color SurfaceColor, Color OutlineColor, Color AvatarSurfaceColor, Color PrimaryTextColor, Color AccentColor, Color HoverColor, Color PressedColor, Color DividerColor); private void InitializeTaskbarProfileFlyout() { if (TaskbarProfileButton is null || TaskbarProfilePopup is null) { return; } TaskbarProfilePopup.PlacementTarget = TaskbarProfileButton; RefreshTaskbarProfilePresentation(); } private void RefreshTaskbarProfilePresentation() { if (TaskbarProfileButton is null) { return; } var profile = _currentUserProfileService.GetCurrentProfile(); ApplyProfileAvatarVisual(TaskbarProfileAvatarImage, TaskbarProfileAvatarFallbackText, profile); ApplyProfileAvatarVisual(TaskbarProfileHeaderAvatarImage, TaskbarProfileHeaderAvatarFallbackText, profile); TaskbarProfileDisplayNameTextBlock.Text = profile.DisplayName; TaskbarProfileSettingsActionTextBlock.Text = L("tooltip.open_settings", "Settings"); TaskbarProfileDesktopEditActionTextBlock.Text = L("button.component_library", "Edit Desktop"); ApplyTaskbarProfilePopupTheme(_appearanceThemeService.GetCurrent()); ToolTip.SetTip(TaskbarProfileButton, profile.DisplayName); } private static void ApplyProfileAvatarVisual(Image? image, TextBlock? fallbackText, CurrentUserProfileSnapshot profile) { if (image is not null) { image.Source = profile.AvatarBitmap; image.IsVisible = profile.AvatarBitmap is not null; } if (fallbackText is not null) { fallbackText.Text = profile.FallbackMonogram; fallbackText.IsVisible = profile.AvatarBitmap is null; } } private void ApplyTaskbarProfilePopupTheme(AppearanceThemeSnapshot snapshot) { if (TaskbarProfilePopupPanel is null) { return; } var palette = BuildTaskbarProfilePopupMaterialPalette(snapshot); SetTaskbarProfilePopupBrush("TaskbarProfilePopupSurfaceBrush", palette.SurfaceColor); SetTaskbarProfilePopupBrush("TaskbarProfilePopupOutlineBrush", palette.OutlineColor); SetTaskbarProfilePopupBrush("TaskbarProfilePopupAvatarSurfaceBrush", palette.AvatarSurfaceColor); SetTaskbarProfilePopupBrush("TaskbarProfilePopupTextBrush", palette.PrimaryTextColor); SetTaskbarProfilePopupBrush("TaskbarProfilePopupAccentBrush", palette.AccentColor); SetTaskbarProfilePopupBrush("TaskbarProfilePopupActionHoverBrush", palette.HoverColor); SetTaskbarProfilePopupBrush("TaskbarProfilePopupActionPressedBrush", palette.PressedColor); SetTaskbarProfilePopupBrush("TaskbarProfilePopupDividerBrush", palette.DividerColor); } private void SetTaskbarProfilePopupBrush(string resourceKey, Color color) { TaskbarProfilePopupPanel.Resources[resourceKey] = new SolidColorBrush(color); } private static TaskbarProfilePopupMaterialPalette BuildTaskbarProfilePopupMaterialPalette(AppearanceThemeSnapshot snapshot) { var primary = snapshot.MonetPalette.Primary.A > 0 ? snapshot.MonetPalette.Primary : snapshot.AccentColor; if (primary == default) { primary = Color.Parse("#FF6750A4"); } var neutral = snapshot.MonetPalette.Neutral.A > 0 ? snapshot.MonetPalette.Neutral : snapshot.IsNightMode ? Color.Parse("#FF1A1F27") : Color.Parse("#FFF7F9FD"); var neutralVariant = snapshot.MonetPalette.NeutralVariant.A > 0 ? snapshot.MonetPalette.NeutralVariant : ColorMath.Blend(neutral, primary, snapshot.IsNightMode ? 0.20 : 0.10); var surfaceBase = snapshot.IsNightMode ? Color.Parse("#FF141A22") : Color.Parse("#FFFCFCFF"); var surface = ColorMath.Blend(surfaceBase, neutral, snapshot.IsNightMode ? 0.52 : 0.46); surface = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.12 : 0.05); var outlineSeed = snapshot.IsNightMode ? ColorMath.Blend(neutralVariant, Color.Parse("#FFFFFFFF"), 0.28) : ColorMath.Blend(neutralVariant, Color.Parse("#FF111827"), 0.12); var outline = Color.FromArgb( snapshot.IsNightMode ? (byte)0x82 : (byte)0x38, outlineSeed.R, outlineSeed.G, outlineSeed.B); var primaryTextPreferred = snapshot.IsNightMode ? Color.Parse("#FFF4F7FB") : Color.Parse("#FF14171B"); var primaryText = ColorMath.EnsureContrast(primaryTextPreferred, surface, 7.0); var accent = ColorMath.EnsureContrast(primary, surface, 3.0); var avatarSurface = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.26 : 0.16); var hover = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.20 : 0.10); var pressed = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.30 : 0.18); var divider = Color.FromArgb( snapshot.IsNightMode ? (byte)0x44 : (byte)0x20, outlineSeed.R, outlineSeed.G, outlineSeed.B); return new TaskbarProfilePopupMaterialPalette( surface, outline, avatarSurface, primaryText, accent, hover, pressed, divider); } private void OnTaskbarProfileButtonClick(object? sender, RoutedEventArgs e) { _ = sender; _ = e; if (TaskbarProfileButton is null || TaskbarProfilePopup is null) { return; } if (TaskbarProfilePopup.IsOpen) { TaskbarProfilePopup.IsOpen = false; return; } RefreshTaskbarProfilePresentation(); TaskbarProfilePopup.IsOpen = true; } private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e) { _ = sender; _ = e; if (TaskbarProfilePopup is not null) { TaskbarProfilePopup.IsOpen = false; } ExecuteTaskbarDesktopEditAction(); } private void OnOpenSettingsClick(object? sender, RoutedEventArgs e) { _ = sender; _ = e; if (TaskbarProfilePopup is not null) { TaskbarProfilePopup.IsOpen = false; } ExecuteTaskbarSettingsAction(); } private void ExecuteTaskbarDesktopEditAction() { if (_isComponentLibraryOpen) { CloseComponentLibraryWindow(reopenSettings: false); return; } var settingsWindowService = (Application.Current as App)?.SettingsWindowService; _reopenSettingsAfterComponentLibraryClose = settingsWindowService?.IsOpen == true; if (_reopenSettingsAfterComponentLibraryClose) { settingsWindowService?.Close(); } OpenComponentLibraryWindow(); } private void ExecuteTaskbarSettingsAction() { if (_isComponentLibraryOpen) { CloseComponentLibraryWindow(reopenSettings: false); } var app = Application.Current as App; if (app?.SettingsWindowService is { } settingsWindowService) { settingsWindowService.Toggle(new SettingsWindowOpenRequest( Source: "MainWindowTaskbar", Owner: this)); return; } app?.OpenIndependentSettingsModule("MainWindowTaskbar"); } private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e) { _componentLibraryWindowService.Close(this); } private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e) { _ = sender; _ = e; } private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e) { if (_suppressStatusBarToggleEvents) { return; } _topStatusComponentIds.Add(BuiltInComponentIds.Clock); ApplyTopStatusComponentVisibility(); PersistSettings(); } private void OnStatusBarClockUnchecked(object? sender, RoutedEventArgs e) { if (_suppressStatusBarToggleEvents) { return; } _topStatusComponentIds.Remove(BuiltInComponentIds.Clock); ApplyTopStatusComponentVisibility(); PersistSettings(); } private void ApplyTaskbarSettings(AppSettingsSnapshot snapshot) { _topStatusComponentIds.Clear(); if (snapshot.TopStatusComponentIds is not null) { foreach (var componentId in snapshot.TopStatusComponentIds) { if (string.IsNullOrWhiteSpace(componentId)) { continue; } var normalizedId = componentId.Trim(); if (_componentRegistry.IsKnownComponent(normalizedId) && _componentRegistry.AllowsStatusBarPlacement(normalizedId)) { _topStatusComponentIds.Add(normalizedId); } } } _pinnedTaskbarActions.Clear(); if (snapshot.PinnedTaskbarActions is not null) { foreach (var actionText in snapshot.PinnedTaskbarActions) { if (Enum.TryParse(actionText, ignoreCase: true, out var action)) { _pinnedTaskbarActions.Add(action); } } } if (_pinnedTaskbarActions.Count == 0) { foreach (var action in DefaultPinnedTaskbarActions) { _pinnedTaskbarActions.Add(action); } } _enableDynamicTaskbarActions = snapshot.EnableDynamicTaskbarActions; _taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode) ? TaskbarLayoutBottomFullRowMacStyle : snapshot.TaskbarLayoutMode; _clockDisplayFormat = snapshot.ClockDisplayFormat == "HourMinute" ? ClockDisplayFormat.HourMinute : ClockDisplayFormat.HourMinuteSecond; _statusBarClockTransparentBackground = snapshot.StatusBarClockTransparentBackground; _clockPosition = NormalizeClockPosition(snapshot.ClockPosition); _showTextCapsule = snapshot.ShowTextCapsule; _textCapsuleContent = snapshot.TextCapsuleContent ?? "**Hello** World!"; _textCapsulePosition = NormalizeTextCapsulePosition(snapshot.TextCapsulePosition); _textCapsuleTransparentBackground = snapshot.TextCapsuleTransparentBackground; ApplyClockSettingsToAllWidgets(); ApplyTextCapsuleSettingsToAllWidgets(); } private void ApplyClockSettingsToAllWidgets() { if (ClockWidgetLeft is not null) { ClockWidgetLeft.SetDisplayFormat(_clockDisplayFormat); ClockWidgetLeft.SetTransparentBackground(_statusBarClockTransparentBackground); } if (ClockWidgetCenter is not null) { ClockWidgetCenter.SetDisplayFormat(_clockDisplayFormat); ClockWidgetCenter.SetTransparentBackground(_statusBarClockTransparentBackground); } if (ClockWidgetRight is not null) { ClockWidgetRight.SetDisplayFormat(_clockDisplayFormat); ClockWidgetRight.SetTransparentBackground(_statusBarClockTransparentBackground); } } private static string NormalizeClockPosition(string? value) { return value switch { _ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center", _ when string.Equals(value, "Right", StringComparison.OrdinalIgnoreCase) => "Right", _ => "Left" }; } private void ApplyTextCapsuleSettingsToAllWidgets() { if (TextCapsuleWidgetLeft is not null) { TextCapsuleWidgetLeft.SetText(_textCapsuleContent); TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground); } if (TextCapsuleWidgetCenter is not null) { TextCapsuleWidgetCenter.SetText(_textCapsuleContent); TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground); } if (TextCapsuleWidgetRight is not null) { TextCapsuleWidgetRight.SetText(_textCapsuleContent); TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground); } } private static string NormalizeTextCapsulePosition(string? value) { return value switch { _ when string.Equals(value, "Center", StringComparison.OrdinalIgnoreCase) => "Center", _ when string.Equals(value, "Left", StringComparison.OrdinalIgnoreCase) => "Left", _ => "Right" }; } /// /// 检测状态栏组件是否会发生碰撞 /// private bool WouldComponentsCollide() { if (TopStatusBarHost is null) return false; // 获取各区域当前占用的宽度 var leftWidth = GetLeftPanelOccupiedWidth(); var centerWidth = GetCenterPanelOccupiedWidth(); var rightWidth = GetRightPanelOccupiedWidth(); // 获取状态栏总宽度 var totalWidth = TopStatusBarHost.Bounds.Width; if (totalWidth <= 0) return false; // 计算中间区域的实际位置 // 左列是 *, 中列是 Auto, 右列是 * // 中间区域居中显示 var centerLeft = (totalWidth - centerWidth) / 2; var centerRight = centerLeft + centerWidth; // 安全间距(像素) const double safetyMargin = 20; // 检测左侧组件是否会与中间区域碰撞 // 左侧组件右边界 = leftWidth // 中间区域左边界 = centerLeft if (leftWidth + safetyMargin > centerLeft) { return true; } // 检测右侧组件是否会与中间区域碰撞 // 右侧组件左边界 = totalWidth - rightWidth // 中间区域右边界 = centerRight if (totalWidth - rightWidth - safetyMargin < centerRight) { return true; } // 检测中间区域是否会与左右两侧碰撞(中间区域过宽) if (centerLeft < leftWidth + safetyMargin || centerRight > totalWidth - rightWidth - safetyMargin) { return true; } return false; } /// /// 获取左侧面板占用的宽度(包括间距) /// private double GetLeftPanelOccupiedWidth() { if (TopStatusLeftPanel is null) return 0; var spacing = TopStatusLeftPanel.Spacing; var width = 0.0; var visibleCount = 0; foreach (var child in TopStatusLeftPanel.Children) { if (child is Control control && control.IsVisible) { width += control.Bounds.Width; visibleCount++; } } // 添加间距 if (visibleCount > 1) { width += spacing * (visibleCount - 1); } return width; } /// /// 获取中间面板占用的宽度(包括间距) /// private double GetCenterPanelOccupiedWidth() { if (TopStatusCenterPanel is null) return 0; var spacing = TopStatusCenterPanel.Spacing; var width = 0.0; var visibleCount = 0; foreach (var child in TopStatusCenterPanel.Children) { if (child is Control control && control.IsVisible) { width += control.Bounds.Width; visibleCount++; } } // 添加间距 if (visibleCount > 1) { width += spacing * (visibleCount - 1); } return width; } /// /// 获取右侧面板占用的宽度(包括间距) /// private double GetRightPanelOccupiedWidth() { if (TopStatusRightPanel is null) return 0; var spacing = TopStatusRightPanel.Spacing; var width = 0.0; var visibleCount = 0; foreach (var child in TopStatusRightPanel.Children) { if (child is Control control && control.IsVisible) { width += control.Bounds.Width; visibleCount++; } } // 添加间距 if (visibleCount > 1) { width += spacing * (visibleCount - 1); } return width; } /// /// 检查是否可以在指定位置添加组件 /// private bool CanAddComponentAtPosition(string position) { // 先临时显示组件以计算宽度 var wouldCollide = WouldComponentsCollide(); if (!wouldCollide) return true; // 如果会发生碰撞,检查是否是因为目标位置导致的 // 获取当前各区域宽度 var leftWidth = GetLeftPanelOccupiedWidth(); var centerWidth = GetCenterPanelOccupiedWidth(); var rightWidth = GetRightPanelOccupiedWidth(); // 估算新组件的宽度(基于当前单元格大小) var estimatedNewComponentWidth = _currentDesktopCellSize > 0 ? _currentDesktopCellSize * 2 : 120; // 根据目标位置检查添加后是否会碰撞 return position switch { "Left" => CanAddToLeft(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth), "Center" => CanAddToCenter(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth), "Right" => CanAddToRight(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth), _ => false }; } private bool CanAddToLeft(double leftWidth, double centerWidth, double rightWidth, double newWidth) { if (TopStatusBarHost is null) return false; var totalWidth = TopStatusBarHost.Bounds.Width; if (totalWidth <= 0) return true; var newLeftWidth = leftWidth + newWidth + TopStatusLeftPanel?.Spacing ?? 6; var centerLeft = (totalWidth - centerWidth) / 2; const double safetyMargin = 20; return newLeftWidth + safetyMargin <= centerLeft; } private bool CanAddToCenter(double leftWidth, double centerWidth, double rightWidth, double newWidth) { if (TopStatusBarHost is null) return false; var totalWidth = TopStatusBarHost.Bounds.Width; if (totalWidth <= 0) return true; var newCenterWidth = centerWidth + newWidth + TopStatusCenterPanel?.Spacing ?? 6; var centerLeft = (totalWidth - newCenterWidth) / 2; var centerRight = centerLeft + newCenterWidth; const double safetyMargin = 20; return centerLeft >= leftWidth + safetyMargin && centerRight <= totalWidth - rightWidth - safetyMargin; } private bool CanAddToRight(double leftWidth, double centerWidth, double rightWidth, double newWidth) { if (TopStatusBarHost is null) return false; var totalWidth = TopStatusBarHost.Bounds.Width; if (totalWidth <= 0) return true; var newRightWidth = rightWidth + newWidth + TopStatusRightPanel?.Spacing ?? 6; var centerRight = (totalWidth + centerWidth) / 2; const double safetyMargin = 20; return totalWidth - newRightWidth - safetyMargin >= centerRight; } private void ApplyTopStatusComponentVisibility() { var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock); var hasVisibleTopStatusComponent = false; // 先隐藏所有时钟控件 if (ClockWidgetLeft is not null) ClockWidgetLeft.IsVisible = false; if (ClockWidgetCenter is not null) ClockWidgetCenter.IsVisible = false; if (ClockWidgetRight is not null) ClockWidgetRight.IsVisible = false; // 先隐藏所有文字胶囊控件 if (TextCapsuleWidgetLeft is not null) TextCapsuleWidgetLeft.IsVisible = false; if (TextCapsuleWidgetCenter is not null) TextCapsuleWidgetCenter.IsVisible = false; if (TextCapsuleWidgetRight is not null) TextCapsuleWidgetRight.IsVisible = false; // 根据位置设置显示对应的时钟控件(带碰撞检测) if (showClock) { var targetPosition = _clockPosition; var canAdd = CanAddComponentAtPosition(targetPosition); if (canAdd) { var targetClock = targetPosition switch { "Center" => ClockWidgetCenter, "Right" => ClockWidgetRight, _ => ClockWidgetLeft }; if (targetClock is not null) { targetClock.IsVisible = true; targetClock.SetTransparentBackground(_statusBarClockTransparentBackground); targetClock.SetDisplayFormat(_clockDisplayFormat); hasVisibleTopStatusComponent = true; } } else { // 如果目标位置无法添加,尝试其他位置 var alternativePosition = FindAlternativePosition(targetPosition); if (alternativePosition is not null) { var targetClock = alternativePosition switch { "Center" => ClockWidgetCenter, "Right" => ClockWidgetRight, _ => ClockWidgetLeft }; if (targetClock is not null) { targetClock.IsVisible = true; targetClock.SetTransparentBackground(_statusBarClockTransparentBackground); targetClock.SetDisplayFormat(_clockDisplayFormat); hasVisibleTopStatusComponent = true; } } } } // 根据位置设置显示对应的文字胶囊控件(带碰撞检测) if (_showTextCapsule) { var targetPosition = _textCapsulePosition; var canAdd = CanAddComponentAtPosition(targetPosition); if (canAdd) { var targetTextCapsule = targetPosition switch { "Left" => TextCapsuleWidgetLeft, "Center" => TextCapsuleWidgetCenter, _ => TextCapsuleWidgetRight }; if (targetTextCapsule is not null) { targetTextCapsule.IsVisible = true; targetTextCapsule.SetTransparentBackground(_textCapsuleTransparentBackground); targetTextCapsule.SetText(_textCapsuleContent); hasVisibleTopStatusComponent = true; } } else { // 如果目标位置无法添加,尝试其他位置 var alternativePosition = FindAlternativePosition(targetPosition); if (alternativePosition is not null) { var targetTextCapsule = alternativePosition switch { "Left" => TextCapsuleWidgetLeft, "Center" => TextCapsuleWidgetCenter, _ => TextCapsuleWidgetRight }; if (targetTextCapsule is not null) { targetTextCapsule.IsVisible = true; targetTextCapsule.SetTransparentBackground(_textCapsuleTransparentBackground); targetTextCapsule.SetText(_textCapsuleContent); hasVisibleTopStatusComponent = true; } } } } if (TopStatusBarHost is not null) { TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent; } // 延迟检查碰撞并调整 Dispatcher.UIThread.Post(async () => { await System.Threading.Tasks.Task.Delay(50); AdjustComponentsIfColliding(); }); } /// /// 当组件发生碰撞时,自动调整位置 /// private void AdjustComponentsIfColliding() { if (!WouldComponentsCollide()) return; // 获取当前可见的组件 var leftComponents = GetVisibleLeftComponents(); var centerComponents = GetVisibleCenterComponents(); var rightComponents = GetVisibleRightComponents(); // 优先保留时钟,调整文字胶囊位置 if (TextCapsuleWidgetLeft?.IsVisible == true && WouldComponentsCollide()) { // 尝试将左侧文字胶囊移到中间 if (CanAddComponentAtPosition("Center")) { TextCapsuleWidgetLeft.IsVisible = false; TextCapsuleWidgetCenter!.IsVisible = true; TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground); TextCapsuleWidgetCenter.SetText(_textCapsuleContent); } // 或者移到右侧 else if (CanAddComponentAtPosition("Right")) { TextCapsuleWidgetLeft.IsVisible = false; TextCapsuleWidgetRight!.IsVisible = true; TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground); TextCapsuleWidgetRight.SetText(_textCapsuleContent); } // 如果都无法添加,则隐藏文字胶囊 else { TextCapsuleWidgetLeft.IsVisible = false; } } if (TextCapsuleWidgetRight?.IsVisible == true && WouldComponentsCollide()) { // 尝试将右侧文字胶囊移到中间 if (CanAddComponentAtPosition("Center")) { TextCapsuleWidgetRight.IsVisible = false; TextCapsuleWidgetCenter!.IsVisible = true; TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground); TextCapsuleWidgetCenter.SetText(_textCapsuleContent); } // 或者移到左侧 else if (CanAddComponentAtPosition("Left")) { TextCapsuleWidgetRight.IsVisible = false; TextCapsuleWidgetLeft!.IsVisible = true; TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground); TextCapsuleWidgetLeft.SetText(_textCapsuleContent); } // 如果都无法添加,则隐藏文字胶囊 else { TextCapsuleWidgetRight.IsVisible = false; } } if (TextCapsuleWidgetCenter?.IsVisible == true && WouldComponentsCollide()) { // 尝试将中间文字胶囊移到左侧 if (CanAddComponentAtPosition("Left")) { TextCapsuleWidgetCenter.IsVisible = false; TextCapsuleWidgetLeft!.IsVisible = true; TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground); TextCapsuleWidgetLeft.SetText(_textCapsuleContent); } // 或者移到右侧 else if (CanAddComponentAtPosition("Right")) { TextCapsuleWidgetCenter.IsVisible = false; TextCapsuleWidgetRight!.IsVisible = true; TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground); TextCapsuleWidgetRight.SetText(_textCapsuleContent); } // 如果都无法添加,则隐藏文字胶囊 else { TextCapsuleWidgetCenter.IsVisible = false; } } } /// /// 查找可用的替代位置 /// private string? FindAlternativePosition(string originalPosition) { // 尝试所有可能的位置 var positions = new[] { "Left", "Center", "Right" }; foreach (var position in positions) { if (position != originalPosition && CanAddComponentAtPosition(position)) { return position; } } return null; } /// /// 获取左侧可见组件列表 /// private List GetVisibleLeftComponents() { var result = new List(); if (TopStatusLeftPanel is null) return result; foreach (var child in TopStatusLeftPanel.Children) { if (child is Control control && control.IsVisible) result.Add(control); } return result; } /// /// 获取中间可见组件列表 /// private List GetVisibleCenterComponents() { var result = new List(); if (TopStatusCenterPanel is null) return result; foreach (var child in TopStatusCenterPanel.Children) { if (child is Control control && control.IsVisible) result.Add(control); } return result; } /// /// 获取右侧可见组件列表 /// private List GetVisibleRightComponents() { var result = new List(); if (TopStatusRightPanel is null) return result; foreach (var child in TopStatusRightPanel.Children) { if (child is Control control && control.IsVisible) result.Add(control); } return result; } private TaskbarContext GetCurrentTaskbarContext() { return TaskbarContext.Desktop; } private void ApplyTaskbarActionVisibility(TaskbarContext context) { if (BackToWindowsButton is null || TaskbarProfileButton is null) { return; } var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows); BackToWindowsButton.IsVisible = showMinimize; TaskbarProfileButton.IsVisible = true; if (TaskbarFixedActionsHost is not null) { TaskbarFixedActionsHost.IsVisible = showMinimize; } if (TaskbarSettingsActionHost is not null) { TaskbarSettingsActionHost.IsVisible = true; } UpdateOpenSettingsActionVisualState(); var dynamicActions = ResolveDynamicTaskbarActions(context) .Where(action => action.IsVisible) .ToList(); var hasDynamicActions = dynamicActions.Count > 0; BuildDynamicTaskbarVisuals(dynamicActions, _currentDesktopCellSize); if (TaskbarDynamicActionsHost is not null) { TaskbarDynamicActionsHost.IsVisible = hasDynamicActions; } } private void UpdateOpenSettingsActionVisualState() { var effectiveCellSize = _currentDesktopCellSize > 0 ? _currentDesktopCellSize : Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells)); RefreshTaskbarProfilePresentation(); ApplyWidgetSizing(effectiveCellSize); } private void OpenComponentLibraryWindow() { if (ComponentLibraryWindow is null) { return; } _isComponentLibraryOpen = true; UpdateDesktopComponentHostEditState(); ShowComponentLibraryCategoryView(); RestoreComponentLibraryAfterDesktopEdit(); ComponentLibraryWindow.IsVisible = true; ComponentLibraryWindow.Opacity = 0; ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); RestoreComponentLibraryWindowPosition(); Dispatcher.UIThread.Post(() => { if (!_isComponentLibraryOpen || ComponentLibraryWindow is null) { return; } BuildComponentLibraryCategoryPages(); ComponentLibraryWindow.Opacity = 1; SyncComponentLibraryCollapseExpandedState(); }, DispatcherPriority.Background); } private void CloseComponentLibraryWindow(bool reopenSettings) { if (!_isComponentLibraryOpen || ComponentLibraryWindow is null) { return; } RestoreComponentLibraryAfterDesktopEdit(); _isComponentLibraryOpen = false; CancelDesktopComponentDrag(); CancelDesktopComponentResize(restoreOriginalSpan: true); ClearDesktopComponentSelection(); ClearSelectedLauncherTile(refreshTaskbar: false); UpdateDesktopComponentHostEditState(); ComponentLibraryWindow.Opacity = 0; ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); DispatcherTimer.RunOnce(() => { if (_isComponentLibraryOpen || ComponentLibraryWindow is null) { return; } ComponentLibraryWindow.IsVisible = false; ClearComponentLibraryPreviewControls(); var shouldReopenSettings = reopenSettings && _reopenSettingsAfterComponentLibraryClose; _reopenSettingsAfterComponentLibraryClose = false; if (shouldReopenSettings) { (Application.Current as App)?.OpenIndependentSettingsModule("ComponentLibraryClose"); } }, FluttermotionToken.Slow); } private void OpenDetachedComponentLibraryWindow() { _detachedComponentLibraryWindow ??= CreateDetachedComponentLibraryWindow(); _detachedComponentLibraryWindow.Reload(); if (!_detachedComponentLibraryWindow.IsVisible) { if (IsVisible) { _detachedComponentLibraryWindow.Show(this); } else { _detachedComponentLibraryWindow.Show(); } return; } _detachedComponentLibraryWindow.Activate(); } private void CloseDetachedComponentLibraryWindow() { if (_detachedComponentLibraryWindow is null) { return; } _detachedComponentLibraryWindow.Hide(); } private ComponentLibraryWindow CreateDetachedComponentLibraryWindow() { var window = new ComponentLibraryWindow( _componentLibraryService, cellSize => { var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); return new ComponentLibraryCreateContext( cellSize, appearanceSnapshot.GlobalCornerRadiusScale, _timeZoneService, _weatherDataService, _recommendationInfoService, _calculatorDataService, _settingsFacade); }, L, previewKeyResolver: ResolveDetachedLibraryPreviewKey, previewEntryResolver: ResolveDetachedLibraryPreviewEntry, warmPreviewRequested: RequestDetachedLibraryPreviewWarm, renderPreviewRequested: RequestDetachedLibraryPreviewRender); window.AddComponentRequested += OnDetachedComponentLibraryAddComponentRequested; window.Closed += OnDetachedComponentLibraryClosed; return window; } private void OnDetachedComponentLibraryAddComponentRequested(object? sender, string componentId) { _ = sender; if (string.IsNullOrWhiteSpace(componentId) || _currentDesktopSurfaceIndex < 0 || _currentDesktopSurfaceIndex >= _desktopPageCount || !_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid) || !_componentRuntimeRegistry.TryGetDescriptor(componentId, out var descriptor)) { return; } var span = NormalizeComponentCellSpan( componentId, ComponentPlacementRules.EnsureMinimumSize( descriptor.Definition, descriptor.Definition.MinWidthCells, descriptor.Definition.MinHeightCells)); var row = Math.Max(0, (pageGrid.RowDefinitions.Count - span.HeightCells) / 2); var column = Math.Max(0, (pageGrid.ColumnDefinitions.Count - span.WidthCells) / 2); PlaceDesktopComponentOnPage(componentId, _currentDesktopSurfaceIndex, row, column); } private void OnDetachedComponentLibraryClosed(object? sender, EventArgs e) { _ = e; if (ReferenceEquals(sender, _detachedComponentLibraryWindow)) { _detachedComponentLibraryWindow.AddComponentRequested -= OnDetachedComponentLibraryAddComponentRequested; _detachedComponentLibraryWindow.Closed -= OnDetachedComponentLibraryClosed; _detachedComponentLibraryWindow = null; } } private static DesktopComponentPlacementSnapshot ClonePlacementSnapshot(DesktopComponentPlacementSnapshot placement) { return new DesktopComponentPlacementSnapshot { PlacementId = placement.PlacementId, PageIndex = placement.PageIndex, ComponentId = placement.ComponentId, Row = placement.Row, Column = placement.Column, WidthCells = placement.WidthCells, HeightCells = placement.HeightCells }; } private void OnSettingsWindowStateChanged(object? sender, EventArgs e) { _ = sender; _ = e; SyncSettingsWindowState(); } private void SyncSettingsWindowState() { var isOpen = (Application.Current as App)?.SettingsWindowService?.IsOpen == true; _isSettingsOpen = isOpen; UpdateDesktopPageAwareComponentContext(); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); UpdateOpenSettingsActionVisualState(); } private void InitializeDesktopComponentDragHandlers() { // Global handlers: we capture the pointer during drag, then track move/release anywhere. AddHandler(PointerMovedEvent, OnDesktopComponentDragPointerMoved, RoutingStrategies.Tunnel); AddHandler(PointerReleasedEvent, OnDesktopComponentDragPointerReleased, RoutingStrategies.Tunnel); AddHandler(PointerCaptureLostEvent, OnDesktopComponentDragPointerCaptureLost, RoutingStrategies.Tunnel); } private IReadOnlyList ResolveDynamicTaskbarActions(TaskbarContext context) { if (context == TaskbarContext.Desktop && _isComponentLibraryOpen) { var actions = new List(); var isLauncherSurface = _currentDesktopSurfaceIndex == LauncherSurfaceIndex; if (isLauncherSurface && IsLauncherTileSelected()) { actions.Add(new TaskbarActionItem( TaskbarActionId.HideLauncherEntry, L("launcher.action.hide", "Hide"), "Hide", IsVisible: true, CommandKey: "launcher.hide")); return actions; } if (_selectedDesktopComponentHost is not null) { if (TryGetSelectedDesktopPlacement(out var selectedPlacement) && _componentEditorRegistry.TryGetDescriptor(selectedPlacement.ComponentId, out _)) { actions.Add(new TaskbarActionItem( TaskbarActionId.EditComponent, L("component.edit", "Edit"), "Edit", IsVisible: true, CommandKey: "component.edit")); } actions.Add(new TaskbarActionItem( TaskbarActionId.DeleteComponent, L("component.delete", "Delete"), "Delete", IsVisible: true, CommandKey: "component.delete")); return actions; } var canAddPage = _desktopPageCount < MaxDesktopPageCount; var canDeletePage = _desktopPageCount > MinDesktopPageCount; if (canAddPage) { actions.Add(new TaskbarActionItem( TaskbarActionId.AddDesktopPage, L("desktop.add_page", "Add page"), "Add", IsVisible: true, CommandKey: "desktop.add_page")); } if (canDeletePage) { actions.Add(new TaskbarActionItem( TaskbarActionId.DeleteDesktopPage, L("desktop.delete_page", "Delete page"), "Delete", IsVisible: true, CommandKey: "desktop.delete_page")); } return actions; } if (!_enableDynamicTaskbarActions) { return Array.Empty(); } _ = context; return Array.Empty(); } private void BuildDynamicTaskbarVisuals(IReadOnlyList actions, double cellSize) { if (TaskbarDynamicActionsPanel is not null) { TaskbarDynamicActionsPanel.Children.Clear(); } if (actions.Count == 0 || TaskbarDynamicActionsPanel is null) { return; } // Match taskbar typographic scale to the current grid cell size. var taskbarCellHeight = Math.Clamp(cellSize * 0.76, 36, 76); var fontSize = Math.Clamp(taskbarCellHeight * 0.36, 11, 22); var iconSize = Math.Clamp(taskbarCellHeight * 0.44, 12, 26); var padding = Math.Clamp(taskbarCellHeight * 0.20, 6, 14); var cornerRadius = Math.Clamp(taskbarCellHeight * 0.32, 8, 16); var spacing = Math.Clamp(taskbarCellHeight * 0.18, 4, 10); var pageCountText = $"{_currentDesktopSurfaceIndex + 1}/{_desktopPageCount}"; var pageCountBlock = new TextBlock { Text = pageCountText, Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"), FontSize = fontSize, FontWeight = FontWeight.SemiBold, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, Margin = new Thickness(0, 0, spacing, 0) }; var pageCountContainer = new Border { Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"), CornerRadius = new CornerRadius(cornerRadius), Padding = new Thickness(padding), Child = pageCountBlock, Margin = new Thickness(0, 0, spacing, 0) }; TaskbarDynamicActionsPanel.Children.Add(pageCountContainer); foreach (var action in actions) { if (!action.IsVisible) { continue; } var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage || action.Id == TaskbarActionId.DeleteComponent; var isHideAction = action.Id == TaskbarActionId.HideLauncherEntry; var iconSymbol = action.Id switch { TaskbarActionId.EditComponent => Symbol.Edit, _ when isDeleteAction || isHideAction => Symbol.Delete, _ => Symbol.Add }; Control icon = new SymbolIcon { Symbol = iconSymbol, IconVariant = IconVariant.Regular, FontSize = iconSize }; var buttonContent = new StackPanel { Orientation = Orientation.Horizontal, Spacing = spacing * 0.6, Children = { icon, new TextBlock { Text = action.Title, FontSize = fontSize, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center } } }; var button = new Button { Content = buttonContent, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Padding = new Thickness(padding), Foreground = (isDeleteAction || isHideAction) ? new SolidColorBrush(Color.Parse("#FFFF6B6B")) : Foreground, Tag = action.CommandKey }; button.Click += OnDynamicTaskbarActionClick; TaskbarDynamicActionsPanel.Children.Add(button); } } private void OnDynamicTaskbarActionClick(object? sender, RoutedEventArgs e) { if (sender is not Button button || button.Tag is not string commandKey) { return; } switch (commandKey) { case "desktop.add_page": AddDesktopPage(); break; case "desktop.delete_page": ConfirmAndDeleteCurrentDesktopPage(); break; case "component.delete": DeleteSelectedComponent(); break; case "component.edit": OpenSelectedComponentEditor(); break; case "launcher.hide": HideSelectedLauncherEntry(); break; } } private async void ConfirmAndDeleteCurrentDesktopPage() { if (_desktopPageCount <= MinDesktopPageCount) { return; } var dialog = new ContentDialog { Title = L("desktop.delete_page_confirm.title", "确认删除页面"), Content = L("desktop.delete_page_confirm.message", "确定要删除当前页面吗?\n\n此操作将删除当前页面上的所有组件,且无法撤销。"), PrimaryButtonText = L("desktop.delete_page_confirm.close", "取消"), SecondaryButtonText = L("desktop.delete_page_confirm.primary", "删除"), DefaultButton = ContentDialogButton.Primary }; var result = await dialog.ShowAsync(this); if (result == ContentDialogResult.Secondary) { DeleteCurrentDesktopPage(); } } private void DeleteSelectedComponent() { if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId) { return; } var placement = _desktopComponentPlacements.FirstOrDefault(p => string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); if (placement is null) { return; } var before = ClonePlacementSnapshot(placement); if (string.Equals(_componentEditorWindowService.CurrentPlacementId, placement.PlacementId, StringComparison.OrdinalIgnoreCase)) { _componentEditorWindowService.Close(); } ClearTimeZoneServiceBindings(_selectedDesktopComponentHost); DisposeComponentIfNeeded(_selectedDesktopComponentHost); if (_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid)) { pageGrid.Children.Remove(_selectedDesktopComponentHost); } _desktopComponentPlacements.Remove(placement); _componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId); RemovePlacementPreviewImage(placement.PlacementId); ClearDesktopComponentSelection(); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); PersistSettings(); TelemetryServices.Usage?.TrackDesktopComponentDeleted(before, "component.delete"); } private void OpenSelectedComponentEditor() { if (!TryGetSelectedDesktopPlacement(out var placement) || !_componentEditorRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor)) { return; } _componentEditorWindowService.Open(new ComponentEditorOpenRequest( Owner: this, Descriptor: descriptor, ComponentId: placement.ComponentId, PlacementId: placement.PlacementId, RefreshAction: () => RefreshDesktopComponentPlacement(placement.PlacementId))); TelemetryServices.Usage?.TrackDesktopComponentEditorOpened(ClonePlacementSnapshot(placement), "component.edit"); } private bool TryGetSelectedDesktopPlacement(out DesktopComponentPlacementSnapshot placement) { placement = null!; if (_selectedDesktopComponentHost?.Tag is not string placementId) { return false; } var matchedPlacement = _desktopComponentPlacements.FirstOrDefault(candidate => string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); if (matchedPlacement is null) { return false; } placement = matchedPlacement; return true; } private void RefreshDesktopComponentPlacement(string placementId) { if (string.IsNullOrWhiteSpace(placementId)) { return; } var placement = _desktopComponentPlacements.FirstOrDefault(candidate => string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); if (placement is null || !_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid)) { return; } var host = pageGrid.Children .OfType() .FirstOrDefault(candidate => string.Equals(candidate.Tag as string, placementId, StringComparison.OrdinalIgnoreCase)); if (host is null) { RestoreDesktopPageComponents(placement.PageIndex); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); QueuePlacementPreviewRefresh(placement); return; } var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId, placement.PageIndex); if (component is null) { return; } if (TryGetContentHost(host) is not Border contentHost) { RestoreDesktopPageComponents(placement.PageIndex); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); return; } ClearTimeZoneServiceBindings(host); DisposeComponentIfNeeded(host); contentHost.Child = component; ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen); InvalidateDesktopPageAwareComponentContextCache(); UpdateDesktopPageAwareComponentContext(); if (_selectedDesktopComponentHost == host) { ApplySelectionStateToHost(host, true); } QueuePlacementPreviewRefresh(placement); } private static void DisposeComponentIfNeeded(Border host) { if (TryGetContentHost(host) is Border contentHost && contentHost.Child is Control componentControl) { if (componentControl is IDisposable disposableComponent) { disposableComponent.Dispose(); } } } // Legacy in-window popup editor is removed; component editing now routes through the Material editor window service. private void AddDesktopPage() { if (_desktopPageCount >= MaxDesktopPageCount) { return; } _desktopPageCount = Math.Clamp(_desktopPageCount + 1, MinDesktopPageCount, MaxDesktopPageCount); _currentDesktopSurfaceIndex = Math.Clamp(_desktopPageCount - 1, 0, LauncherSurfaceIndex); RebuildDesktopGrid(); PersistSettings(); // Refresh taskbar actions after page count changes. ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); } private void DeleteCurrentDesktopPage() { if (_desktopPageCount <= MinDesktopPageCount) { return; } var placementsToRemove = _desktopComponentPlacements .Where(p => p.PageIndex == _currentDesktopSurfaceIndex) .ToList(); if (_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid)) { ClearTimeZoneServiceBindings(pageGrid.Children.OfType().ToList()); foreach (var child in pageGrid.Children.OfType()) { DisposeComponentIfNeeded(child); } } foreach (var placement in placementsToRemove) { _desktopComponentPlacements.Remove(placement); _componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId); } RemovePlacementPreviewImages(placementsToRemove); _desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount); // Clamp current page index to valid range after deletion. _currentDesktopSurfaceIndex = Math.Clamp(_currentDesktopSurfaceIndex, 0, _desktopPageCount - 1); // Update remaining page indices after deletion. foreach (var placement in _desktopComponentPlacements) { if (placement.PageIndex > _currentDesktopSurfaceIndex) { placement.PageIndex--; } } RebuildDesktopGrid(); PersistSettings(); // Refresh taskbar actions after page count changes. ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); } private void InitializeDesktopComponentPlacements(DesktopLayoutSettingsSnapshot snapshot) { _desktopComponentPlacements.Clear(); if (snapshot.DesktopComponentPlacements is null) { return; } foreach (var placement in snapshot.DesktopComponentPlacements) { if (placement is null || string.IsNullOrWhiteSpace(placement.ComponentId)) { continue; } var placementId = string.IsNullOrWhiteSpace(placement.PlacementId) ? Guid.NewGuid().ToString("N") : placement.PlacementId.Trim(); var componentId = placement.ComponentId.Trim(); if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) || !runtimeDescriptor.Definition.AllowDesktopPlacement) { continue; } var (widthCells, heightCells) = NormalizeComponentCellSpan( componentId, ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, placement.WidthCells, placement.HeightCells)); _desktopComponentPlacements.Add(new DesktopComponentPlacementSnapshot { PlacementId = placementId, PageIndex = Math.Max(0, placement.PageIndex), ComponentId = componentId, Row = Math.Max(0, placement.Row), Column = Math.Max(0, placement.Column), WidthCells = widthCells, HeightCells = heightCells }); } } private void RestoreDesktopPageComponents(int pageIndex) { if (!_desktopPageComponentGrids.TryGetValue(pageIndex, out var pageGrid)) { return; } ClearTimeZoneServiceBindings(pageGrid.Children.OfType().ToList()); pageGrid.Children.Clear(); InvalidateDesktopPageAwareComponentContextCache(); var maxColumns = pageGrid.ColumnDefinitions.Count; var maxRows = pageGrid.RowDefinitions.Count; if (maxColumns <= 0 || maxRows <= 0) { return; } foreach (var placement in _desktopComponentPlacements.Where(p => p.PageIndex == pageIndex)) { if (!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor) || !runtimeDescriptor.Definition.AllowDesktopPlacement) { continue; } var (widthCells, heightCells) = NormalizeComponentCellSpan( placement.ComponentId, ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, placement.WidthCells, placement.HeightCells)); var clampedColumn = Math.Clamp(placement.Column, 0, Math.Max(0, maxColumns - widthCells)); var clampedRow = Math.Clamp(placement.Row, 0, Math.Max(0, maxRows - heightCells)); var host = CreateDesktopComponentHost(placement); if (host is null) { continue; } placement.Column = clampedColumn; placement.Row = clampedRow; placement.WidthCells = widthCells; placement.HeightCells = heightCells; Grid.SetColumn(host, clampedColumn); Grid.SetRow(host, clampedRow); Grid.SetColumnSpan(host, widthCells); Grid.SetRowSpan(host, heightCells); pageGrid.Children.Add(host); } UpdateDesktopPageAwareComponentContext(); } private void PlaceDesktopComponentOnPage(string componentId, int pageIndex, int row, int column) { if (!_desktopPageComponentGrids.TryGetValue(pageIndex, out var pageGrid)) { return; } if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) || !runtimeDescriptor.Definition.AllowDesktopPlacement) { return; } var (widthCells, heightCells) = NormalizeComponentCellSpan( componentId, ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, runtimeDescriptor.Definition.MinWidthCells, runtimeDescriptor.Definition.MinHeightCells)); var maxColumns = pageGrid.ColumnDefinitions.Count; var maxRows = pageGrid.RowDefinitions.Count; if (maxColumns <= 0 || maxRows <= 0) { return; } column = Math.Clamp(column, 0, Math.Max(0, maxColumns - widthCells)); row = Math.Clamp(row, 0, Math.Max(0, maxRows - heightCells)); var placementId = Guid.NewGuid().ToString("N"); var placement = new DesktopComponentPlacementSnapshot { PlacementId = placementId, PageIndex = pageIndex, ComponentId = componentId, Row = row, Column = column, WidthCells = widthCells, HeightCells = heightCells }; var host = CreateDesktopComponentHost(placement); if (host is null) { return; } Grid.SetColumn(host, column); Grid.SetRow(host, row); Grid.SetColumnSpan(host, widthCells); Grid.SetRowSpan(host, heightCells); pageGrid.Children.Add(host); _desktopComponentPlacements.Add(placement); QueuePlacementPreviewRefresh(placement); InvalidateDesktopPageAwareComponentContextCache(); UpdateDesktopPageAwareComponentContext(); PersistSettings(); TelemetryServices.Usage?.TrackDesktopComponentPlaced(ClonePlacementSnapshot(placement), "component.create"); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); } private Border? CreateDesktopComponentHost(DesktopComponentPlacementSnapshot placement) { if (string.IsNullOrWhiteSpace(placement.PlacementId)) { placement.PlacementId = Guid.NewGuid().ToString("N"); } var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId, placement.PageIndex); if (component is null) { return null; } var componentCornerRadius = GetComponentCornerRadius(placement.ComponentId); var visualInset = GetDesktopComponentVisualInset( Math.Max(1, placement.WidthCells), Math.Max(1, placement.HeightCells)); var contentHost = new Border { Tag = DesktopComponentContentHostTag, Background = Brushes.Transparent, CornerRadius = new CornerRadius(componentCornerRadius), ClipToBounds = true, Padding = visualInset, Child = component }; // Separate visual arc size from hit target size for better touch usability. var handleTouchSize = Math.Clamp(_currentDesktopCellSize * 0.72, 30, 54); var handleVisualSize = Math.Clamp(_currentDesktopCellSize * 0.56, 20, 40); var handlePadding = Math.Max(2, (handleTouchSize - handleVisualSize) / 2); var arcThickness = Math.Clamp(_currentDesktopCellSize * 0.17, 7, 14); var arcData = Geometry.Parse("M 24,6 A 18,18 0 0 1 6,24"); var resizeHandleVisual = new Grid { Width = handleVisualSize, Height = handleVisualSize, IsHitTestVisible = false }; resizeHandleVisual.Children.Add(new PathShape { Data = arcData, Stretch = Stretch.Fill, Stroke = GetThemeBrush("AdaptiveTextAccentBrush"), StrokeThickness = arcThickness + 3, StrokeLineCap = PenLineCap.Round }); resizeHandleVisual.Children.Add(new PathShape { Data = arcData, Stretch = Stretch.Fill, Stroke = GetThemeBrush("AdaptiveAccentBrush"), StrokeThickness = arcThickness, StrokeLineCap = PenLineCap.Round }); var resizeHandle = new Border { Tag = DesktopComponentResizeHandleTag, Width = handleTouchSize, Height = handleTouchSize, Background = Brushes.Transparent, BorderBrush = Brushes.Transparent, BorderThickness = new Thickness(0), CornerRadius = new CornerRadius(handleTouchSize * 0.5), Padding = new Thickness(handlePadding), HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Bottom, Margin = new Thickness( 0, 0, -Math.Clamp(handleTouchSize * 0.42, 10, 24), -Math.Clamp(handleTouchSize * 0.42, 10, 24)), Child = resizeHandleVisual, Opacity = 1, IsVisible = false, IsHitTestVisible = false }; resizeHandle.PointerPressed += OnDesktopComponentResizeHandlePointerPressed; var hostChrome = new Grid { ClipToBounds = false }; hostChrome.Children.Add(contentHost); hostChrome.Children.Add(resizeHandle); var host = new Border { Tag = placement.PlacementId, Background = Brushes.Transparent, ClipToBounds = false, CornerRadius = new CornerRadius(componentCornerRadius), Child = hostChrome }; host.Classes.Add(DesktopComponentHostClass); ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen); host.PointerPressed += OnDesktopComponentHostPointerPressed; return host; } private (int WidthCells, int HeightCells) NormalizeComponentCellSpan( string componentId, (int WidthCells, int HeightCells) span) { if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor)) { var normalized = ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, span.WidthCells, span.HeightCells); if (runtimeDescriptor.Definition.ResizeMode == DesktopComponentResizeMode.Free) { return normalized; } return NormalizeAspectRatioForComponent(componentId, normalized); } return NormalizeAspectRatioForComponent( componentId, (Math.Max(1, span.WidthCells), Math.Max(1, span.HeightCells))); } private DesktopComponentResizeMode GetComponentResizeMode(string componentId) { if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor)) { return runtimeDescriptor.Definition.ResizeMode; } return DesktopComponentResizeMode.Proportional; } private static (int WidthCells, int HeightCells) NormalizeAspectRatioForComponent( string componentId, (int WidthCells, int HeightCells) span) { if (string.Equals(componentId, BuiltInComponentIds.DesktopWhiteboard, StringComparison.OrdinalIgnoreCase)) { // Support both portrait ratios and snap to nearest viable scale tier. return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 1, HeightUnit: 2, MinScale: 2), // 2x4, 3x6, 4x8... new ComponentScaleRule(WidthUnit: 3, HeightUnit: 4, MinScale: 1)); // 3x4, 6x8... } if (string.Equals(componentId, BuiltInComponentIds.DesktopBlackboardLandscape, StringComparison.OrdinalIgnoreCase)) { // Support both landscape ratios and snap to nearest viable scale tier. return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2), // 4x2, 6x3, 8x4... new ComponentScaleRule(WidthUnit: 4, HeightUnit: 3, MinScale: 1)); // 4x3, 8x6... } if (string.Equals(componentId, BuiltInComponentIds.DesktopDailyPoetry, StringComparison.OrdinalIgnoreCase)) { // Keep recommendation card at a 2:1 ratio with a minimum footprint of 4x2. return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopCnrDailyNews, StringComparison.OrdinalIgnoreCase)) { // Keep CNR widget at a 2:1 ratio: 4x2, 6x3, 8x4... return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopIfengNews, StringComparison.OrdinalIgnoreCase)) { // Keep iFeng news widget square with a minimum footprint of 4x4. return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopBilibiliHotSearch, StringComparison.OrdinalIgnoreCase)) { // Keep Bilibili hot search widget at a 2:1 ratio: 4x2, 6x3, 8x4... return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopBaiduHotSearch, StringComparison.OrdinalIgnoreCase)) { // Keep Baidu hot search widget at a 2:1 ratio: 4x2, 6x3, 8x4... return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopStcn24Forum, StringComparison.OrdinalIgnoreCase)) { // Keep STCN forum widget square with a minimum footprint of 4x4. return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopExchangeRateCalculator, StringComparison.OrdinalIgnoreCase)) { // Keep exchange rate converter square with minimum size 4x4. return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyNoiseCurve, StringComparison.OrdinalIgnoreCase)) { // Keep noise curve widget in a 2:1 ratio with minimum 4x2. return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopWorldClock, StringComparison.OrdinalIgnoreCase)) { // Keep world clock widget at 2:1 ratio: 4x2, 6x3, 8x4... return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyScoreOverview, StringComparison.OrdinalIgnoreCase)) { // Keep score overview widget square: 4x4, 5x5, 6x6... return SnapSpanToScaleRules( span, new ComponentScaleRule(WidthUnit: 1, HeightUnit: 1, MinScale: 4)); } if (string.Equals(componentId, BuiltInComponentIds.DesktopZhiJiaoHub, StringComparison.OrdinalIgnoreCase)) { // ZhiJiao Hub allows free resize but starts at 2x2 // Allow any aspect ratio, minimum 2x2 var width = Math.Max(2, span.WidthCells); var height = Math.Max(2, span.HeightCells); return (width, height); } return span; } private static (int WidthCells, int HeightCells) SnapSpanToScaleRules( (int WidthCells, int HeightCells) span, params ComponentScaleRule[] rules) { var targetWidth = Math.Max(1, span.WidthCells); var targetHeight = Math.Max(1, span.HeightCells); var hasCandidate = false; var bestWidth = targetWidth; var bestHeight = targetHeight; var bestArea = -1; var bestDistance = double.MaxValue; foreach (var rule in rules) { if (rule.WidthUnit <= 0 || rule.HeightUnit <= 0 || rule.MinScale <= 0) { continue; } var maxScale = Math.Min(targetWidth / rule.WidthUnit, targetHeight / rule.HeightUnit); if (maxScale < rule.MinScale) { continue; } for (var scale = rule.MinScale; scale <= maxScale; scale++) { var width = rule.WidthUnit * scale; var height = rule.HeightUnit * scale; var area = width * height; var dx = targetWidth - width; var dy = targetHeight - height; var distance = dx * dx + dy * dy; if (!hasCandidate || area > bestArea || (area == bestArea && distance < bestDistance)) { hasCandidate = true; bestWidth = width; bestHeight = height; bestArea = area; bestDistance = distance; } } } return hasCandidate ? (bestWidth, bestHeight) : (targetWidth, targetHeight); } private double GetComponentCornerRadius(string componentId) { var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor)) { return runtimeDescriptor.ResolveCornerRadius(new ComponentChromeContext( componentId, null, _currentDesktopCellSize, appearanceSnapshot.GlobalCornerRadiusScale, appearanceSnapshot.CornerRadiusTokens)); } var scale = Math.Max(GlobalAppearanceSettings.MinimumCornerRadiusScale, appearanceSnapshot.GlobalCornerRadiusScale); return Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18) * scale; } private Thickness GetDesktopComponentVisualInset(int widthCells, int heightCells) { // Keep the drop/selection bounds on grid cells while reducing visual footprint. var baseInset = Math.Clamp(_currentDesktopCellSize * 0.08, 2, 10); var horizontal = Math.Clamp(baseInset + Math.Max(0, widthCells - 1) * 0.25, 2, 12); var vertical = Math.Clamp(baseInset * 0.85 + Math.Max(0, heightCells - 1) * 0.2, 2, 10); return new Thickness(horizontal, vertical, horizontal, vertical); } private static Border? FindDesktopComponentHost(Visual? visual) { var current = visual; while (current is not null) { if (current is Border border && border.Classes.Contains(DesktopComponentHostClass)) { return border; } current = current.GetVisualParent(); } return null; } private static Border? TryGetContentHost(Border host) { if (host.Child is Grid hostChrome) { return hostChrome.Children .OfType() .FirstOrDefault(child => string.Equals(child.Tag?.ToString(), DesktopComponentContentHostTag, StringComparison.Ordinal)); } return null; } private static void ClearTimeZoneServiceBindings(IEnumerable roots) { foreach (var root in roots) { ClearTimeZoneServiceBindings(root); } } private static void ClearTimeZoneServiceBindings(Control root) { if (root is ITimeZoneAwareComponentWidget timeZoneAwareRoot) { timeZoneAwareRoot.ClearTimeZoneService(); } foreach (var descendant in root.GetVisualDescendants()) { if (descendant is ITimeZoneAwareComponentWidget timeZoneAwareChild) { timeZoneAwareChild.ClearTimeZoneService(); } } } private void InvalidateDesktopPageAwareComponentContextCache() { _desktopPageContextInitialized = false; _desktopPageContextActiveMask = 0; } private int BuildDesktopPageAwareComponentActiveMask() { if (_isSettingsOpen) { return 0; } var activeMask = 0; if (_desktopSurfacePageWidth > 1 && _desktopPagesHostTransform is not null && (_isDesktopSwipeActive || _desktopPageContextSettlingSourceIndex is not null || _desktopPageContextSettlingTargetIndex is not null)) { var viewportLeft = -_desktopPagesHostTransform.X; var viewportRight = viewportLeft + _desktopSurfacePageWidth; for (var pageIndex = 0; pageIndex < _desktopPageCount; pageIndex++) { var pageLeft = pageIndex * _desktopSurfacePageWidth; var pageRight = pageLeft + _desktopSurfacePageWidth; if (pageRight > viewportLeft + 0.5d && pageLeft < viewportRight - 0.5d) { activeMask |= 1 << pageIndex; } } } if (_currentDesktopSurfaceIndex >= 0 && _currentDesktopSurfaceIndex < _desktopPageCount) { activeMask |= 1 << _currentDesktopSurfaceIndex; } if (_desktopPageContextSettlingSourceIndex is int sourceIndex && sourceIndex >= 0 && sourceIndex < _desktopPageCount) { activeMask |= 1 << sourceIndex; } if (_desktopPageContextSettlingTargetIndex is int targetIndex && targetIndex >= 0 && targetIndex < _desktopPageCount) { activeMask |= 1 << targetIndex; } return activeMask; } private void UpdateDesktopPageAwareComponentContext() { var isEditMode = _isComponentLibraryOpen || _isSettingsOpen; var activeMask = BuildDesktopPageAwareComponentActiveMask(); var pageUpdateMask = !_desktopPageContextInitialized || isEditMode != _desktopPageContextEditMode ? _desktopPageComponentGrids.Keys.Aggregate(0, (mask, pageIndex) => mask | (1 << pageIndex)) : activeMask ^ _desktopPageContextActiveMask; if (_desktopPageContextInitialized && pageUpdateMask == 0 && isEditMode == _desktopPageContextEditMode && activeMask == _desktopPageContextActiveMask) { return; } foreach (var pair in _desktopPageComponentGrids) { var pageBit = 1 << pair.Key; if ((pageUpdateMask & pageBit) == 0) { continue; } var isOnActivePage = (activeMask & pageBit) != 0; foreach (var host in pair.Value.Children.OfType()) { if (!host.Classes.Contains(DesktopComponentHostClass)) { continue; } if (TryGetContentHost(host)?.Child is Control componentRoot) { ApplyDesktopPageContext(componentRoot, isOnActivePage, isEditMode); } } } _desktopPageContextInitialized = true; _desktopPageContextEditMode = isEditMode; _desktopPageContextActiveMask = activeMask; } private static void ApplyDesktopPageContext(Control root, bool isOnActivePage, bool isEditMode) { if (root is IDesktopPageVisibilityAwareComponentWidget awareRoot) { awareRoot.SetDesktopPageContext(isOnActivePage, isEditMode); } foreach (var descendant in root.GetVisualDescendants()) { if (descendant is IDesktopPageVisibilityAwareComponentWidget awareChild) { awareChild.SetDesktopPageContext(isOnActivePage, isEditMode); } } } private static Border? TryGetResizeHandle(Border host) { if (host.Child is Grid hostChrome) { return hostChrome.Children .OfType() .FirstOrDefault(child => string.Equals(child.Tag?.ToString(), DesktopComponentResizeHandleTag, StringComparison.Ordinal)); } return null; } private bool IsPointerOnSelectedFrameBorder(Border host, Point pointerInHost) { if (host != _selectedDesktopComponentHost || !_isComponentLibraryOpen) { return false; } var width = host.Bounds.Width; var height = host.Bounds.Height; if (width <= 1 || height <= 1) { return false; } var borderBand = Math.Clamp(_currentDesktopCellSize * 0.15, 8, 22); var onLeft = pointerInHost.X <= borderBand; var onRight = pointerInHost.X >= width - borderBand; var onTop = pointerInHost.Y <= borderBand; var onBottom = pointerInHost.Y >= height - borderBand; return onLeft || onRight || onTop || onBottom; } private Control? CreateDesktopComponentControl(string componentId, string? placementId = null, int? pageIndex = null) { if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor)) { return null; } return CreateDesktopComponentControl(runtimeDescriptor, _currentDesktopCellSize, placementId, pageIndex, "DesktopSurface"); } private Control? CreateDesktopComponentControl( string componentId, double cellSize, string? placementId, int? pageIndex, string action) { if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor)) { return null; } return CreateDesktopComponentControl(runtimeDescriptor, cellSize, placementId, pageIndex, action); } private Control? CreateDesktopComponentControl( DesktopComponentRuntimeDescriptor runtimeDescriptor, double cellSize, string? placementId, int? pageIndex, string action) { try { var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); var createContext = new ComponentLibraryCreateContext( cellSize, appearanceSnapshot.GlobalCornerRadiusScale, _timeZoneService, _weatherDataService, _recommendationInfoService, _calculatorDataService, _settingsFacade, placementId); if (!_componentLibraryService.TryCreateControl(runtimeDescriptor.Definition.Id, createContext, out var component, out var exception) || component is null) { throw exception ?? new InvalidOperationException("Component library service returned no control."); } component.Classes.Add(DesktopComponentClass); return component; } catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex)) { AppLogger.Warn( "ComponentRuntime", $"Action={action}; ComponentId={runtimeDescriptor.Definition.Id}; PlacementId={placementId ?? string.Empty}; PageIndex={pageIndex?.ToString() ?? string.Empty}; ExceptionType={ex.GetType().FullName}; IsFatal=false", ex); var failureView = new DesktopComponentFailureView( runtimeDescriptor.Definition.DisplayName, runtimeDescriptor.Definition.Id, placementId, pageIndex, action, ex); failureView.ApplyCellSize(cellSize); failureView.Classes.Add(DesktopComponentClass); return failureView; } } internal bool IsComponentLibraryOpenFromService => _isComponentLibraryOpen; internal bool IsDetachedComponentLibraryWindowOpenFromService => _detachedComponentLibraryWindow is { IsVisible: true }; internal void OpenComponentLibraryWindowFromService() { OpenComponentLibraryWindow(); } internal void CloseComponentLibraryWindowFromService() { CloseComponentLibraryWindow(reopenSettings: false); } internal void OpenDetachedComponentLibraryWindowFromService() { OpenDetachedComponentLibraryWindow(); } internal void CloseDetachedComponentLibraryWindowFromService() { CloseDetachedComponentLibraryWindow(); } public bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds) { anchorBounds = default; if (!IsVisible || BottomTaskbarContainer is null) { return false; } var origin = BottomTaskbarContainer.TranslatePoint(new Point(0, 0), this); if (origin is null) { return false; } var scale = RenderScaling > 0 ? RenderScaling : 1d; var width = (int)Math.Round(BottomTaskbarContainer.Bounds.Width * scale); var height = (int)Math.Round(BottomTaskbarContainer.Bounds.Height * scale); if (width <= 0 || height <= 0) { return false; } var x = Position.X + (int)Math.Round(origin.Value.X * scale); var y = Position.Y + (int)Math.Round(origin.Value.Y * scale); anchorBounds = new PixelRect(x, y, width, height); return true; } private void CollapseComponentLibraryPanel() { // Animate component library panel collapsing downward if (ComponentLibraryWindow is not null) { ComponentLibraryWindow.Height = 0; ComponentLibraryWindow.IsVisible = false; } _isComponentLibraryOpen = false; CancelDesktopComponentDrag(); CancelDesktopComponentResize(restoreOriginalSpan: true); CloseDetachedComponentLibraryWindow(); ClearDesktopComponentSelection(); ClearSelectedLauncherTile(refreshTaskbar: false); UpdateDesktopComponentHostEditState(); ClearComponentLibraryPreviewControls(); UpdateComponentLibraryLayout(_currentDesktopCellSize); } private void UpdateDesktopComponentHostEditState() { foreach (var pageGrid in _desktopPageComponentGrids.Values) { foreach (var child in pageGrid.Children) { if (child is Border host && host.Classes.Contains(DesktopComponentHostClass)) { ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen); } } } UpdateDesktopPageAwareComponentContext(); } private void ApplyDesktopEditStateToHost(Border host, bool isEditMode) { host.IsHitTestVisible = true; var keepContentInteractive = ShouldKeepContentInteractiveInEditMode(host); if (TryGetContentHost(host) is Border contentHost) { // In edit mode, keep selected interactive widgets usable; drag/resize still uses host border/handles. contentHost.IsHitTestVisible = !isEditMode || keepContentInteractive; if (contentHost.Child is Control componentControl) { componentControl.IsHitTestVisible = !isEditMode || keepContentInteractive; } } var isSelected = host == _selectedDesktopComponentHost; ApplySelectionStateToHost(host, isSelected); } private bool ShouldKeepContentInteractiveInEditMode(Border host) { if (!_isComponentLibraryOpen || host.Tag is not string placementId) { return false; } var placement = _desktopComponentPlacements.FirstOrDefault(p => string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); if (placement is null) { return false; } return string.Equals( placement.ComponentId, BuiltInComponentIds.DesktopStudySessionHistory, StringComparison.OrdinalIgnoreCase); } private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e) { if (!_isComponentLibraryOpen || HasActiveDesktopEditSession) { return; } if (DesktopPagesViewport is null || sender is not Border host || host.Tag is not string placementId || !e.GetCurrentPoint(host).Properties.IsLeftButtonPressed) { return; } var placement = _desktopComponentPlacements.FirstOrDefault(p => string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); if (placement is null) { return; } var wasSelected = host == _selectedDesktopComponentHost; SetSelectedDesktopComponent(host); if (!wasSelected) { e.Handled = true; return; } var pointerInHost = e.GetPosition(host); if (IsPointerOnSelectedFrameBorder(host, pointerInHost)) { BeginDesktopComponentResizeDrag(host, placement, e); if (IsDesktopEditResizeMode) { e.Handled = true; } return; } BeginDesktopComponentMoveDrag(host, placement, e); e.Handled = true; } private void SetSelectedDesktopComponent(Border? host) { ClearSelectedLauncherTile(refreshTaskbar: false); // Clear previous selection if (_selectedDesktopComponentHost is not null && _selectedDesktopComponentHost != host) { ApplySelectionStateToHost(_selectedDesktopComponentHost, false); } // Set new selection _selectedDesktopComponentHost = host; if (host is not null) { ApplySelectionStateToHost(host, true); } // Refresh taskbar actions to show delete/edit buttons ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); } private void ApplySelectionStateToHost(Border host, bool isSelected) { var showSelection = isSelected && _isComponentLibraryOpen; host.BorderThickness = showSelection ? new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)) : new Thickness(0); host.BorderBrush = showSelection ? GetThemeBrush("AdaptiveAccentBrush") : null; if (TryGetResizeHandle(host) is Border resizeHandle) { resizeHandle.IsVisible = showSelection; resizeHandle.IsHitTestVisible = showSelection; } } private void ClearDesktopComponentSelection() { if (_selectedDesktopComponentHost is not null) { ApplySelectionStateToHost(_selectedDesktopComponentHost, false); _selectedDesktopComponentHost = null; } _componentEditorWindowService.Close(); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); } private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e) { if (HasActiveDesktopEditSession || DesktopPagesViewport is null || !TryGetCurrentDesktopGridGeometry(out var grid) || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor)) { return; } var (widthCells, heightCells) = NormalizeComponentCellSpan( placement.ComponentId, ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, placement.WidthCells, placement.HeightCells)); var pointerInViewport = e.GetPosition(DesktopPagesViewport); _desktopEditOriginalRect = DesktopPlacementMath.GetCellRect(grid, placement.Column, placement.Row, widthCells, heightCells); _desktopEditStartRow = placement.Row; _desktopEditStartColumn = placement.Column; var pointerOffset = DesktopPlacementMath.Subtract( pointerInViewport, new Point(_desktopEditOriginalRect.X, _desktopEditOriginalRect.Y)); _desktopEditSession = DesktopEditSession.CreateDraggingExisting( placement.ComponentId, placement.PlacementId, placement.PageIndex, widthCells, heightCells, pointerInViewport, pointerOffset, GetComponentLibraryBoundsInViewport()); CollapseComponentLibraryForDesktopEdit(ResolveDesktopEditTitle(placement.ComponentId)); SetDesktopEditSourceHost(sourceHost, 0.22); EnsureDesktopEditOverlayPresenter(); UpdateDesktopEditOverlayMetadata(placement.ComponentId, widthCells, heightCells, L("component.move", "Move")); ApplyDesktopEditOverlayPreviewImage(placement.ComponentId, placement.PlacementId, widthCells, heightCells); PrimeDesktopEditPreviewImage( placement.ComponentId, placement.PlacementId, placement.PageIndex, widthCells, heightCells); _desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect); _desktopEditOverlayPresenter?.SetCandidateRect(_desktopEditOriginalRect); _desktopEditOverlayPresenter?.SetInvalid(false); _desktopEditOverlayPresenter?.Show(DesktopEditGhostVisualStyle.StandardLift); UpdateDesktopEditSession(pointerInViewport); e.Pointer.Capture(this); } private void BeginDesktopComponentNewDrag(string componentId, PointerPressedEventArgs e) { if (!_isComponentLibraryOpen || HasActiveDesktopEditSession || DesktopPagesViewport is null || _currentDesktopCellSize <= 0 || !_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) || !runtimeDescriptor.Definition.AllowDesktopPlacement) { return; } var (widthCells, heightCells) = NormalizeComponentCellSpan( componentId, ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, runtimeDescriptor.Definition.MinWidthCells, runtimeDescriptor.Definition.MinHeightCells)); var pointerInViewport = e.GetPosition(DesktopPagesViewport); var previewSize = GetComponentPixelSize(widthCells, heightCells, _currentDesktopCellSize, _currentDesktopCellGap); var pointerOffset = new Point(previewSize.Width * 0.5, previewSize.Height * 0.5); _desktopEditOriginalRect = new Rect( DesktopPlacementMath.Subtract(pointerInViewport, pointerOffset), previewSize); _desktopEditSession = DesktopEditSession.CreatePendingNew( componentId, _currentDesktopSurfaceIndex, widthCells, heightCells, pointerInViewport, pointerOffset, GetComponentLibraryBoundsInViewport()); EnsureDesktopEditOverlayPresenter(); UpdateDesktopEditOverlayMetadata(componentId, widthCells, heightCells, L("component_library.drag_hint", "Drag to place")); ApplyDesktopEditOverlayPreviewImage(componentId, placementId: null, widthCells, heightCells); PrimeDesktopEditPreviewImage( componentId, placementId: null, _currentDesktopSurfaceIndex, widthCells, heightCells); _desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect); _desktopEditOverlayPresenter?.SetCandidateRect(null); _desktopEditOverlayPresenter?.SetInvalid(false); e.Pointer.Capture(this); } private void OnDesktopComponentResizeHandlePointerPressed(object? sender, PointerPressedEventArgs e) { if (!_isComponentLibraryOpen || HasActiveDesktopEditSession || DesktopPagesViewport is null || sender is not Border handle || !e.GetCurrentPoint(handle).Properties.IsLeftButtonPressed) { return; } var host = FindDesktopComponentHost(handle); if (host?.Tag is not string placementId) { return; } var placement = _desktopComponentPlacements.FirstOrDefault(p => string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); if (placement is null) { return; } SetSelectedDesktopComponent(host); BeginDesktopComponentResizeDrag(host, placement, e); if (IsDesktopEditResizeMode) { e.Handled = true; } } private void BeginDesktopComponentResizeDrag( Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e) { if (HasActiveDesktopEditSession || DesktopPagesViewport is null || _currentDesktopCellSize <= 0 || !_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor) || !_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid) || !TryGetCurrentDesktopGridGeometry(out var grid)) { return; } var startSpan = NormalizeComponentCellSpan( placement.ComponentId, ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, placement.WidthCells, placement.HeightCells)); var minSpan = NormalizeComponentCellSpan( placement.ComponentId, ComponentPlacementRules.EnsureMinimumSize( runtimeDescriptor.Definition, runtimeDescriptor.Definition.MinWidthCells, runtimeDescriptor.Definition.MinHeightCells)); var maxWidthCells = Math.Max(startSpan.WidthCells, pageGrid.ColumnDefinitions.Count - placement.Column); var maxHeightCells = Math.Max(startSpan.HeightCells, pageGrid.RowDefinitions.Count - placement.Row); if (maxWidthCells <= 0 || maxHeightCells <= 0) { return; } var pointerInViewport = e.GetPosition(DesktopPagesViewport); _desktopEditOriginalRect = DesktopPlacementMath.GetCellRect( grid, placement.Column, placement.Row, startSpan.WidthCells, startSpan.HeightCells); _desktopEditStartWidthCells = startSpan.WidthCells; _desktopEditStartHeightCells = startSpan.HeightCells; _desktopEditMinWidthCells = Math.Max(1, Math.Min(minSpan.WidthCells, maxWidthCells)); _desktopEditMinHeightCells = Math.Max(1, Math.Min(minSpan.HeightCells, maxHeightCells)); _desktopEditMaxWidthCells = maxWidthCells; _desktopEditMaxHeightCells = maxHeightCells; _desktopEditResizeMode = runtimeDescriptor.Definition.ResizeMode; _desktopEditSession = DesktopEditSession.CreateResizingExisting( placement.ComponentId, placement.PlacementId, placement.PageIndex, startSpan.WidthCells, startSpan.HeightCells, pointerInViewport, GetComponentLibraryBoundsInViewport()) with { TargetRow = placement.Row, TargetColumn = placement.Column }; CollapseComponentLibraryForDesktopEdit(ResolveDesktopEditTitle(placement.ComponentId)); SetDesktopEditSourceHost(sourceHost, 0.22); EnsureDesktopEditOverlayPresenter(); UpdateDesktopEditOverlayMetadata(placement.ComponentId, startSpan.WidthCells, startSpan.HeightCells, L("component.resize", "Resize")); ApplyDesktopEditOverlayPreviewImage(placement.ComponentId, placement.PlacementId, startSpan.WidthCells, startSpan.HeightCells); PrimeDesktopEditPreviewImage( placement.ComponentId, placement.PlacementId, placement.PageIndex, startSpan.WidthCells, startSpan.HeightCells); _desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect); _desktopEditOverlayPresenter?.SetCandidateRect(_desktopEditOriginalRect); _desktopEditOverlayPresenter?.SetInvalid(false); _desktopEditOverlayPresenter?.Show(DesktopEditGhostVisualStyle.StandardLift); UpdateDesktopEditSession(pointerInViewport); e.Pointer.Capture(this); } private void CancelDesktopComponentResize(bool restoreOriginalSpan) { if (!IsDesktopEditResizeMode && !_isDesktopEditCommitPending) { return; } if (restoreOriginalSpan && _desktopEditSourceHost is not null) { Grid.SetColumnSpan(_desktopEditSourceHost, Math.Max(1, _desktopEditStartWidthCells)); Grid.SetRowSpan(_desktopEditSourceHost, Math.Max(1, _desktopEditStartHeightCells)); } CancelDesktopEditSession(animate: false); } private void OnDesktopComponentDragPointerMoved(object? sender, PointerEventArgs e) { if (DesktopPagesViewport is null) { return; } if (!HasActiveDesktopEditSession || _isDesktopEditCommitPending) { return; } UpdateDesktopEditSession(e.GetPosition(DesktopPagesViewport)); } private void OnDesktopComponentDragPointerReleased(object? sender, PointerReleasedEventArgs e) { if (DesktopPagesViewport is null) { return; } if (!HasActiveDesktopEditSession) { return; } var pointerInViewport = e.GetPosition(DesktopPagesViewport); var success = CompleteDesktopEditSession(pointerInViewport); if (!success) { CancelDesktopEditSession(animate: !_desktopEditSession.IsPendingNew); } e.Pointer.Capture(null); if (success) { e.Handled = true; } } private void OnDesktopComponentDragPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) { if (_isDesktopEditCommitPending) { return; } if (!HasActiveDesktopEditSession) { return; } CancelDesktopEditSession(animate: !_desktopEditSession.IsPendingNew); } private bool TryMoveExistingDesktopComponent(string placementId, int row, int column) { if (string.IsNullOrWhiteSpace(placementId)) { return false; } if (!TryGetDesktopPlacementById(placementId, out var placement)) { return false; } var before = ClonePlacementSnapshot(placement); if (!DesktopPlacementMath.HasCellPositionChanged(placement.Row, placement.Column, row, column)) { return false; } var host = _desktopEditSourceHost; if (host is null && _desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid)) { host = pageGrid.Children .OfType() .FirstOrDefault(candidate => string.Equals(candidate.Tag as string, placementId, StringComparison.OrdinalIgnoreCase)); } placement.Row = Math.Max(0, row); placement.Column = Math.Max(0, column); if (host is not null) { Grid.SetRow(host, placement.Row); Grid.SetColumn(host, placement.Column); ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen); } PersistSettings(); TelemetryServices.Usage?.TrackDesktopComponentMoved(before, ClonePlacementSnapshot(placement), "component.move"); return true; } private void CancelDesktopComponentDrag() { if (!IsDesktopEditDragMode && !_isDesktopEditCommitPending) { return; } CancelDesktopEditSession(animate: false); } private void ShowComponentLibraryCategoryView() { if (ComponentLibraryCategoriesView is not null) { ComponentLibraryCategoriesView.IsVisible = true; } if (ComponentLibraryComponentsView is not null) { ComponentLibraryComponentsView.IsVisible = false; } } private void ShowComponentLibraryComponentsView() { if (ComponentLibraryCategoriesView is not null) { ComponentLibraryCategoriesView.IsVisible = false; } if (ComponentLibraryComponentsView is not null) { ComponentLibraryComponentsView.IsVisible = true; } } private void BuildComponentLibraryCategoryPages() { if (ComponentLibraryCategoryViewport is null || ComponentLibraryCategoryPagesHost is null || ComponentLibraryCategoryPagesContainer is null || ComponentLibraryEmptyTextBlock is null) { return; } _componentLibraryCategories = GetComponentLibraryCategories(); var categoryCount = _componentLibraryCategories.Count; ComponentLibraryEmptyTextBlock.IsVisible = categoryCount == 0; ComponentLibraryCategoryPagesContainer.Children.Clear(); ComponentLibraryCategoryPagesContainer.RowDefinitions.Clear(); ComponentLibraryCategoryPagesContainer.ColumnDefinitions.Clear(); ComponentLibraryCategoryPagesContainer.Width = double.NaN; ComponentLibraryCategoryPagesContainer.Height = double.NaN; ComponentLibraryCategoryPagesHost.Width = double.NaN; ComponentLibraryCategoryPagesHost.Height = double.NaN; if (categoryCount == 0) { _componentLibraryCategoryIndex = 0; _componentLibraryActiveCategoryId = null; UpdateComponentLibraryComponentNavigationButtons(); return; } if (!string.IsNullOrWhiteSpace(_componentLibraryActiveCategoryId)) { var activeIndex = _componentLibraryCategories .Select((category, index) => (category, index)) .FirstOrDefault(tuple => string.Equals(tuple.category.Id, _componentLibraryActiveCategoryId, StringComparison.OrdinalIgnoreCase)) .index; _componentLibraryCategoryIndex = Math.Clamp(activeIndex, 0, Math.Max(0, categoryCount - 1)); } else { _componentLibraryCategoryIndex = Math.Clamp(_componentLibraryCategoryIndex, 0, Math.Max(0, categoryCount - 1)); } _componentLibraryActiveCategoryId = _componentLibraryCategories[_componentLibraryCategoryIndex].Id; ComponentLibraryCategoryPagesContainer.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star))); for (var i = 0; i < categoryCount; i++) { var category = _componentLibraryCategories[i]; var isSelected = i == _componentLibraryCategoryIndex; var row = new RowDefinition(GridLength.Auto); ComponentLibraryCategoryPagesContainer.RowDefinitions.Add(row); var icon = new SymbolIcon { Symbol = category.Icon, IconVariant = IconVariant.Regular, FontSize = 18, VerticalAlignment = VerticalAlignment.Center }; var title = new TextBlock { Text = category.Title, FontSize = 15, FontWeight = isSelected ? FontWeight.Bold : FontWeight.SemiBold, Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"), VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis }; var contentGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*"), ColumnSpacing = 10, Children = { icon, title } }; Grid.SetColumn(icon, 0); Grid.SetColumn(title, 1); var itemButton = new Button { Tag = i, Margin = new Thickness(0, 0, 0, i < categoryCount - 1 ? 8 : 0), Padding = new Thickness(12, 10), HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalContentAlignment = HorizontalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Center, Background = isSelected ? GetThemeBrush("AdaptiveNavItemSelectedBackgroundBrush") : GetThemeBrush("AdaptiveNavItemBackgroundBrush"), BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"), BorderThickness = new Thickness(isSelected ? 1.5 : 1), Content = contentGrid }; itemButton.Click += OnComponentLibraryCategoryItemClick; Grid.SetRow(itemButton, i); Grid.SetColumn(itemButton, 0); ComponentLibraryCategoryPagesContainer.Children.Add(itemButton); } _componentLibraryCategoryHostTransform = null; _componentLibraryCategoryPageWidth = 0; if (ComponentLibraryBackTextBlock is not null) { ComponentLibraryBackTextBlock.Text = L("common.back", "Back"); } EnsureComponentLibraryPreviewWarmup(); } private IReadOnlyList GetComponentLibraryCategories() { var categories = _componentLibraryService.GetDesktopCategories(); if (categories.Count == 0) { return Array.Empty(); } return categories .Select(category => new ComponentLibraryCategory( category.Id, ResolveComponentLibraryCategoryIcon(category.Id), GetLocalizedComponentLibraryCategoryTitle(category.Id), category.Components)) .ToList(); } private Symbol ResolveComponentLibraryCategoryIcon(string categoryId) { if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) { return Symbol.Clock; } if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase)) { return Symbol.CalendarDate; } if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase)) { return Symbol.WeatherSunny; } if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase)) { return Symbol.Edit; } if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase)) { return Symbol.Play; } if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase)) { return Symbol.Apps; } if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) { return Symbol.Calculator; } if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase)) { return Symbol.Hourglass; } if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase)) { return Symbol.Folder; } return Symbol.Apps; } private string GetLocalizedComponentLibraryCategoryTitle(string categoryId) { if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) { return L("component_category.clock", "Clock"); } if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase)) { return L("component_category.date", "Calendar"); } if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase)) { return L("component_category.weather", "Weather"); } if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase)) { return L("component_category.board", "Board"); } if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase)) { return L("component_category.media", "Media"); } if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase)) { return L("component_category.info", "Info"); } if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase)) { return L("component_category.calculator", "Calculator"); } if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase)) { return L("component_category.study", "Study"); } if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase)) { return L("component_category.file", "File"); } return categoryId; } private void ApplyComponentLibraryCategoryOffset() { if (_componentLibraryCategoryHostTransform is null || _componentLibraryCategoryPageWidth <= 0) { return; } _componentLibraryCategoryHostTransform.X = -_componentLibraryCategoryIndex * _componentLibraryCategoryPageWidth; } private void ApplyComponentLibraryComponentOffset() { if (_componentLibraryComponentHostTransform is null || _componentLibraryComponentPageWidth <= 0) { return; } _componentLibraryComponentHostTransform.X = -_componentLibraryComponentIndex * _componentLibraryComponentPageWidth; UpdateComponentLibraryComponentNavigationButtons(); } private void UpdateComponentLibraryComponentNavigationButtons() { if (ComponentLibraryPrevComponentButton is null || ComponentLibraryNextComponentButton is null) { return; } var maxIndex = Math.Max(0, _componentLibraryActiveComponents.Count - 1); var hasMultiplePages = maxIndex > 0; ComponentLibraryPrevComponentButton.IsVisible = hasMultiplePages; ComponentLibraryNextComponentButton.IsVisible = hasMultiplePages; if (!hasMultiplePages) { ComponentLibraryPrevComponentButton.IsEnabled = false; ComponentLibraryNextComponentButton.IsEnabled = false; return; } ComponentLibraryPrevComponentButton.IsEnabled = _componentLibraryComponentIndex > 0; ComponentLibraryNextComponentButton.IsEnabled = _componentLibraryComponentIndex < maxIndex; } private void OpenComponentLibraryCurrentCategory() { if (_componentLibraryCategories.Count == 0) { return; } _componentLibraryCategoryIndex = Math.Clamp(_componentLibraryCategoryIndex, 0, Math.Max(0, _componentLibraryCategories.Count - 1)); var category = _componentLibraryCategories[_componentLibraryCategoryIndex]; _componentLibraryActiveCategoryId = category.Id; _componentLibraryComponentIndex = 0; _ = WarmComponentLibraryCategoryPreviewsAsync(category); BuildComponentLibraryComponentPages(category); ShowComponentLibraryComponentsView(); } private void BuildComponentLibraryComponentPages(ComponentLibraryCategory category) { if (ComponentLibraryComponentViewport is null || ComponentLibraryComponentPagesHost is null || ComponentLibraryComponentPagesContainer is null) { return; } _componentLibraryActiveComponents = category.Components; var componentCount = _componentLibraryActiveComponents.Count; ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType().ToList()); ComponentLibraryComponentPagesContainer.Children.Clear(); ComponentLibraryComponentPagesContainer.RowDefinitions.Clear(); ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear(); ClearComponentLibraryPreviewVisualTargets(); if (componentCount == 0) { _componentLibraryComponentIndex = 0; UpdateComponentLibraryComponentNavigationButtons(); return; } var viewportWidth = ComponentLibraryComponentViewport.Bounds.Width; if (viewportWidth <= 1) { if (ComponentLibraryComponentViewport.Parent is Control parent && parent.Bounds.Width > 1) { // Parent includes left/right nav buttons; reserve space to get true viewport width. viewportWidth = Math.Max(1, parent.Bounds.Width - 96); } else if (ComponentLibraryWindow is not null) { viewportWidth = Math.Max(1, ComponentLibraryWindow.Bounds.Width - 150); } } var viewportHeight = ComponentLibraryComponentViewport.Bounds.Height; if (viewportHeight <= 1) { if (ComponentLibraryComponentViewport.Parent is Control parent && parent.Bounds.Height > 1) { viewportHeight = Math.Max(1, parent.Bounds.Height); } else if (ComponentLibraryWindow is not null) { viewportHeight = Math.Max(1, ComponentLibraryWindow.Bounds.Height - 170); } } _componentLibraryComponentPageWidth = Math.Max(1, viewportWidth); ComponentLibraryComponentPagesHost.Width = _componentLibraryComponentPageWidth * componentCount; ComponentLibraryComponentPagesHost.Height = viewportHeight; ComponentLibraryComponentPagesContainer.Width = ComponentLibraryComponentPagesHost.Width; ComponentLibraryComponentPagesContainer.Height = viewportHeight; ComponentLibraryComponentPagesContainer.RowDefinitions.Add(new RowDefinition(new GridLength(viewportHeight, GridUnitType.Pixel))); for (var i = 0; i < componentCount; i++) { ComponentLibraryComponentPagesContainer.ColumnDefinitions.Add( new ColumnDefinition(new GridLength(_componentLibraryComponentPageWidth, GridUnitType.Pixel))); } _componentLibraryComponentIndex = Math.Clamp(_componentLibraryComponentIndex, 0, Math.Max(0, componentCount - 1)); for (var i = 0; i < componentCount; i++) { var component = _componentLibraryActiveComponents[i]; var page = new Grid { Width = _componentLibraryComponentPageWidth, Height = viewportHeight, Background = Brushes.Transparent }; // Fit the preview to the page while preserving component cell span proportions. var previewMaxWidth = _componentLibraryComponentPageWidth * 0.94; var previewMaxHeight = viewportHeight * 0.86; var previewSpan = NormalizeComponentCellSpan( component.ComponentId, (component.MinWidthCells, component.MinHeightCells)); var previewCellSize = Math.Min( previewMaxWidth / Math.Max(1, previewSpan.WidthCells), previewMaxHeight / Math.Max(1, previewSpan.HeightCells)); previewCellSize = Math.Clamp(previewCellSize, 24, 96); var previewWidth = previewSpan.WidthCells * previewCellSize; var previewHeight = previewSpan.HeightCells * previewCellSize; var previewKey = CreateComponentTypePreviewKey(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells); var cachedPreviewImage = ResolveComponentTypePreviewImage(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells); var previewImage = new Image { Width = previewWidth, Height = previewHeight, Stretch = Stretch.Uniform, Source = cachedPreviewImage, IsVisible = cachedPreviewImage is not null, IsHitTestVisible = false }; var previewFallback = new Border { Width = previewWidth, Height = previewHeight, Background = GetThemeBrush("AdaptiveCardBackgroundBrush"), BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(Math.Clamp(Math.Min(previewWidth, previewHeight) * 0.18, 12, 28)), IsVisible = cachedPreviewImage is null, Child = new TextBlock { Text = L("component_library.preview_loading", "Preparing preview"), FontSize = 11, Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center } }; RegisterComponentLibraryPreviewVisual(previewKey, previewImage, previewFallback); var previewSurface = new Grid { Width = previewWidth, Height = previewHeight, IsHitTestVisible = false, Children = { previewImage, previewFallback } }; var previewBorder = new Border { Width = previewWidth, Height = previewHeight, ClipToBounds = false, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Child = previewSurface, Tag = component.ComponentId }; previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed; var label = new TextBlock { Text = GetLocalizedComponentDisplayName(component), FontSize = 14, FontWeight = FontWeight.SemiBold, Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"), HorizontalAlignment = HorizontalAlignment.Center }; var hint = new TextBlock { Text = L("component_library.drag_hint", "Drag to place"), FontSize = 12, Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"), HorizontalAlignment = HorizontalAlignment.Center }; var stack = new StackPanel { Spacing = 8, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Children = { previewBorder, label, hint } }; page.Children.Add(stack); Grid.SetRow(page, 0); Grid.SetColumn(page, i); ComponentLibraryComponentPagesContainer.Children.Add(page); if (cachedPreviewImage is null) { _ = EnsureComponentTypePreviewImageAsync(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells); } else { ApplyPreviewEntryToEmbeddedVisuals(previewKey); } } _componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform; if (_componentLibraryComponentHostTransform is null) { _componentLibraryComponentHostTransform = new TranslateTransform(); ComponentLibraryComponentPagesHost.RenderTransform = _componentLibraryComponentHostTransform; } ApplyComponentLibraryComponentOffset(); UpdateComponentLibraryComponentNavigationButtons(); } private void ClearComponentLibraryPreviewControls() { if (ComponentLibraryComponentPagesContainer is null) { return; } ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType().ToList()); ComponentLibraryComponentPagesContainer.Children.Clear(); ComponentLibraryComponentPagesContainer.RowDefinitions.Clear(); ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear(); ClearComponentLibraryPreviewVisualTargets(); } private string GetLocalizedComponentDisplayName(ComponentLibraryComponentEntry component) { return string.IsNullOrWhiteSpace(component.DisplayNameLocalizationKey) ? component.DisplayName : L(component.DisplayNameLocalizationKey, component.DisplayName); } private void OnComponentLibraryComponentPreviewPointerPressed(object? sender, PointerPressedEventArgs e) { if (sender is not Border border || border.Tag is not string componentId || !e.GetCurrentPoint(border).Properties.IsLeftButtonPressed) { return; } BeginDesktopComponentNewDrag(componentId, e); if (HasActiveDesktopEditSession) { e.Handled = true; } } private bool _isComponentLibraryWindowDragging; private Point _componentLibraryWindowDragStartPoint; private Thickness _componentLibraryWindowOriginalMargin; private bool _isComponentLibraryWindowPositionCustomized; private void OnComponentLibraryWindowPointerPressed(object? sender, PointerPressedEventArgs e) { if (ComponentLibraryWindow is null || !_isComponentLibraryOpen || IsComponentLibraryTemporarilyCollapsedForDesktopEdit()) { return; } var point = e.GetPosition(ComponentLibraryWindow); if (point.Y > 40) // 閺嶅洭顣介弽蹇涚彯鎼达妇瀹虫稉?0px { return; } _isComponentLibraryWindowDragging = true; _componentLibraryWindowDragStartPoint = e.GetPosition(this); _componentLibraryWindowOriginalMargin = ComponentLibraryWindow.Margin; e.Pointer.Capture(ComponentLibraryWindow); e.Handled = true; } private void OnComponentLibraryWindowPointerMoved(object? sender, PointerEventArgs e) { if (IsComponentLibraryTemporarilyCollapsedForDesktopEdit()) { if (_isComponentLibraryWindowDragging) { _isComponentLibraryWindowDragging = false; e.Pointer.Capture(null); } return; } if (!_isComponentLibraryWindowDragging || ComponentLibraryWindow is null) { return; } var currentPoint = e.GetPosition(this); var delta = currentPoint - _componentLibraryWindowDragStartPoint; var newMargin = new Thickness( Math.Max(10, _componentLibraryWindowOriginalMargin.Left + delta.X), Math.Max(10, _componentLibraryWindowOriginalMargin.Top + delta.Y), Math.Max(10, _componentLibraryWindowOriginalMargin.Right - delta.X), Math.Max(10, _componentLibraryWindowOriginalMargin.Bottom - delta.Y) ); ComponentLibraryWindow.Margin = newMargin; SyncComponentLibraryCollapseExpandedState(); e.Handled = true; } private void OnComponentLibraryWindowPointerReleased(object? sender, PointerReleasedEventArgs e) { if (IsComponentLibraryTemporarilyCollapsedForDesktopEdit()) { if (_isComponentLibraryWindowDragging) { _isComponentLibraryWindowDragging = false; e.Pointer.Capture(null); } return; } if (!_isComponentLibraryWindowDragging) { return; } _isComponentLibraryWindowDragging = false; e.Pointer.Capture(null); if (ComponentLibraryWindow is not null) { SaveComponentLibraryWindowPosition(); } e.Handled = true; } private void OnComponentLibraryBackClick(object? sender, RoutedEventArgs e) { ShowComponentLibraryCategoryView(); BuildComponentLibraryCategoryPages(); } private void OnComponentLibraryCategoryItemClick(object? sender, RoutedEventArgs e) { if (sender is not Button button || button.Tag is not int categoryIndex || _componentLibraryCategories.Count == 0) { return; } _componentLibraryCategoryIndex = Math.Clamp(categoryIndex, 0, Math.Max(0, _componentLibraryCategories.Count - 1)); OpenComponentLibraryCurrentCategory(); } private void OnComponentLibraryPrevComponentClick(object? sender, RoutedEventArgs e) { if (_componentLibraryActiveComponents.Count <= 1) { return; } _componentLibraryComponentIndex = Math.Max(0, _componentLibraryComponentIndex - 1); ApplyComponentLibraryComponentOffset(); } private void OnComponentLibraryNextComponentClick(object? sender, RoutedEventArgs e) { var maxIndex = Math.Max(0, _componentLibraryActiveComponents.Count - 1); if (maxIndex <= 0) { return; } _componentLibraryComponentIndex = Math.Min(maxIndex, _componentLibraryComponentIndex + 1); ApplyComponentLibraryComponentOffset(); } private void OnComponentLibraryCategoryViewportPointerPressed(object? sender, PointerPressedEventArgs e) { if (!_isComponentLibraryOpen || _componentLibraryCategories.Count == 0 || ComponentLibraryCategoryViewport is null || _componentLibraryCategoryHostTransform is null || !e.GetCurrentPoint(ComponentLibraryCategoryViewport).Properties.IsLeftButtonPressed) { return; } _isComponentLibraryCategoryGestureActive = true; _componentLibraryCategoryGestureStartPoint = e.GetPosition(ComponentLibraryCategoryViewport); _componentLibraryCategoryGestureCurrentPoint = _componentLibraryCategoryGestureStartPoint; _componentLibraryCategoryGestureBaseOffset = -_componentLibraryCategoryIndex * _componentLibraryCategoryPageWidth; e.Pointer.Capture(ComponentLibraryCategoryViewport); } private void OnComponentLibraryCategoryViewportPointerMoved(object? sender, PointerEventArgs e) { if (!_isComponentLibraryCategoryGestureActive || ComponentLibraryCategoryViewport is null || _componentLibraryCategoryHostTransform is null) { return; } _componentLibraryCategoryGestureCurrentPoint = e.GetPosition(ComponentLibraryCategoryViewport); var deltaX = _componentLibraryCategoryGestureCurrentPoint.X - _componentLibraryCategoryGestureStartPoint.X; var minOffset = -Math.Max(0, _componentLibraryCategories.Count - 1) * _componentLibraryCategoryPageWidth; var tentative = _componentLibraryCategoryGestureBaseOffset + deltaX; _componentLibraryCategoryHostTransform.X = Math.Clamp(tentative, minOffset, 0); } private void OnComponentLibraryCategoryViewportPointerReleased(object? sender, PointerReleasedEventArgs e) { if (!_isComponentLibraryCategoryGestureActive || ComponentLibraryCategoryViewport is null) { return; } _isComponentLibraryCategoryGestureActive = false; e.Pointer.Capture(null); var endPoint = e.GetPosition(ComponentLibraryCategoryViewport); var deltaX = endPoint.X - _componentLibraryCategoryGestureStartPoint.X; var deltaY = endPoint.Y - _componentLibraryCategoryGestureStartPoint.Y; var tapThreshold = 6; if (Math.Abs(deltaX) <= tapThreshold && Math.Abs(deltaY) <= tapThreshold) { OpenComponentLibraryCurrentCategory(); return; } var swipeThreshold = Math.Max(40, _componentLibraryCategoryPageWidth * 0.18); if (deltaX <= -swipeThreshold) { _componentLibraryCategoryIndex = Math.Min(_componentLibraryCategoryIndex + 1, Math.Max(0, _componentLibraryCategories.Count - 1)); } else if (deltaX >= swipeThreshold) { _componentLibraryCategoryIndex = Math.Max(_componentLibraryCategoryIndex - 1, 0); } _componentLibraryActiveCategoryId = _componentLibraryCategories.Count > 0 ? _componentLibraryCategories[_componentLibraryCategoryIndex].Id : null; ApplyComponentLibraryCategoryOffset(); } private void OnComponentLibraryCategoryViewportPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) { if (!_isComponentLibraryCategoryGestureActive) { return; } _isComponentLibraryCategoryGestureActive = false; ApplyComponentLibraryCategoryOffset(); } private void OnComponentLibraryComponentViewportPointerPressed(object? sender, PointerPressedEventArgs e) { if (!_isComponentLibraryOpen || _componentLibraryActiveComponents.Count <= 1 || ComponentLibraryComponentViewport is null || _componentLibraryComponentHostTransform is null || !e.GetCurrentPoint(ComponentLibraryComponentViewport).Properties.IsLeftButtonPressed) { return; } _isComponentLibraryComponentGestureActive = true; _componentLibraryComponentGestureStartPoint = e.GetPosition(ComponentLibraryComponentViewport); _componentLibraryComponentGestureCurrentPoint = _componentLibraryComponentGestureStartPoint; _componentLibraryComponentGestureBaseOffset = -_componentLibraryComponentIndex * _componentLibraryComponentPageWidth; e.Pointer.Capture(ComponentLibraryComponentViewport); } private void OnComponentLibraryComponentViewportPointerMoved(object? sender, PointerEventArgs e) { if (!_isComponentLibraryComponentGestureActive || ComponentLibraryComponentViewport is null || _componentLibraryComponentHostTransform is null) { return; } _componentLibraryComponentGestureCurrentPoint = e.GetPosition(ComponentLibraryComponentViewport); var deltaX = _componentLibraryComponentGestureCurrentPoint.X - _componentLibraryComponentGestureStartPoint.X; var minOffset = -Math.Max(0, _componentLibraryActiveComponents.Count - 1) * _componentLibraryComponentPageWidth; var tentative = _componentLibraryComponentGestureBaseOffset + deltaX; _componentLibraryComponentHostTransform.X = Math.Clamp(tentative, minOffset, 0); } private void SaveComponentLibraryWindowPosition() { if (ComponentLibraryWindow is null) { return; } var margin = ComponentLibraryWindow.Margin; _savedComponentLibraryMargin = margin; _isComponentLibraryWindowPositionCustomized = true; SyncComponentLibraryCollapseExpandedState(); } private void RestoreComponentLibraryWindowPosition() { if (ComponentLibraryWindow is null) { return; } ComponentLibraryWindow.Margin = _savedComponentLibraryMargin; SyncComponentLibraryCollapseExpandedState(); } private Thickness _savedComponentLibraryMargin = new Thickness(24, 24, 24, 100); private void OnComponentLibraryComponentViewportPointerReleased(object? sender, PointerReleasedEventArgs e) { if (!_isComponentLibraryComponentGestureActive || ComponentLibraryComponentViewport is null) { return; } _isComponentLibraryComponentGestureActive = false; e.Pointer.Capture(null); var endPoint = e.GetPosition(ComponentLibraryComponentViewport); var deltaX = endPoint.X - _componentLibraryComponentGestureStartPoint.X; var swipeThreshold = Math.Max(40, _componentLibraryComponentPageWidth * 0.18); if (deltaX <= -swipeThreshold) { _componentLibraryComponentIndex = Math.Min(_componentLibraryComponentIndex + 1, Math.Max(0, _componentLibraryActiveComponents.Count - 1)); } else if (deltaX >= swipeThreshold) { _componentLibraryComponentIndex = Math.Max(_componentLibraryComponentIndex - 1, 0); } ApplyComponentLibraryComponentOffset(); } private void OnComponentLibraryComponentViewportPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) { if (!_isComponentLibraryComponentGestureActive) { return; } _isComponentLibraryComponentGestureActive = false; ApplyComponentLibraryComponentOffset(); } internal void SaveAllWhiteboardNotes() { foreach (var pageGrid in _desktopPageComponentGrids.Values) { foreach (var host in pageGrid.Children.OfType()) { var contentHost = TryGetContentHost(host); if (contentHost?.Child is WhiteboardWidget whiteboard) { whiteboard.ForceSaveNote(); } } } } }