Files
LanMountainDesktop/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
lincube 685323e057 0.7.5
顺滑的组件放置与调整
2026-03-22 15:21:29 +08:00

3196 lines
114 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
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 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;
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);
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":
DeleteCurrentDesktopPage();
break;
case "component.delete":
DeleteSelectedComponent();
break;
case "component.edit":
OpenSelectedComponentEditor();
break;
case "launcher.hide":
HideSelectedLauncherEntry();
break;
}
}
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);
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());
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);
}
}
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);
}
_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);
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));
}
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"));
_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"));
_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"));
_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");
}
}
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;
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();
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 renderCellSize = Math.Clamp(previewCellSize * 1.15, 26, 110);
var previewControl = CreateDesktopComponentControl(
component.ComponentId,
renderCellSize,
placementId: null,
pageIndex: null,
action: "ComponentLibraryPreview");
if (previewControl is null)
{
continue;
}
// Component library previews must stay non-interactive so drag gesture is reliable.
previewControl.IsHitTestVisible = false;
previewControl.Focusable = false;
var previewSurface = new Border
{
Width = previewSpan.WidthCells * renderCellSize,
Height = previewSpan.HeightCells * renderCellSize,
Background = Brushes.Transparent,
IsHitTestVisible = false,
Child = previewControl
};
var previewViewbox = new Viewbox
{
Width = previewWidth,
Height = previewHeight,
Stretch = Stretch.Uniform,
Child = previewSurface
};
var previewBorder = new Border
{
Width = previewWidth,
Height = previewHeight,
ClipToBounds = false,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Child = previewViewbox,
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);
}
_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();
}
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();
}
}