Files
LanMountainDesktop/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
2026-03-29 15:34:17 +08:00

3304 lines
118 KiB
C#
Raw Blame History

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