From 9e9f49599fe7f51e34eef513982dea77c7d660d2 Mon Sep 17 00:00:00 2001 From: vgm Date: Tue, 6 Oct 2026 00:09:28 +0800 Subject: [PATCH] ux: show feedback while dragging repository tabs Show a title/icon preview, dim the dragged tab, and highlight the destination with an insertion marker matching the existing reorder behavior. Clear feedback when leaving the tab bar, cancelling, dropping, or detaching. Match the tab's Border style key and keep its transparent background in a default style so the drag feedback styles take effect. Constrain preview titles to the available width and ellipsize long names. Clip the preview content to keep it inside the tab-sized border. Keep native drag-and-drop and the current Launcher.MoveTab behavior. --- src/Views/LauncherTabBar.axaml | 20 +++- src/Views/LauncherTabBar.axaml.cs | 170 +++++++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 4 deletions(-) diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index 76b4f7ddf..593011f1c 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -9,7 +9,11 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.LauncherTabBar" x:DataType="vm:Launcher" - x:Name="ThisControl"> + x:Name="ThisControl" + DragDrop.AllowDrop="True" + DragDrop.DragEnter="DragOverTabs" + DragDrop.DragOver="DragOverTabs" + DragDrop.DragLeave="DragLeaveTabs"> + + + @@ -61,9 +75,9 @@ - ().FirstOrDefault(t => t.IsEffectivelyVisible); + var icon = page.Node.IsRepository + ? (Control)new Bookmark { Value = page.Node.Bookmark } + : new Avalonia.Controls.Shapes.Path + { + Data = this.FindResource("Icons.Repositories") as Geometry, + Fill = this.FindResource("Brush.FG1") as IBrush + }; + icon.Width = 12; + icon.Height = 12; + icon.Margin = new Thickness(0, 0, 8, 0); + var content = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*"), + Margin = new Thickness(12, 0), + VerticalAlignment = VerticalAlignment.Center + }; + content.Children.Add(icon); + var title = new TextBlock + { + Text = page.Node.IsRepository ? page.Node.Name : App.Text("PageTabBar.Welcome.Title"), + FontFamily = text?.FontFamily ?? FontFamily.Default, + FontSize = text?.FontSize ?? 12, + Foreground = this.FindResource("Brush.FG1") as IBrush, + TextTrimming = TextTrimming.CharacterEllipsis, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(title, 1); + content.Children.Add(title); + _dragPreview = new Border + { + Width = size.Width, + Height = size.Height, + Background = this.FindResource("Brush.ToolBar") as IBrush, + BorderBrush = this.FindResource("Brush.Border0") as IBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(4), + Opacity = 0.85, + IsVisible = false, + ClipToBounds = true, + Child = content + }; + _dropIndicator = new Border + { + Width = 3, + Height = Math.Max(0, size.Height - 4), + Background = this.FindResource("Brush.Accent") as IBrush, + IsVisible = false + }; + _dragOverlay = new Canvas { IsHitTestVisible = false }; + _dragOverlay.Children.Add(_dropIndicator); + _dragOverlay.Children.Add(_dragPreview); + AdornerLayer.SetAdornedElement(_dragOverlay, this); + AdornerLayer.SetIsClipEnabled(_dragOverlay, false); + _dragLayer.Children.Add(_dragOverlay); + } + source.Classes.Add("tab_drag_source"); + } + + private void DragOverTabs(object sender, DragEventArgs e) + { + if (_draggedPage == null || DataContext is not ViewModels.Launcher launcher) + return; + + var point = e.GetPosition(this); + if (_dragPreview != null) + { + Canvas.SetLeft(_dragPreview, point.X + 12); + Canvas.SetTop(_dragPreview, point.Y + 12); + _dragPreview.IsVisible = true; + } + + ClearDropTarget(); + e.DragEffects = DragDropEffects.None; + var fromIndex = launcher.Pages.IndexOf(_draggedPage); + if (fromIndex < 0 || !new Rect(LauncherTabsScroller.Bounds.Size).Contains(e.GetPosition(LauncherTabsScroller))) + return; + + for (var i = 0; i < LauncherTabsList.ItemCount; i++) + { + var container = LauncherTabsList.ContainerFromIndex(i); + var tab = container?.GetVisualDescendants().OfType().FirstOrDefault(); + if (tab == null || i == fromIndex) + continue; + + var position = e.GetPosition(tab); + if (!new Rect(tab.Bounds.Size).Contains(position)) + continue; + + _dropTarget = tab; + tab.Classes.Add("tab_drop_target"); + e.DragEffects = DragDropEffects.Move; + if (_dropIndicator != null && tab.TranslatePoint(new Point(i > fromIndex ? tab.Bounds.Width : 0, 2), this) is { } edge) + { + Canvas.SetLeft(_dropIndicator, edge.X - 1.5); + Canvas.SetTop(_dropIndicator, edge.Y); + _dropIndicator.IsVisible = true; + } + break; + } + e.Handled = true; + } + + private void DragLeaveTabs(object sender, DragEventArgs e) + { + ClearDropTarget(); + if (_dragPreview != null) + _dragPreview.IsVisible = false; + } + + private void ClearDropTarget() + { + _dropTarget?.Classes.Remove("tab_drop_target"); + _dropTarget = null; + if (_dropIndicator != null) + _dropIndicator.IsVisible = false; + } + + private void ClearDragFeedback() + { + ClearDropTarget(); + _dragSource?.Classes.Remove("tab_drag_source"); + if (_dragOverlay != null) + _dragLayer?.Children.Remove(_dragOverlay); + _draggedPage = null; + _dragSource = null; + _dragOverlay = null; + _dragPreview = null; + _dropIndicator = null; + _dragLayer = null; + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + ClearDragFeedback(); + base.OnDetachedFromVisualTree(e); + } + private void DropTab(object sender, DragEventArgs e) { if (e.DataTransfer.TryGetValue(_dndMainTabFormat) is not { Length: > 0 } id) @@ -286,6 +446,7 @@ private void DropTab(object sender, DragEventArgs e) if (target == to) return; + ClearDragFeedback(); launcher.MoveTab(target, to); _pressedTabEvent = null; @@ -427,6 +588,13 @@ private void OnCloseTab(object sender, RoutedEventArgs e) private readonly Vector _scrollStep = new(64, 0); private PointerPressedEventArgs _pressedTabEvent = null; private bool _startDragTab = false; + private ViewModels.LauncherPage _draggedPage; + private Border _dragSource; + private LauncherTabSizeBox _dropTarget; + private AdornerLayer _dragLayer; + private Canvas _dragOverlay; + private Border _dragPreview; + private Border _dropIndicator; private readonly DataFormat _dndMainTabFormat = DataFormat.CreateStringApplicationFormat("sourcegit-dnd-main-tab"); } }