diff --git a/src/Files.App.Controls/BladeView/BladeItem.cs b/src/Files.App.Controls/BladeView/BladeItem.cs index 0ba8f525f6c0..a96dd33afeb4 100644 --- a/src/Files.App.Controls/BladeView/BladeItem.cs +++ b/src/Files.App.Controls/BladeView/BladeItem.cs @@ -1,7 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using CommunityToolkit.WinUI; +using Microsoft.UI.Input; using Microsoft.UI.Xaml.Automation.Peers; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Windows.Foundation; namespace Files.App.Controls { @@ -11,7 +16,14 @@ namespace Files.App.Controls [TemplatePart(Name = "CloseButton", Type = typeof(Button))] public partial class BladeItem : ContentControl { + private const double MINIMUM_WIDTH = 150; + private const double DEFAULT_WIDTH = 300; // Default width for the blade item + private Button _closeButton; + private Border _bladeResizer; + private bool _draggingSidebarResizer; + private double _preManipulationSidebarWidth = 0; + /// /// Initializes a new instance of the class. /// @@ -36,7 +48,31 @@ protected override void OnApplyTemplate() _closeButton.Click -= CloseButton_Click; _closeButton.Click += CloseButton_Click; + + _bladeResizer = GetTemplateChild("BladeResizer") as Border; + + if (_bladeResizer != null) + { + _bladeResizer.ManipulationStarted -= BladeResizer_ManipulationStarted; + _bladeResizer.ManipulationStarted += BladeResizer_ManipulationStarted; + + _bladeResizer.ManipulationDelta -= BladeResizer_ManipulationDelta; + _bladeResizer.ManipulationDelta += BladeResizer_ManipulationDelta; + + _bladeResizer.ManipulationCompleted -= BladeResizer_ManipulationCompleted; + _bladeResizer.ManipulationCompleted += BladeResizer_ManipulationCompleted; + + _bladeResizer.PointerEntered -= BladeResizer_PointerEntered; + _bladeResizer.PointerEntered += BladeResizer_PointerEntered; + + _bladeResizer.PointerExited -= BladeResizer_PointerExited; + _bladeResizer.PointerExited += BladeResizer_PointerExited; + + _bladeResizer.DoubleTapped -= BladeResizer_DoubleTapped; + _bladeResizer.DoubleTapped += BladeResizer_DoubleTapped; + } } + /// /// Creates AutomationPeer () /// @@ -50,5 +86,166 @@ private void CloseButton_Click(object sender, RoutedEventArgs e) { IsOpen = false; } + + private void BladeResizer_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e) + { + _draggingSidebarResizer = true; + _preManipulationSidebarWidth = ActualWidth; + VisualStateManager.GoToState(this, "ResizerPressed", true); + e.Handled = true; + } + + private void BladeResizer_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) + { + var newWidth = _preManipulationSidebarWidth + e.Cumulative.Translation.X; + if (newWidth < MINIMUM_WIDTH) + newWidth = MINIMUM_WIDTH; + + Width = newWidth; + e.Handled = true; + } + + private void BladeResizer_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) + { + _draggingSidebarResizer = false; + VisualStateManager.GoToState(this, "ResizerNormal", true); + e.Handled = true; + } + + private void BladeResizer_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) + { + var optimalWidth = CalculateOptimalWidth(); + if (optimalWidth > 0) + { + Width = Math.Max(optimalWidth, MINIMUM_WIDTH); + } + else + { + // Fallback to default width if calculation fails + Width = DEFAULT_WIDTH; + } + + e.Handled = true; + } + + private double CalculateOptimalWidth() + { + try + { + // Look for any ListView within this BladeItem that contains text content + var listView = this.FindDescendant(); + if (listView?.Items == null || !listView.Items.Any()) + return 0; + + // Calculate the maximum width needed by measuring text content + var maxTextWidth = MeasureContentWidth(listView); + + // Add padding for icon, margins, and other UI elements + // Icon width (32) + margins (24) + padding (24) + chevron/tags (40) = 120 + var totalPadding = 120; + + return maxTextWidth + totalPadding; + } + catch (Exception) + { + return 0; + } + } + + private double MeasureContentWidth(ListView listView) + { + try + { + double maxWidth = 0; + + // Find all TextBlocks in the ListView using visual tree walking + var textBlocks = GetTextBlocksFromVisualTree(listView); + + if (textBlocks.Any()) + { + // Measure each TextBlock and find the widest one + foreach (var textBlock in textBlocks) + { + if (string.IsNullOrEmpty(textBlock.Text)) + continue; + + // Create a measuring TextBlock with the same properties + var measuringBlock = new TextBlock + { + Text = textBlock.Text, + FontSize = textBlock.FontSize, + FontFamily = textBlock.FontFamily, + FontWeight = textBlock.FontWeight, + FontStyle = textBlock.FontStyle + }; + + measuringBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + maxWidth = Math.Max(maxWidth, measuringBlock.DesiredSize.Width); + } + } + else + { + // Fallback: estimate based on item count and average text width + var itemCount = listView.Items.Count; + if (itemCount > 0) + { + // Estimate average filename length and multiply by character width + var estimatedCharWidth = 8; // Approximate pixel width per character + var estimatedMaxLength = Math.Min(50, Math.Max(20, itemCount * 2)); // Heuristic + maxWidth = estimatedCharWidth * estimatedMaxLength; + } + } + + return maxWidth; + } + catch (Exception) + { + // Fallback calculation + return 200; // Default reasonable width + } + } + + private List GetTextBlocksFromVisualTree(DependencyObject parent) + { + var textBlocks = new List(); + + if (parent == null) + return textBlocks; + + var childrenCount = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < childrenCount; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + + if (child is TextBlock textBlock) + { + textBlocks.Add(textBlock); + } + + // Recursively search child elements + textBlocks.AddRange(GetTextBlocksFromVisualTree(child)); + } + + return textBlocks; + } + + private void BladeResizer_PointerEntered(object sender, PointerRoutedEventArgs e) + { + var sidebarResizer = (FrameworkElement)sender; + sidebarResizer.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.SizeWestEast)); + VisualStateManager.GoToState(this, "ResizerPointerOver", true); + e.Handled = true; + } + + private void BladeResizer_PointerExited(object sender, PointerRoutedEventArgs e) + { + if (_draggingSidebarResizer) + return; + + var sidebarResizer = (FrameworkElement)sender; + sidebarResizer.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.Arrow)); + VisualStateManager.GoToState(this, "ResizerNormal", true); + e.Handled = true; + } } } diff --git a/src/Files.App.Controls/BladeView/BladeView.xaml b/src/Files.App.Controls/BladeView/BladeView.xaml index 82a593fed509..98ebd4d629be 100644 --- a/src/Files.App.Controls/BladeView/BladeView.xaml +++ b/src/Files.App.Controls/BladeView/BladeView.xaml @@ -114,6 +114,18 @@ VerticalAlignment="Stretch" Background="{TemplateBinding Background}" /> + + +