using System; using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; using FluentIcons.Avalonia; using FluentIcons.Common; using LanMontainDesktop.ComponentSystem; using LanMontainDesktop.Models; using LanMontainDesktop.Views.Components; namespace LanMontainDesktop.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 bool _isDesktopComponentDragActive; private DesktopComponentDragState? _desktopComponentDrag; private Border? _desktopComponentDragGhost; 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 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; } } private sealed record ComponentLibraryCategory( string Id, Symbol Icon, string Title, IReadOnlyList Components); private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e) { // "Desktop edit" toggle. While editing, show the component library window. if (_isComponentLibraryOpen) { CloseComponentLibraryWindow(reopenSettings: false); return; } _reopenSettingsAfterComponentLibraryClose = _isSettingsOpen; if (_isSettingsOpen) { CloseSettingsPage(immediate: true); } OpenComponentLibraryWindow(); } private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e) { CloseComponentLibraryWindow(reopenSettings: true); } 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(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; } private void ApplyTopStatusComponentVisibility() { var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock); if (ClockWidget is not null) { ClockWidget.IsVisible = showClock; } if (WallpaperPreviewClockContainer is not null) { WallpaperPreviewClockContainer.IsVisible = showClock; } if (WallpaperPreviewClockTextBlock is not null && showClock) { WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm"); } } 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); var showDesktopEdit = true; BackToWindowsButton.IsVisible = showMinimize; OpenComponentLibraryButton.IsVisible = showDesktopEdit; OpenSettingsButton.IsVisible = showSettings; WallpaperPreviewBackButtonVisual.IsVisible = showMinimize; WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit; WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings; if (TaskbarFixedActionsHost is not null) { TaskbarFixedActionsHost.IsVisible = showMinimize; } if (TaskbarSettingsActionHost is not null) { TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; } if (WallpaperPreviewTaskbarFixedActionsHost is not null) { WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize; } if (WallpaperPreviewTaskbarSettingsActionHost is not null) { WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; } var dynamicActions = ResolveDynamicTaskbarActions(context) .Where(action => action.IsVisible) .ToList(); var hasDynamicActions = dynamicActions.Count > 0; BuildDynamicTaskbarVisuals(dynamicActions); 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; UpdateDesktopComponentHostEditState(); ShowComponentLibraryCategoryView(); ComponentLibraryWindow.IsVisible = true; ComponentLibraryWindow.Opacity = 0; ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); Dispatcher.UIThread.Post(() => { if (!_isComponentLibraryOpen || ComponentLibraryWindow is null) { return; } BuildComponentLibraryCategoryPages(); ComponentLibraryWindow.Opacity = 1; }, DispatcherPriority.Background); } private void CloseComponentLibraryWindow(bool reopenSettings) { if (!_isComponentLibraryOpen || ComponentLibraryWindow is null) { return; } _isComponentLibraryOpen = false; CancelDesktopComponentDrag(); UpdateDesktopComponentHostEditState(); 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)); } 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 canAddPage = _desktopPageCount < MaxDesktopPageCount; return [ new TaskbarActionItem( TaskbarActionId.AddDesktopPage, L("desktop.add_page", "Add page"), "Add", IsVisible: canAddPage, CommandKey: "desktop.add_page") ]; } if (!_enableDynamicTaskbarActions) { return Array.Empty(); } // Reserved for page-specific actions. Disabled by default in this phase. _ = context; return Array.Empty(); } private void BuildDynamicTaskbarVisuals(IReadOnlyList actions) { if (TaskbarDynamicActionsPanel is not null) { TaskbarDynamicActionsPanel.Children.Clear(); } if (WallpaperPreviewTaskbarDynamicActionsHost is not null) { WallpaperPreviewTaskbarDynamicActionsHost.Children.Clear(); } if (actions.Count == 0 || TaskbarDynamicActionsPanel is null || WallpaperPreviewTaskbarDynamicActionsHost is null) { return; } foreach (var action in actions) { if (!action.IsVisible) { continue; } var button = new Button { Content = action.Title, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Padding = new Thickness(12, 6), Foreground = Foreground, Tag = action.CommandKey }; button.Click += OnDynamicTaskbarActionClick; TaskbarDynamicActionsPanel.Children.Add(button); var previewText = new TextBlock { Text = action.Title, Foreground = Foreground, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; var previewBorder = new Border { Background = Brushes.Transparent, BorderThickness = new Thickness(0), Child = previewText }; WallpaperPreviewTaskbarDynamicActionsHost.Children.Add(previewBorder); } } 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; } } private void AddDesktopPage() { if (_desktopPageCount >= MaxDesktopPageCount) { return; } _desktopPageCount = Math.Clamp(_desktopPageCount + 1, MinDesktopPageCount, MaxDesktopPageCount); _currentDesktopSurfaceIndex = Math.Clamp(_desktopPageCount - 1, 0, LauncherSurfaceIndex); RebuildDesktopGrid(); PersistSettings(); } private void InitializeDesktopComponentPlacements(AppSettingsSnapshot 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 (!_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement) { continue; } var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( 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; } pageGrid.Children.Clear(); 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 (!_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition) || !definition.AllowDesktopPlacement) { continue; } var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( 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); } } 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; } var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( definition, definition.MinWidthCells, 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); 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; } var host = new Border { Tag = placement.PlacementId, Background = Brushes.Transparent, ClipToBounds = true, Child = component }; host.Classes.Add(DesktopComponentHostClass); ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen); host.PointerPressed += OnDesktopComponentHostPointerPressed; return host; } 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; } 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(); 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) { host.IsHitTestVisible = isEditMode; host.CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)); if (isEditMode) { host.BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)); host.BorderBrush = GetThemeBrush("AdaptiveAccentBrush"); } else { host.BorderThickness = new Thickness(0); host.BorderBrush = null; } if (host.Child is Control child) { // In edit mode, prefer drag interactions over component interactions. child.IsHitTestVisible = !isEditMode; } } private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e) { if (!_isComponentLibraryOpen || _isDesktopComponentDragActive) { 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; } BeginDesktopComponentMoveDrag(host, placement, e); e.Handled = true; } private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e) { if (DesktopEditDragLayer is null || DesktopPagesViewport is null || _currentDesktopCellSize <= 0 || !_componentRegistry.TryGetDefinition(placement.ComponentId, out var definition)) { return; } var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( definition, placement.WidthCells, placement.HeightCells); var pointerInViewport = e.GetPosition(DesktopPagesViewport); var topLeft = new Point(placement.Column * _currentDesktopCellSize, placement.Row * _currentDesktopCellSize); 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 || DesktopEditDragLayer is null || DesktopPagesViewport is null || _currentDesktopCellSize <= 0 || !_componentRegistry.TryGetDefinition(componentId, out var definition) || !definition.AllowDesktopPlacement) { return; } var (widthCells, heightCells) = ComponentPlacementRules.EnsureMinimumSize( definition, definition.MinWidthCells, definition.MinHeightCells); // Center the component under the pointer while dragging from the library. var pointerOffset = new Point( (widthCells * _currentDesktopCellSize) * 0.5, (heightCells * _currentDesktopCellSize) * 0.5); _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); e.Pointer.Capture(this); } private void EnsureDesktopComponentDragGhost(string componentId, int widthCells, int heightCells) { if (DesktopEditDragLayer is null) { return; } DesktopEditDragLayer.Children.Clear(); var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize); var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize); var ghostContent = CreateDesktopComponentControl(componentId); if (ghostContent is not null) { ghostContent.IsHitTestVisible = false; } _desktopComponentDragGhost = new Border { Width = ghostWidth, Height = ghostHeight, CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.22, 8, 18)), Background = new SolidColorBrush(Color.Parse("#331E40AF")), BorderBrush = GetThemeBrush("AdaptiveAccentBrush"), BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)), Child = ghostContent, Opacity = 0.92, IsHitTestVisible = false }; DesktopEditDragLayer.Children.Add(_desktopComponentDragGhost); } private void OnDesktopComponentDragPointerMoved(object? sender, PointerEventArgs e) { if (!_isDesktopComponentDragActive || _desktopComponentDrag is null || DesktopPagesViewport is null) { return; } UpdateDesktopComponentDragVisual(e.GetPosition(DesktopPagesViewport)); } private void OnDesktopComponentDragPointerReleased(object? sender, PointerReleasedEventArgs e) { if (!_isDesktopComponentDragActive || _desktopComponentDrag is null || DesktopPagesViewport is null) { 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) { 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; } _desktopComponentDragGhost.IsVisible = true; _desktopComponentDrag.TargetRow = row; _desktopComponentDrag.TargetColumn = column; Canvas.SetLeft(_desktopComponentDragGhost, column * _currentDesktopCellSize); Canvas.SetTop(_desktopComponentDragGhost, row * _currentDesktopCellSize); } 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; } var x = pointerInViewport.X - state.PointerOffset.X; var y = pointerInViewport.Y - state.PointerOffset.Y; column = (int)Math.Floor(x / _currentDesktopCellSize); row = (int)Math.Floor(y / _currentDesktopCellSize); 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) { return false; } 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, CornerRadius = new CornerRadius(18), 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 GetComponentLibraryCategories() { var definitions = _componentRegistry .GetAll() .Where(definition => definition.AllowDesktopPlacement) .ToList(); if (definitions.Count == 0) { return Array.Empty(); } 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)) { return L("component_category.date", "Date"); } 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; var previewCellSize = Math.Min( previewMaxWidth / Math.Max(1, resolved.MinWidthCells), previewMaxHeight / Math.Max(1, resolved.MinHeightCells)); previewCellSize = Math.Clamp(previewCellSize, 18, 64); var previewWidth = resolved.MinWidthCells * previewCellSize; var previewHeight = resolved.MinHeightCells * previewCellSize; var previewControl = CreateComponentLibraryPreviewControl(resolved.Id, previewCellSize); if (previewControl is null) { continue; } var previewBorder = new Border { Width = previewWidth, Height = previewHeight, CornerRadius = new CornerRadius(16), ClipToBounds = true, Background = Brushes.Transparent, BorderThickness = new Thickness(0), Child = previewControl, 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" }, CornerRadius = new CornerRadius(18), 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; } return null; } private string GetLocalizedComponentDisplayName(DesktopComponentDefinition definition) { if (string.Equals(definition.Id, BuiltInComponentIds.Date, StringComparison.OrdinalIgnoreCase)) { return L("component.date", definition.DisplayName); } 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; } } 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); } 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(); } }