Files
LanMountainDesktop/LanMontainDesktop/Views/MainWindow.ComponentSystem.cs

2602 lines
91 KiB
C#
Raw Normal View History

2026-03-02 20:02:14 +08:00
using System;
2026-02-28 12:30:16 +08:00
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
2026-03-02 20:02:14 +08:00
using Avalonia.Controls.Shapes;
2026-03-01 16:50:06 +08:00
using Avalonia.Input;
2026-02-28 12:30:16 +08:00
using Avalonia.Interactivity;
2026-03-01 00:34:07 +08:00
using Avalonia.Layout;
2026-02-28 12:30:16 +08:00
using Avalonia.Media;
using Avalonia.Threading;
2026-03-02 20:02:14 +08:00
using Avalonia.VisualTree;
2026-03-01 16:50:06 +08:00
using FluentIcons.Avalonia;
using FluentIcons.Common;
2026-02-28 12:30:16 +08:00
using LanMontainDesktop.ComponentSystem;
using LanMontainDesktop.Models;
2026-03-01 16:50:06 +08:00
using LanMontainDesktop.Views.Components;
2026-02-28 12:30:16 +08:00
namespace LanMontainDesktop.Views;
public partial class MainWindow
{
2026-03-01 16:50:06 +08:00
private readonly List<DesktopComponentPlacementSnapshot> _desktopComponentPlacements = [];
private readonly Dictionary<int, Grid> _desktopPageComponentGrids = new();
private const string DesktopComponentClass = "desktop-component";
private const string DesktopComponentHostClass = "desktop-component-host";
2026-03-02 20:02:14 +08:00
private const string DesktopComponentContentHostTag = "desktop-component-content-host";
private const string DesktopComponentResizeHandleTag = "desktop-component-resize-handle";
2026-03-01 16:50:06 +08:00
private bool _isDesktopComponentDragActive;
private DesktopComponentDragState? _desktopComponentDrag;
private Border? _desktopComponentDragGhost;
2026-03-02 20:02:14 +08:00
private bool _isDesktopComponentResizeActive;
private DesktopComponentResizeState? _desktopComponentResize;
2026-03-01 16:50:06 +08:00
private string? _componentLibraryActiveCategoryId;
private int _componentLibraryCategoryIndex;
private int _componentLibraryComponentIndex;
private double _componentLibraryCategoryPageWidth;
private double _componentLibraryComponentPageWidth;
private TranslateTransform? _componentLibraryCategoryHostTransform;
private TranslateTransform? _componentLibraryComponentHostTransform;
private IReadOnlyList<ComponentLibraryCategory> _componentLibraryCategories = Array.Empty<ComponentLibraryCategory>();
private IReadOnlyList<DesktopComponentDefinition> _componentLibraryActiveComponents = Array.Empty<DesktopComponentDefinition>();
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 enum DesktopComponentDragKind
{
None,
NewFromLibrary,
MoveExisting
}
private sealed class DesktopComponentDragState
{
public DesktopComponentDragKind Kind { get; init; }
public string ComponentId { get; init; } = string.Empty;
public string PlacementId { get; init; } = string.Empty;
public int PageIndex { get; init; }
public int WidthCells { get; init; }
public int HeightCells { get; init; }
public Point PointerOffset { get; init; }
public Border? SourceHost { get; init; }
public int TargetRow { get; set; }
public int TargetColumn { get; set; }
}
2026-03-02 20:02:14 +08:00
private sealed class DesktopComponentResizeState
{
public string PlacementId { get; init; } = string.Empty;
public string ComponentId { get; init; } = string.Empty;
public Border SourceHost { get; init; } = null!;
public int StartWidthCells { get; init; }
public int StartHeightCells { get; init; }
public int MinWidthCells { get; init; }
public int MinHeightCells { get; init; }
public int MaxWidthCells { get; init; }
public int MaxHeightCells { get; init; }
public Point StartPointerInViewport { get; init; }
public int CurrentWidthCells { get; set; }
public int CurrentHeightCells { get; set; }
}
2026-03-01 16:50:06 +08:00
private sealed record ComponentLibraryCategory(
string Id,
Symbol Icon,
string Title,
IReadOnlyList<DesktopComponentDefinition> Components);
2026-02-28 12:30:16 +08:00
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
{
2026-03-01 16:50:06 +08:00
// "Desktop edit" toggle. While editing, show the component library window.
2026-02-28 12:30:16 +08:00
if (_isComponentLibraryOpen)
{
2026-03-01 16:50:06 +08:00
CloseComponentLibraryWindow(reopenSettings: false);
2026-02-28 12:30:16 +08:00
return;
}
_reopenSettingsAfterComponentLibraryClose = _isSettingsOpen;
if (_isSettingsOpen)
{
CloseSettingsPage(immediate: true);
}
OpenComponentLibraryWindow();
}
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
{
CloseComponentLibraryWindow(reopenSettings: true);
}
2026-03-02 20:02:14 +08:00
private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e)
{
CloseComponentSettingsWindow();
}
2026-02-28 12:30:16 +08:00
private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Add(BuiltInComponentIds.Clock);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void OnStatusBarClockUnchecked(object? sender, RoutedEventArgs e)
{
if (_suppressStatusBarToggleEvents)
{
return;
}
_topStatusComponentIds.Remove(BuiltInComponentIds.Clock);
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
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<TaskbarActionId>(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;
2026-03-02 20:02:14 +08:00
_clockDisplayFormat = snapshot.ClockDisplayFormat == "HourMinute"
? ClockDisplayFormat.HourMinute
: ClockDisplayFormat.HourMinuteSecond;
if (ClockWidget is not null)
{
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
}
if (_clockDisplayFormat == ClockDisplayFormat.HourMinute)
{
if (ClockFormatHMRadio is not null)
{
ClockFormatHMRadio.IsChecked = true;
}
}
else
{
if (ClockFormatHMSSRadio is not null)
{
ClockFormatHMSSRadio.IsChecked = true;
}
}
2026-02-28 12:30:16 +08:00
}
private void ApplyTopStatusComponentVisibility()
{
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
if (ClockWidget is not null)
{
ClockWidget.IsVisible = showClock;
2026-03-02 20:02:14 +08:00
if (showClock)
{
ClockWidget.SetDisplayFormat(_clockDisplayFormat);
var columnSpan = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? 2 : 3;
Grid.SetColumnSpan(ClockWidget, columnSpan);
}
2026-02-28 12:30:16 +08:00
}
2026-03-02 20:02:14 +08:00
if (WallpaperPreviewClockWidget is not null)
2026-02-28 12:30:16 +08:00
{
2026-03-02 20:02:14 +08:00
WallpaperPreviewClockWidget.IsVisible = showClock;
if (showClock)
{
WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat);
}
2026-02-28 12:30:16 +08:00
}
}
private TaskbarContext GetCurrentTaskbarContext()
{
if (!_isSettingsOpen)
{
return TaskbarContext.Desktop;
}
return SettingsNavListBox?.SelectedIndex switch
{
0 => TaskbarContext.SettingsWallpaper,
1 => TaskbarContext.SettingsGrid,
2 => TaskbarContext.SettingsColor,
3 => TaskbarContext.SettingsStatusBar,
4 => TaskbarContext.SettingsRegion,
_ => TaskbarContext.Desktop
};
}
private void ApplyTaskbarActionVisibility(TaskbarContext context)
{
if (BackToWindowsButton is null ||
OpenComponentLibraryButton is null ||
OpenSettingsButton is null ||
WallpaperPreviewBackButtonVisual is null ||
WallpaperPreviewComponentLibraryVisual is null ||
WallpaperPreviewSettingsButtonIcon is null)
{
return;
}
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings);
2026-03-02 20:02:14 +08:00
var showDesktopEdit = _isSettingsOpen;
2026-02-28 12:30:16 +08:00
BackToWindowsButton.IsVisible = showMinimize;
2026-03-01 16:50:06 +08:00
OpenComponentLibraryButton.IsVisible = showDesktopEdit;
2026-02-28 12:30:16 +08:00
OpenSettingsButton.IsVisible = showSettings;
WallpaperPreviewBackButtonVisual.IsVisible = showMinimize;
2026-03-01 16:50:06 +08:00
WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit;
2026-02-28 12:30:16 +08:00
WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings;
if (TaskbarFixedActionsHost is not null)
{
TaskbarFixedActionsHost.IsVisible = showMinimize;
}
if (TaskbarSettingsActionHost is not null)
{
2026-03-01 16:50:06 +08:00
TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit;
2026-02-28 12:30:16 +08:00
}
if (WallpaperPreviewTaskbarFixedActionsHost is not null)
{
WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize;
}
if (WallpaperPreviewTaskbarSettingsActionHost is not null)
{
2026-03-01 16:50:06 +08:00
WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit;
2026-02-28 12:30:16 +08:00
}
2026-03-01 16:50:06 +08:00
var dynamicActions = ResolveDynamicTaskbarActions(context)
.Where(action => action.IsVisible)
.ToList();
2026-02-28 12:30:16 +08:00
var hasDynamicActions = dynamicActions.Count > 0;
2026-03-02 20:02:14 +08:00
BuildDynamicTaskbarVisuals(dynamicActions, _currentDesktopCellSize);
2026-02-28 12:30:16 +08:00
if (TaskbarDynamicActionsHost is not null)
{
TaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
{
WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions;
}
UpdateOpenSettingsActionVisualState();
}
private void UpdateOpenSettingsActionVisualState()
{
if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null)
{
return;
}
var showBackToDesktop = _isSettingsOpen;
OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop;
OpenSettingsButtonTextBlock.Text = L("settings.back_to_desktop", "Back to Desktop");
ToolTip.SetTip(
OpenSettingsButton,
showBackToDesktop
? L("settings.back_to_desktop", "Back to Desktop")
: L("tooltip.open_settings", "Settings"));
var effectiveCellSize = _currentDesktopCellSize > 0
? _currentDesktopCellSize
: Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells));
ApplyWidgetSizing(effectiveCellSize);
}
private void OpenComponentLibraryWindow()
{
if (ComponentLibraryWindow is null)
{
return;
}
_isComponentLibraryOpen = true;
2026-03-01 16:50:06 +08:00
UpdateDesktopComponentHostEditState();
ShowComponentLibraryCategoryView();
2026-02-28 12:30:16 +08:00
ComponentLibraryWindow.IsVisible = true;
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
2026-03-02 20:02:14 +08:00
RestoreComponentLibraryWindowPosition();
2026-02-28 12:30:16 +08:00
Dispatcher.UIThread.Post(() =>
{
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
2026-03-01 16:50:06 +08:00
BuildComponentLibraryCategoryPages();
2026-02-28 12:30:16 +08:00
ComponentLibraryWindow.Opacity = 1;
}, DispatcherPriority.Background);
}
private void CloseComponentLibraryWindow(bool reopenSettings)
{
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
_isComponentLibraryOpen = false;
2026-03-01 16:50:06 +08:00
CancelDesktopComponentDrag();
2026-03-02 20:02:14 +08:00
CancelDesktopComponentResize(restoreOriginalSpan: true);
ClearDesktopComponentSelection();
2026-03-01 16:50:06 +08:00
UpdateDesktopComponentHostEditState();
2026-02-28 12:30:16 +08:00
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
DispatcherTimer.RunOnce(() =>
{
if (_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
ComponentLibraryWindow.IsVisible = false;
var shouldReopenSettings = reopenSettings && _reopenSettingsAfterComponentLibraryClose;
_reopenSettingsAfterComponentLibraryClose = false;
if (shouldReopenSettings)
{
OpenSettingsPage();
}
}, TimeSpan.FromMilliseconds(200));
}
2026-03-01 16:50:06 +08:00
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);
}
2026-02-28 12:30:16 +08:00
private IReadOnlyList<TaskbarActionItem> ResolveDynamicTaskbarActions(TaskbarContext context)
{
2026-03-01 16:50:06 +08:00
if (context == TaskbarContext.Desktop && _isComponentLibraryOpen)
{
2026-03-02 20:02:14 +08:00
var actions = new List<TaskbarActionItem>();
if (_selectedDesktopComponentHost is not null)
{
actions.Add(new TaskbarActionItem(
TaskbarActionId.DeleteComponent,
L("component.delete", "Delete"),
"Delete",
IsVisible: true,
CommandKey: "component.delete"));
actions.Add(new TaskbarActionItem(
TaskbarActionId.EditComponent,
L("component.edit", "Edit"),
"Edit",
IsVisible: true,
CommandKey: "component.edit"));
return actions;
}
2026-03-01 16:50:06 +08:00
var canAddPage = _desktopPageCount < MaxDesktopPageCount;
2026-03-02 20:02:14 +08:00
var canDeletePage = _desktopPageCount > MinDesktopPageCount;
if (canAddPage)
{
actions.Add(new TaskbarActionItem(
2026-03-01 16:50:06 +08:00
TaskbarActionId.AddDesktopPage,
L("desktop.add_page", "Add page"),
"Add",
2026-03-02 20:02:14 +08:00
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;
2026-03-01 16:50:06 +08:00
}
2026-02-28 12:30:16 +08:00
if (!_enableDynamicTaskbarActions)
{
return Array.Empty<TaskbarActionItem>();
}
_ = context;
return Array.Empty<TaskbarActionItem>();
}
2026-03-02 20:02:14 +08:00
private void BuildDynamicTaskbarVisuals(IReadOnlyList<TaskbarActionItem> actions, double cellSize)
2026-02-28 12:30:16 +08:00
{
if (TaskbarDynamicActionsPanel is not null)
{
TaskbarDynamicActionsPanel.Children.Clear();
}
2026-03-01 00:34:07 +08:00
if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
2026-02-28 12:30:16 +08:00
{
2026-03-01 00:34:07 +08:00
WallpaperPreviewTaskbarDynamicActionsHost.Children.Clear();
2026-02-28 12:30:16 +08:00
}
if (actions.Count == 0 ||
TaskbarDynamicActionsPanel is null ||
2026-03-01 00:34:07 +08:00
WallpaperPreviewTaskbarDynamicActionsHost is null)
2026-02-28 12:30:16 +08:00
{
return;
}
2026-03-02 20:02:14 +08:00
// 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);
2026-02-28 12:30:16 +08:00
foreach (var action in actions)
{
if (!action.IsVisible)
{
continue;
}
2026-03-02 20:02:14 +08:00
var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage ||
action.Id == TaskbarActionId.DeleteComponent;
var isEditAction = action.Id == TaskbarActionId.EditComponent;
Symbol iconSymbol;
if (isDeleteAction)
{
iconSymbol = Symbol.Delete;
}
else if (isEditAction)
{
iconSymbol = Symbol.Edit;
}
else
{
iconSymbol = 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
}
}
};
2026-02-28 12:30:16 +08:00
var button = new Button
{
2026-03-02 20:02:14 +08:00
Content = buttonContent,
2026-02-28 12:30:16 +08:00
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
2026-03-02 20:02:14 +08:00
Padding = new Thickness(padding),
Foreground = isDeleteAction
? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
: Foreground,
2026-03-01 16:50:06 +08:00
Tag = action.CommandKey
2026-02-28 12:30:16 +08:00
};
2026-03-01 16:50:06 +08:00
button.Click += OnDynamicTaskbarActionClick;
2026-02-28 12:30:16 +08:00
TaskbarDynamicActionsPanel.Children.Add(button);
2026-03-02 20:02:14 +08:00
Control previewIcon = new SymbolIcon
{
Symbol = iconSymbol,
IconVariant = IconVariant.Regular,
FontSize = iconSize * 0.85
};
2026-02-28 12:30:16 +08:00
var previewText = new TextBlock
{
Text = action.Title,
2026-03-02 20:02:14 +08:00
FontSize = fontSize * 0.85,
Foreground = isDeleteAction
? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
: Foreground,
2026-02-28 12:30:16 +08:00
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
2026-03-02 20:02:14 +08:00
var previewContent = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = spacing * 0.5,
Children = { previewIcon, previewText }
};
2026-02-28 12:30:16 +08:00
var previewBorder = new Border
{
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
2026-03-02 20:02:14 +08:00
Child = previewContent
2026-02-28 12:30:16 +08:00
};
2026-03-01 00:34:07 +08:00
WallpaperPreviewTaskbarDynamicActionsHost.Children.Add(previewBorder);
}
}
2026-03-01 16:50:06 +08:00
private void OnDynamicTaskbarActionClick(object? sender, RoutedEventArgs e)
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
if (sender is not Button button || button.Tag is not string commandKey)
2026-03-01 00:34:07 +08:00
{
return;
}
2026-03-01 16:50:06 +08:00
switch (commandKey)
{
case "desktop.add_page":
AddDesktopPage();
break;
2026-03-02 20:02:14 +08:00
case "desktop.delete_page":
DeleteCurrentDesktopPage();
break;
case "component.delete":
DeleteSelectedComponent();
break;
case "component.edit":
OpenComponentSettings();
break;
2026-03-01 16:50:06 +08:00
}
}
2026-03-01 00:34:07 +08:00
2026-03-02 20:02:14 +08:00
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;
}
// 娴犲海缍夐弽闂磋厬缁夊娅庣紒鍕
if (_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
{
pageGrid.Children.Remove(_selectedDesktopComponentHost);
}
// Remove from persisted placement list as well.
_desktopComponentPlacements.Remove(placement);
ClearDesktopComponentSelection();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
// 娣囨繂鐡ㄧ拋鍓х枂
PersistSettings();
}
private void OpenComponentSettings()
{
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;
}
if (placement.ComponentId == BuiltInComponentIds.Date)
{
OpenDateComponentSettings();
}
}
private void OpenDateComponentSettings()
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
{
return;
}
var settingsContent = new DateWidgetSettingsWindow();
ComponentSettingsContentHost.Content = settingsContent;
ComponentSettingsWindow.IsVisible = true;
ComponentSettingsWindow.Opacity = 0;
ComponentSettingsWindow.Opacity = 1;
}
private void CloseComponentSettingsWindow()
{
if (ComponentSettingsWindow is null)
{
return;
}
ComponentSettingsWindow.Opacity = 0;
DispatcherTimer.RunOnce(() =>
{
if (ComponentSettingsWindow is not null)
{
ComponentSettingsWindow.IsVisible = false;
}
if (ComponentSettingsContentHost is not null)
{
ComponentSettingsContentHost.Content = null;
}
}, TimeSpan.FromMilliseconds(200));
}
2026-03-01 16:50:06 +08:00
private void AddDesktopPage()
{
if (_desktopPageCount >= MaxDesktopPageCount)
{
return;
}
_desktopPageCount = Math.Clamp(_desktopPageCount + 1, MinDesktopPageCount, MaxDesktopPageCount);
_currentDesktopSurfaceIndex = Math.Clamp(_desktopPageCount - 1, 0, LauncherSurfaceIndex);
RebuildDesktopGrid();
PersistSettings();
2026-03-02 20:02:14 +08:00
// 閺囧瓨鏌婇崝銊︹偓浣锋崲閸斺剝鐖弰鍓с仛
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private void DeleteCurrentDesktopPage()
{
if (_desktopPageCount <= MinDesktopPageCount)
{
return;
}
var placementsToRemove = _desktopComponentPlacements
.Where(p => p.PageIndex == _currentDesktopSurfaceIndex)
.ToList();
foreach (var placement in placementsToRemove)
{
_desktopComponentPlacements.Remove(placement);
}
_desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount);
// 鐠嬪啯鏆hぐ鎾冲妞ょ敻娼扮槐銏犵穿
_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();
// 閺囧瓨鏌婇崝銊︹偓浣锋崲閸斺剝鐖弰鍓с仛
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
2026-03-01 16:50:06 +08:00
}
private void InitializeDesktopComponentPlacements(AppSettingsSnapshot snapshot)
{
_desktopComponentPlacements.Clear();
if (snapshot.DesktopComponentPlacements is null)
{
return;
}
2026-03-01 00:34:07 +08:00
2026-03-01 16:50:06 +08:00
foreach (var placement in snapshot.DesktopComponentPlacements)
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
if (placement is null || string.IsNullOrWhiteSpace(placement.ComponentId))
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
continue;
}
2026-03-01 00:34:07 +08:00
2026-03-01 16:50:06 +08:00
var placementId = string.IsNullOrWhiteSpace(placement.PlacementId)
? Guid.NewGuid().ToString("N")
: placement.PlacementId.Trim();
var componentId = placement.ComponentId.Trim();
if (!_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement)
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
continue;
}
2026-03-02 20:02:14 +08:00
var (widthCells, heightCells) = NormalizeComponentCellSpan(
componentId,
ComponentPlacementRules.EnsureMinimumSize(
definition,
placement.WidthCells,
placement.HeightCells));
2026-03-01 00:34:07 +08:00
2026-03-01 16:50:06 +08:00
_desktopComponentPlacements.Add(new DesktopComponentPlacementSnapshot
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
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;
}
pageGrid.Children.Clear();
var maxColumns = pageGrid.ColumnDefinitions.Count;
var maxRows = pageGrid.RowDefinitions.Count;
if (maxColumns <= 0 || maxRows <= 0)
{
return;
}
2026-03-01 00:34:07 +08:00
2026-03-01 16:50:06 +08:00
foreach (var placement in _desktopComponentPlacements.Where(p => p.PageIndex == pageIndex))
{
if (!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition) || !definition.AllowDesktopPlacement)
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
continue;
}
2026-03-01 00:34:07 +08:00
2026-03-02 20:02:14 +08:00
var (widthCells, heightCells) = NormalizeComponentCellSpan(
placement.ComponentId,
ComponentPlacementRules.EnsureMinimumSize(
definition,
placement.WidthCells,
placement.HeightCells));
2026-03-01 16:50:06 +08:00
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)
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
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);
}
}
private void PlaceDesktopComponentOnPage(string componentId, int pageIndex, int row, int column)
{
if (!_desktopPageComponentGrids.TryGetValue(pageIndex, out var pageGrid))
{
return;
}
if (!_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement)
{
return;
}
2026-03-02 20:02:14 +08:00
var (widthCells, heightCells) = NormalizeComponentCellSpan(
componentId,
ComponentPlacementRules.EnsureMinimumSize(
definition,
definition.MinWidthCells,
definition.MinHeightCells));
2026-03-01 16:50:06 +08:00
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);
PersistSettings();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private Border? CreateDesktopComponentHost(DesktopComponentPlacementSnapshot placement)
{
if (string.IsNullOrWhiteSpace(placement.PlacementId))
{
placement.PlacementId = Guid.NewGuid().ToString("N");
}
var component = CreateDesktopComponentControl(placement.ComponentId);
if (component is null)
{
return null;
}
2026-03-02 20:02:14 +08:00
var componentCornerRadius = GetComponentCornerRadius(placement.ComponentId);
var visualInset = GetDesktopComponentVisualInset(
Math.Max(1, placement.WidthCells),
Math.Max(1, placement.HeightCells));
var contentHost = new Border
2026-03-01 16:50:06 +08:00
{
2026-03-02 20:02:14 +08:00
Tag = DesktopComponentContentHostTag,
2026-03-01 16:50:06 +08:00
Background = Brushes.Transparent,
2026-03-02 20:02:14 +08:00
CornerRadius = new CornerRadius(componentCornerRadius),
2026-03-01 16:50:06 +08:00
ClipToBounds = true,
2026-03-02 20:02:14 +08:00
Padding = visualInset,
2026-03-01 16:50:06 +08:00
Child = component
};
2026-03-02 20:02:14 +08:00
// 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 Path
{
Data = arcData,
Stretch = Stretch.Fill,
Stroke = GetThemeBrush("AdaptiveTextAccentBrush"),
StrokeThickness = arcThickness + 3,
StrokeLineCap = PenLineCap.Round
});
resizeHandleVisual.Children.Add(new Path
{
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
};
2026-03-01 16:50:06 +08:00
host.Classes.Add(DesktopComponentHostClass);
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
host.PointerPressed += OnDesktopComponentHostPointerPressed;
return host;
}
2026-03-02 20:02:14 +08:00
private static (int WidthCells, int HeightCells) NormalizeComponentCellSpan(
string componentId,
(int WidthCells, int HeightCells) span)
{
if (string.Equals(componentId, BuiltInComponentIds.Date, StringComparison.OrdinalIgnoreCase))
{
return (Math.Max(4, span.WidthCells), Math.Max(2, span.HeightCells));
}
if (string.Equals(componentId, BuiltInComponentIds.MonthCalendar, StringComparison.OrdinalIgnoreCase))
{
return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
}
if (string.Equals(componentId, BuiltInComponentIds.LunarCalendar, StringComparison.OrdinalIgnoreCase))
{
return (Math.Max(2, span.WidthCells), Math.Max(2, span.HeightCells));
}
return (Math.Max(1, span.WidthCells), Math.Max(1, span.HeightCells));
}
private double GetComponentCornerRadius(string componentId)
{
return componentId switch
{
BuiltInComponentIds.Date => 16,
BuiltInComponentIds.MonthCalendar => Math.Clamp(_currentDesktopCellSize * 0.26, 10, 22),
BuiltInComponentIds.LunarCalendar => Math.Clamp(_currentDesktopCellSize * 0.30, 12, 26),
_ => Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)
};
}
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<Border>()
.FirstOrDefault(child =>
string.Equals(child.Tag?.ToString(), DesktopComponentContentHostTag, StringComparison.Ordinal));
}
return null;
}
private static Border? TryGetResizeHandle(Border host)
{
if (host.Child is Grid hostChrome)
{
return hostChrome.Children
.OfType<Border>()
.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;
}
2026-03-01 16:50:06 +08:00
private Control? CreateDesktopComponentControl(string componentId)
{
if (componentId == BuiltInComponentIds.Date)
{
var widget = new DateWidget();
widget.SetTimeZoneService(_timeZoneService);
widget.ApplyCellSize(_currentDesktopCellSize);
widget.Classes.Add(DesktopComponentClass);
return widget;
}
2026-03-02 20:02:14 +08:00
if (componentId == BuiltInComponentIds.MonthCalendar)
{
var widget = new MonthCalendarWidget();
widget.SetTimeZoneService(_timeZoneService);
widget.ApplyCellSize(_currentDesktopCellSize);
widget.Classes.Add(DesktopComponentClass);
return widget;
}
if (componentId == BuiltInComponentIds.LunarCalendar)
{
var widget = new LunarCalendarWidget();
widget.SetTimeZoneService(_timeZoneService);
widget.ApplyCellSize(_currentDesktopCellSize);
widget.Classes.Add(DesktopComponentClass);
return widget;
}
2026-03-01 16:50:06 +08:00
return null;
}
private void CollapseComponentLibraryPanel()
{
// Animate component library panel collapsing downward
if (ComponentLibraryWindow is not null)
{
ComponentLibraryWindow.Height = 0;
ComponentLibraryWindow.IsVisible = false;
}
_isComponentLibraryOpen = false;
CancelDesktopComponentDrag();
2026-03-02 20:02:14 +08:00
CancelDesktopComponentResize(restoreOriginalSpan: true);
ClearDesktopComponentSelection();
2026-03-01 16:50:06 +08:00
UpdateDesktopComponentHostEditState();
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);
}
}
}
}
private void ApplyDesktopEditStateToHost(Border host, bool isEditMode)
{
2026-03-02 20:02:14 +08:00
host.IsHitTestVisible = true;
2026-03-01 16:50:06 +08:00
2026-03-02 20:02:14 +08:00
if (TryGetContentHost(host) is Border contentHost)
2026-03-01 16:50:06 +08:00
{
// In edit mode, prefer drag interactions over component interactions.
2026-03-02 20:02:14 +08:00
contentHost.IsHitTestVisible = !isEditMode;
if (contentHost.Child is Control componentControl)
{
componentControl.IsHitTestVisible = !isEditMode;
}
2026-03-01 16:50:06 +08:00
}
2026-03-02 20:02:14 +08:00
var isSelected = host == _selectedDesktopComponentHost;
ApplySelectionStateToHost(host, isSelected);
2026-03-01 16:50:06 +08:00
}
private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e)
{
2026-03-02 20:02:14 +08:00
if (!_isComponentLibraryOpen || _isDesktopComponentDragActive || _isDesktopComponentResizeActive)
2026-03-01 16:50:06 +08:00
{
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;
}
2026-03-02 20:02:14 +08:00
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 (_isDesktopComponentResizeActive)
{
e.Handled = true;
}
return;
}
2026-03-01 16:50:06 +08:00
BeginDesktopComponentMoveDrag(host, placement, e);
e.Handled = true;
}
2026-03-02 20:02:14 +08:00
private void SetSelectedDesktopComponent(Border? host)
{
// 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;
}
}
2026-03-01 16:50:06 +08:00
private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e)
{
2026-03-02 20:02:14 +08:00
if (_isDesktopComponentResizeActive ||
DesktopEditDragLayer is null ||
2026-03-01 16:50:06 +08:00
DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition))
{
return;
}
2026-03-02 20:02:14 +08:00
var (widthCells, heightCells) = NormalizeComponentCellSpan(
placement.ComponentId,
ComponentPlacementRules.EnsureMinimumSize(
definition,
placement.WidthCells,
placement.HeightCells));
2026-03-01 16:50:06 +08:00
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
2026-03-02 20:02:14 +08:00
var pitch = CurrentDesktopPitch;
var topLeft = new Point(placement.Column * pitch, placement.Row * pitch);
2026-03-01 16:50:06 +08:00
var pointerOffset = pointerInViewport - topLeft;
sourceHost.Opacity = 0.35;
_desktopComponentDrag = new DesktopComponentDragState
{
Kind = DesktopComponentDragKind.MoveExisting,
ComponentId = placement.ComponentId,
PlacementId = placement.PlacementId,
PageIndex = placement.PageIndex,
WidthCells = widthCells,
HeightCells = heightCells,
PointerOffset = pointerOffset,
SourceHost = sourceHost
};
_isDesktopComponentDragActive = true;
EnsureDesktopComponentDragGhost(placement.ComponentId, widthCells, heightCells);
UpdateDesktopComponentDragVisual(pointerInViewport);
e.Pointer.Capture(this);
}
private void BeginDesktopComponentNewDrag(string componentId, PointerPressedEventArgs e)
{
if (!_isComponentLibraryOpen ||
_isDesktopComponentDragActive ||
2026-03-02 20:02:14 +08:00
_isDesktopComponentResizeActive ||
2026-03-01 16:50:06 +08:00
DesktopEditDragLayer is null ||
DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
!_componentRegistry.TryGetDefinition(componentId, out var definition) ||
!definition.AllowDesktopPlacement)
{
return;
}
2026-03-02 20:02:14 +08:00
var (widthCells, heightCells) = NormalizeComponentCellSpan(
componentId,
ComponentPlacementRules.EnsureMinimumSize(
definition,
definition.MinWidthCells,
definition.MinHeightCells));
2026-03-01 16:50:06 +08:00
// Center the component under the pointer while dragging from the library.
2026-03-02 20:02:14 +08:00
var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize + Math.Max(0, widthCells - 1) * _currentDesktopCellGap);
var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize + Math.Max(0, heightCells - 1) * _currentDesktopCellGap);
2026-03-01 16:50:06 +08:00
var pointerOffset = new Point(
2026-03-02 20:02:14 +08:00
ghostWidth * 0.5,
ghostHeight * 0.5);
2026-03-01 16:50:06 +08:00
_desktopComponentDrag = new DesktopComponentDragState
{
Kind = DesktopComponentDragKind.NewFromLibrary,
ComponentId = componentId,
PageIndex = _currentDesktopSurfaceIndex,
WidthCells = widthCells,
HeightCells = heightCells,
PointerOffset = pointerOffset
};
_isDesktopComponentDragActive = true;
EnsureDesktopComponentDragGhost(componentId, widthCells, heightCells);
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
UpdateDesktopComponentDragVisual(pointerInViewport);
2026-03-01 00:34:07 +08:00
2026-03-01 16:50:06 +08:00
e.Pointer.Capture(this);
}
private void EnsureDesktopComponentDragGhost(string componentId, int widthCells, int heightCells)
{
if (DesktopEditDragLayer is null)
{
return;
}
DesktopEditDragLayer.Children.Clear();
2026-03-02 20:02:14 +08:00
var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize + Math.Max(0, widthCells - 1) * _currentDesktopCellGap);
var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize + Math.Max(0, heightCells - 1) * _currentDesktopCellGap);
2026-03-01 16:50:06 +08:00
var ghostContent = CreateDesktopComponentControl(componentId);
if (ghostContent is not null)
{
ghostContent.IsHitTestVisible = false;
}
2026-03-02 20:02:14 +08:00
var visualInset = GetDesktopComponentVisualInset(widthCells, heightCells);
2026-03-01 16:50:06 +08:00
_desktopComponentDragGhost = new Border
{
Width = ghostWidth,
Height = ghostHeight,
2026-03-02 20:02:14 +08:00
CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.45, 16, 36)),
2026-03-01 16:50:06 +08:00
Background = new SolidColorBrush(Color.Parse("#331E40AF")),
BorderBrush = GetThemeBrush("AdaptiveAccentBrush"),
BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)),
2026-03-02 20:02:14 +08:00
Padding = visualInset,
ClipToBounds = true,
2026-03-01 16:50:06 +08:00
Child = ghostContent,
Opacity = 0.92,
IsHitTestVisible = false
};
DesktopEditDragLayer.Children.Add(_desktopComponentDragGhost);
}
2026-03-02 20:02:14 +08:00
private void OnDesktopComponentResizeHandlePointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!_isComponentLibraryOpen ||
_isDesktopComponentDragActive ||
_isDesktopComponentResizeActive ||
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 (_isDesktopComponentResizeActive)
{
e.Handled = true;
}
}
private void BeginDesktopComponentResizeDrag(
Border sourceHost,
DesktopComponentPlacementSnapshot placement,
PointerPressedEventArgs e)
{
if (DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition) ||
!_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
{
return;
}
var startSpan = NormalizeComponentCellSpan(
placement.ComponentId,
ComponentPlacementRules.EnsureMinimumSize(
definition,
placement.WidthCells,
placement.HeightCells));
var minSpan = NormalizeComponentCellSpan(
placement.ComponentId,
ComponentPlacementRules.EnsureMinimumSize(
definition,
definition.MinWidthCells,
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);
_desktopComponentResize = new DesktopComponentResizeState
{
PlacementId = placement.PlacementId,
ComponentId = placement.ComponentId,
SourceHost = sourceHost,
StartWidthCells = startSpan.WidthCells,
StartHeightCells = startSpan.HeightCells,
MinWidthCells = Math.Max(1, Math.Min(minSpan.WidthCells, maxWidthCells)),
MinHeightCells = Math.Max(1, Math.Min(minSpan.HeightCells, maxHeightCells)),
MaxWidthCells = maxWidthCells,
MaxHeightCells = maxHeightCells,
StartPointerInViewport = pointerInViewport,
CurrentWidthCells = startSpan.WidthCells,
CurrentHeightCells = startSpan.HeightCells
};
_isDesktopComponentResizeActive = true;
sourceHost.Opacity = 0.96;
e.Pointer.Capture(this);
}
private void UpdateDesktopComponentResizeVisual(Point pointerInViewport)
{
if (_desktopComponentResize is null)
{
return;
}
var pitch = CurrentDesktopPitch;
if (pitch <= 0 ||
_desktopComponentResize.StartWidthCells <= 0 ||
_desktopComponentResize.StartHeightCells <= 0)
{
return;
}
var deltaX = pointerInViewport.X - _desktopComponentResize.StartPointerInViewport.X;
var deltaY = pointerInViewport.Y - _desktopComponentResize.StartPointerInViewport.Y;
var widthScale = (_desktopComponentResize.StartWidthCells + deltaX / pitch) / _desktopComponentResize.StartWidthCells;
var heightScale = (_desktopComponentResize.StartHeightCells + deltaY / pitch) / _desktopComponentResize.StartHeightCells;
var proposedScale = Math.Max(widthScale, heightScale);
var minScale = Math.Max(
(double)_desktopComponentResize.MinWidthCells / _desktopComponentResize.StartWidthCells,
(double)_desktopComponentResize.MinHeightCells / _desktopComponentResize.StartHeightCells);
var maxScale = Math.Min(
(double)_desktopComponentResize.MaxWidthCells / _desktopComponentResize.StartWidthCells,
(double)_desktopComponentResize.MaxHeightCells / _desktopComponentResize.StartHeightCells);
if (double.IsNaN(proposedScale) || double.IsInfinity(proposedScale))
{
proposedScale = minScale;
}
if (maxScale < minScale)
{
maxScale = minScale;
}
var scale = Math.Clamp(proposedScale, minScale, maxScale);
var widthCells = Math.Clamp(
(int)Math.Round(_desktopComponentResize.StartWidthCells * scale),
_desktopComponentResize.MinWidthCells,
_desktopComponentResize.MaxWidthCells);
var heightCells = Math.Clamp(
(int)Math.Round(_desktopComponentResize.StartHeightCells * scale),
_desktopComponentResize.MinHeightCells,
_desktopComponentResize.MaxHeightCells);
var normalized = NormalizeComponentCellSpan(_desktopComponentResize.ComponentId, (widthCells, heightCells));
widthCells = Math.Clamp(normalized.WidthCells, _desktopComponentResize.MinWidthCells, _desktopComponentResize.MaxWidthCells);
heightCells = Math.Clamp(normalized.HeightCells, _desktopComponentResize.MinHeightCells, _desktopComponentResize.MaxHeightCells);
_desktopComponentResize.CurrentWidthCells = widthCells;
_desktopComponentResize.CurrentHeightCells = heightCells;
Grid.SetColumnSpan(_desktopComponentResize.SourceHost, widthCells);
Grid.SetRowSpan(_desktopComponentResize.SourceHost, heightCells);
}
private bool TryCompleteDesktopComponentResize(Point pointerInViewport)
{
if (_desktopComponentResize is null)
{
return false;
}
UpdateDesktopComponentResizeVisual(pointerInViewport);
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
string.Equals(p.PlacementId, _desktopComponentResize.PlacementId, StringComparison.OrdinalIgnoreCase));
if (placement is null)
{
return false;
}
var widthCells = Math.Max(1, _desktopComponentResize.CurrentWidthCells);
var heightCells = Math.Max(1, _desktopComponentResize.CurrentHeightCells);
var changed = placement.WidthCells != widthCells || placement.HeightCells != heightCells;
placement.WidthCells = widthCells;
placement.HeightCells = heightCells;
ApplyDesktopEditStateToHost(_desktopComponentResize.SourceHost, _isComponentLibraryOpen);
if (changed)
{
PersistSettings();
}
return true;
}
private void CancelDesktopComponentResize(bool restoreOriginalSpan)
{
if (!_isDesktopComponentResizeActive || _desktopComponentResize is null)
{
return;
}
if (restoreOriginalSpan)
{
Grid.SetColumnSpan(_desktopComponentResize.SourceHost, _desktopComponentResize.StartWidthCells);
Grid.SetRowSpan(_desktopComponentResize.SourceHost, _desktopComponentResize.StartHeightCells);
}
_desktopComponentResize.SourceHost.Opacity = 1;
ApplyDesktopEditStateToHost(_desktopComponentResize.SourceHost, _isComponentLibraryOpen);
_desktopComponentResize = null;
_isDesktopComponentResizeActive = false;
}
2026-03-01 16:50:06 +08:00
private void OnDesktopComponentDragPointerMoved(object? sender, PointerEventArgs e)
{
2026-03-02 20:02:14 +08:00
if (DesktopPagesViewport is null)
{
return;
}
if (_isDesktopComponentResizeActive && _desktopComponentResize is not null)
{
UpdateDesktopComponentResizeVisual(e.GetPosition(DesktopPagesViewport));
return;
}
if (!_isDesktopComponentDragActive || _desktopComponentDrag is null)
2026-03-01 16:50:06 +08:00
{
return;
}
UpdateDesktopComponentDragVisual(e.GetPosition(DesktopPagesViewport));
}
private void OnDesktopComponentDragPointerReleased(object? sender, PointerReleasedEventArgs e)
{
2026-03-02 20:02:14 +08:00
if (DesktopPagesViewport is null)
{
return;
}
if (_isDesktopComponentResizeActive && _desktopComponentResize is not null)
{
var resizePointerInViewport = e.GetPosition(DesktopPagesViewport);
var resizeSuccess = TryCompleteDesktopComponentResize(resizePointerInViewport);
CancelDesktopComponentResize(restoreOriginalSpan: !resizeSuccess);
e.Pointer.Capture(null);
if (resizeSuccess)
{
e.Handled = true;
}
return;
}
if (!_isDesktopComponentDragActive || _desktopComponentDrag is null)
2026-03-01 16:50:06 +08:00
{
return;
}
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
var success = TryCompleteDesktopComponentDrag(pointerInViewport);
CancelDesktopComponentDrag();
e.Pointer.Capture(null);
if (success)
{
e.Handled = true;
}
}
private void OnDesktopComponentDragPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
2026-03-02 20:02:14 +08:00
if (_isDesktopComponentResizeActive)
{
CancelDesktopComponentResize(restoreOriginalSpan: true);
return;
}
2026-03-01 16:50:06 +08:00
if (!_isDesktopComponentDragActive)
{
return;
}
CancelDesktopComponentDrag();
}
private void UpdateDesktopComponentDragVisual(Point pointerInViewport)
{
if (_desktopComponentDragGhost is null || _desktopComponentDrag is null || DesktopPagesViewport is null)
{
return;
}
var withinViewport =
pointerInViewport.X >= 0 &&
pointerInViewport.Y >= 0 &&
pointerInViewport.X <= DesktopPagesViewport.Bounds.Width &&
pointerInViewport.Y <= DesktopPagesViewport.Bounds.Height;
if (!withinViewport ||
!TryGetDesktopComponentDropCell(pointerInViewport, _desktopComponentDrag, out var row, out var column))
{
_desktopComponentDragGhost.IsVisible = false;
return;
2026-03-01 00:34:07 +08:00
}
2026-03-01 16:50:06 +08:00
_desktopComponentDragGhost.IsVisible = true;
_desktopComponentDrag.TargetRow = row;
_desktopComponentDrag.TargetColumn = column;
2026-03-02 20:02:14 +08:00
var pitch = CurrentDesktopPitch;
Canvas.SetLeft(_desktopComponentDragGhost, column * pitch);
Canvas.SetTop(_desktopComponentDragGhost, row * pitch);
2026-03-01 16:50:06 +08:00
}
private bool TryGetDesktopComponentDropCell(
Point pointerInViewport,
DesktopComponentDragState state,
out int row,
out int column)
{
row = 0;
column = 0;
if (_currentDesktopCellSize <= 0 ||
_currentDesktopSurfaceIndex < 0 ||
_currentDesktopSurfaceIndex >= _desktopPageCount ||
!_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid))
{
return false;
}
var maxColumns = pageGrid.ColumnDefinitions.Count;
var maxRows = pageGrid.RowDefinitions.Count;
if (maxColumns <= 0 || maxRows <= 0)
{
return false;
}
2026-03-02 20:02:14 +08:00
var pitch = CurrentDesktopPitch;
if (pitch <= 0)
{
return false;
}
2026-03-01 16:50:06 +08:00
var x = pointerInViewport.X - state.PointerOffset.X;
var y = pointerInViewport.Y - state.PointerOffset.Y;
2026-03-02 20:02:14 +08:00
column = (int)Math.Floor(x / pitch);
row = (int)Math.Floor(y / pitch);
2026-03-01 16:50:06 +08:00
column = Math.Clamp(column, 0, Math.Max(0, maxColumns - state.WidthCells));
row = Math.Clamp(row, 0, Math.Max(0, maxRows - state.HeightCells));
return true;
}
private bool TryCompleteDesktopComponentDrag(Point pointerInViewport)
{
if (_desktopComponentDrag is null ||
_currentDesktopCellSize <= 0 ||
_currentDesktopSurfaceIndex < 0 ||
_currentDesktopSurfaceIndex >= _desktopPageCount)
2026-03-01 00:34:07 +08:00
{
2026-03-01 16:50:06 +08:00
return false;
2026-02-28 12:30:16 +08:00
}
2026-03-01 16:50:06 +08:00
if (!TryGetDesktopComponentDropCell(pointerInViewport, _desktopComponentDrag, out var row, out var column))
{
return false;
}
switch (_desktopComponentDrag.Kind)
{
case DesktopComponentDragKind.NewFromLibrary:
PlaceDesktopComponentOnPage(_desktopComponentDrag.ComponentId, _currentDesktopSurfaceIndex, row, column);
return true;
case DesktopComponentDragKind.MoveExisting:
return TryMoveExistingDesktopComponent(_desktopComponentDrag.PlacementId, row, column);
default:
return false;
}
}
private bool TryMoveExistingDesktopComponent(string placementId, int row, int column)
{
if (string.IsNullOrWhiteSpace(placementId) ||
_desktopComponentDrag?.SourceHost is null ||
_desktopComponentDrag.Kind != DesktopComponentDragKind.MoveExisting)
{
return false;
}
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
if (placement is null)
{
return false;
}
placement.Row = Math.Max(0, row);
placement.Column = Math.Max(0, column);
Grid.SetRow(_desktopComponentDrag.SourceHost, placement.Row);
Grid.SetColumn(_desktopComponentDrag.SourceHost, placement.Column);
_desktopComponentDrag.SourceHost.Opacity = 1;
ApplyDesktopEditStateToHost(_desktopComponentDrag.SourceHost, _isComponentLibraryOpen);
PersistSettings();
return true;
}
private void CancelDesktopComponentDrag()
{
if (!_isDesktopComponentDragActive)
{
return;
}
if (_desktopComponentDrag?.SourceHost is not null)
{
_desktopComponentDrag.SourceHost.Opacity = 1;
ApplyDesktopEditStateToHost(_desktopComponentDrag.SourceHost, _isComponentLibraryOpen);
}
_desktopComponentDrag = null;
_isDesktopComponentDragActive = false;
if (DesktopEditDragLayer is not null)
{
DesktopEditDragLayer.Children.Clear();
}
_desktopComponentDragGhost = null;
}
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();
if (categoryCount == 0)
{
_componentLibraryCategoryIndex = 0;
_componentLibraryActiveCategoryId = null;
return;
}
var viewportWidth = ComponentLibraryCategoryViewport.Bounds.Width;
if (viewportWidth <= 1 && ComponentLibraryWindow is not null)
{
viewportWidth = Math.Max(1, ComponentLibraryWindow.Bounds.Width - 48);
}
var viewportHeight = ComponentLibraryCategoryViewport.Bounds.Height;
if (viewportHeight <= 1 && ComponentLibraryWindow is not null)
{
viewportHeight = Math.Max(1, ComponentLibraryWindow.Bounds.Height - 120);
}
_componentLibraryCategoryPageWidth = Math.Max(1, viewportWidth);
ComponentLibraryCategoryPagesHost.Width = _componentLibraryCategoryPageWidth * categoryCount;
ComponentLibraryCategoryPagesHost.Height = viewportHeight;
ComponentLibraryCategoryPagesContainer.Width = ComponentLibraryCategoryPagesHost.Width;
ComponentLibraryCategoryPagesContainer.Height = viewportHeight;
ComponentLibraryCategoryPagesContainer.RowDefinitions.Add(new RowDefinition(new GridLength(viewportHeight, GridUnitType.Pixel)));
for (var i = 0; i < categoryCount; i++)
{
ComponentLibraryCategoryPagesContainer.ColumnDefinitions.Add(
new ColumnDefinition(new GridLength(_componentLibraryCategoryPageWidth, GridUnitType.Pixel)));
}
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;
for (var i = 0; i < categoryCount; i++)
{
var category = _componentLibraryCategories[i];
var page = new Grid
{
Width = _componentLibraryCategoryPageWidth,
Height = viewportHeight,
Background = Brushes.Transparent
};
var cardWidth = Math.Clamp(_componentLibraryCategoryPageWidth * 0.64, 160, 260);
var cardHeight = Math.Clamp(viewportHeight * 0.70, 140, 220);
var iconSize = Math.Clamp(cardHeight * 0.34, 30, 56);
var card = new Border
{
Classes = { "glass-panel" },
Width = cardWidth,
Height = cardHeight,
2026-03-02 20:02:14 +08:00
CornerRadius = new CornerRadius(36),
2026-03-01 16:50:06 +08:00
Padding = new Thickness(18),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new StackPanel
{
Spacing = 12,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
new SymbolIcon
{
Symbol = category.Icon,
IconVariant = IconVariant.Regular,
FontSize = iconSize,
HorizontalAlignment = HorizontalAlignment.Center
},
new TextBlock
{
Text = category.Title,
FontSize = Math.Clamp(cardHeight * 0.14, 12, 18),
FontWeight = FontWeight.SemiBold,
HorizontalAlignment = HorizontalAlignment.Center,
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush")
}
}
}
};
page.Children.Add(card);
Grid.SetRow(page, 0);
Grid.SetColumn(page, i);
ComponentLibraryCategoryPagesContainer.Children.Add(page);
}
_componentLibraryCategoryHostTransform = ComponentLibraryCategoryPagesHost.RenderTransform as TranslateTransform;
if (_componentLibraryCategoryHostTransform is null)
{
_componentLibraryCategoryHostTransform = new TranslateTransform();
ComponentLibraryCategoryPagesHost.RenderTransform = _componentLibraryCategoryHostTransform;
}
ApplyComponentLibraryCategoryOffset();
if (ComponentLibraryBackTextBlock is not null)
{
ComponentLibraryBackTextBlock.Text = L("common.back", "Back");
}
}
private IReadOnlyList<ComponentLibraryCategory> GetComponentLibraryCategories()
{
var definitions = _componentRegistry
.GetAll()
.Where(definition => definition.AllowDesktopPlacement)
.ToList();
if (definitions.Count == 0)
{
return Array.Empty<ComponentLibraryCategory>();
}
return definitions
.GroupBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
.Select(group =>
{
var categoryId = string.IsNullOrWhiteSpace(group.Key) ? "Other" : group.Key.Trim();
var components = group
.OrderBy(definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
return new ComponentLibraryCategory(
categoryId,
ResolveComponentLibraryCategoryIcon(categoryId),
GetLocalizedComponentLibraryCategoryTitle(categoryId),
components);
})
.ToList();
}
private Symbol ResolveComponentLibraryCategoryIcon(string categoryId)
{
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
{
return Symbol.CalendarDate;
}
return Symbol.Apps;
}
private string GetLocalizedComponentLibraryCategoryTitle(string categoryId)
{
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
{
2026-03-02 20:02:14 +08:00
return L("component_category.date", "Calendar");
2026-03-01 16:50:06 +08:00
}
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;
}
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;
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;
ComponentLibraryComponentPagesContainer.Children.Clear();
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
if (componentCount == 0)
{
_componentLibraryComponentIndex = 0;
return;
}
var viewportWidth = ComponentLibraryComponentViewport.Bounds.Width;
if (viewportWidth <= 1 && ComponentLibraryWindow is not null)
{
viewportWidth = Math.Max(1, ComponentLibraryWindow.Bounds.Width - 48);
}
var viewportHeight = ComponentLibraryComponentViewport.Bounds.Height;
if (viewportHeight <= 1 && ComponentLibraryWindow is not null)
{
viewportHeight = Math.Max(1, ComponentLibraryWindow.Bounds.Height - 160);
}
_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 definition = _componentLibraryActiveComponents[i];
if (!_componentRegistry.TryGetDefinition(definition.Id, out var resolved) || !resolved.AllowDesktopPlacement)
{
continue;
}
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.86;
var previewMaxHeight = viewportHeight * 0.72;
2026-03-02 20:02:14 +08:00
var previewSpan = NormalizeComponentCellSpan(
resolved.Id,
(resolved.MinWidthCells, resolved.MinHeightCells));
2026-03-01 16:50:06 +08:00
var previewCellSize = Math.Min(
2026-03-02 20:02:14 +08:00
previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
previewCellSize = Math.Clamp(previewCellSize, 20, 72);
2026-03-01 16:50:06 +08:00
2026-03-02 20:02:14 +08:00
var previewWidth = previewSpan.WidthCells * previewCellSize;
var previewHeight = previewSpan.HeightCells * previewCellSize;
var renderCellSize = Math.Clamp(previewCellSize * 1.35, 28, 82);
2026-03-01 16:50:06 +08:00
2026-03-02 20:02:14 +08:00
var previewControl = CreateComponentLibraryPreviewControl(resolved.Id, renderCellSize);
2026-03-01 16:50:06 +08:00
if (previewControl is null)
{
continue;
}
2026-03-02 20:02:14 +08:00
var previewSurface = new Border
{
Width = previewSpan.WidthCells * renderCellSize,
Height = previewSpan.HeightCells * renderCellSize,
Background = Brushes.Transparent,
Child = previewControl
};
var previewViewbox = new Viewbox
{
Width = previewWidth,
Height = previewHeight,
Stretch = Stretch.Uniform,
Child = previewSurface
};
2026-03-01 16:50:06 +08:00
var previewBorder = new Border
{
Width = previewWidth,
Height = previewHeight,
2026-03-02 20:02:14 +08:00
CornerRadius = new CornerRadius(20),
2026-03-01 16:50:06 +08:00
ClipToBounds = true,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
2026-03-02 20:02:14 +08:00
Child = previewViewbox,
2026-03-01 16:50:06 +08:00
Tag = resolved.Id
};
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
var label = new TextBlock
{
Text = GetLocalizedComponentDisplayName(resolved),
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 = 10,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
new Border
{
Classes = { "glass-panel" },
2026-03-02 20:02:14 +08:00
CornerRadius = new CornerRadius(28),
2026-03-01 16:50:06 +08:00
Padding = new Thickness(12),
Child = previewBorder
},
label,
hint
}
};
page.Children.Add(stack);
Grid.SetRow(page, 0);
Grid.SetColumn(page, i);
ComponentLibraryComponentPagesContainer.Children.Add(page);
}
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
if (_componentLibraryComponentHostTransform is null)
{
_componentLibraryComponentHostTransform = new TranslateTransform();
ComponentLibraryComponentPagesHost.RenderTransform = _componentLibraryComponentHostTransform;
}
ApplyComponentLibraryComponentOffset();
}
private Control? CreateComponentLibraryPreviewControl(string componentId, double cellSize)
{
if (componentId == BuiltInComponentIds.Date)
{
var widget = new DateWidget();
widget.SetTimeZoneService(_timeZoneService);
widget.ApplyCellSize(cellSize);
return widget;
}
2026-03-02 20:02:14 +08:00
if (componentId == BuiltInComponentIds.MonthCalendar)
{
var widget = new MonthCalendarWidget();
widget.SetTimeZoneService(_timeZoneService);
widget.ApplyCellSize(cellSize);
return widget;
}
if (componentId == BuiltInComponentIds.LunarCalendar)
{
var widget = new LunarCalendarWidget();
widget.SetTimeZoneService(_timeZoneService);
widget.ApplyCellSize(cellSize);
return widget;
}
2026-03-01 16:50:06 +08:00
return null;
}
private string GetLocalizedComponentDisplayName(DesktopComponentDefinition definition)
{
if (string.Equals(definition.Id, BuiltInComponentIds.Date, StringComparison.OrdinalIgnoreCase))
{
return L("component.date", definition.DisplayName);
}
2026-03-02 20:02:14 +08:00
if (string.Equals(definition.Id, BuiltInComponentIds.MonthCalendar, StringComparison.OrdinalIgnoreCase))
{
return L("component.month_calendar", definition.DisplayName);
}
if (string.Equals(definition.Id, BuiltInComponentIds.LunarCalendar, StringComparison.OrdinalIgnoreCase))
{
return L("component.lunar_calendar", definition.DisplayName);
}
2026-03-01 16:50:06 +08:00
return definition.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 (_isDesktopComponentDragActive)
{
e.Handled = true;
}
}
2026-03-02 20:02:14 +08:00
private bool _isComponentLibraryWindowDragging;
private Point _componentLibraryWindowDragStartPoint;
private Thickness _componentLibraryWindowOriginalMargin;
private bool _isComponentLibraryWindowPositionCustomized;
private void OnComponentLibraryWindowPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ComponentLibraryWindow is null || !_isComponentLibraryOpen)
{
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 (!_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;
e.Handled = true;
}
private void OnComponentLibraryWindowPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_isComponentLibraryWindowDragging)
{
return;
}
_isComponentLibraryWindowDragging = false;
e.Pointer.Capture(null);
if (ComponentLibraryWindow is not null)
{
SaveComponentLibraryWindowPosition();
}
e.Handled = true;
}
2026-03-01 16:50:06 +08:00
private void OnComponentLibraryBackClick(object? sender, RoutedEventArgs e)
{
ShowComponentLibraryCategoryView();
BuildComponentLibraryCategoryPages();
}
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);
}
2026-03-02 20:02:14 +08:00
private void SaveComponentLibraryWindowPosition()
{
if (ComponentLibraryWindow is null)
{
return;
}
var margin = ComponentLibraryWindow.Margin;
_savedComponentLibraryMargin = margin;
_isComponentLibraryWindowPositionCustomized = true;
}
private void RestoreComponentLibraryWindowPosition()
{
if (ComponentLibraryWindow is null)
{
return;
}
ComponentLibraryWindow.Margin = _savedComponentLibraryMargin;
}
private Thickness _savedComponentLibraryMargin = new Thickness(24, 24, 24, 100);
2026-03-01 16:50:06 +08:00
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();
2026-02-28 12:30:16 +08:00
}
}
2026-03-02 20:02:14 +08:00