11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using CommunityToolkit . WinUI ;
5+ using Microsoft . UI . Input ;
46using Microsoft . UI . Xaml . Automation . Peers ;
7+ using Microsoft . UI . Xaml . Input ;
8+ using Microsoft . UI . Xaml . Media ;
9+ using Windows . Foundation ;
510
611namespace Files . App . Controls
712{
@@ -11,7 +16,14 @@ namespace Files.App.Controls
1116 [ TemplatePart ( Name = "CloseButton" , Type = typeof ( Button ) ) ]
1217 public partial class BladeItem : ContentControl
1318 {
19+ private const double MINIMUM_WIDTH = 150 ;
20+ private const double DEFAULT_WIDTH = 300 ; // Default width for the blade item
21+
1422 private Button _closeButton ;
23+ private Border _bladeResizer ;
24+ private bool _draggingSidebarResizer ;
25+ private double _preManipulationSidebarWidth = 0 ;
26+
1527 /// <summary>
1628 /// Initializes a new instance of the <see cref="BladeItem"/> class.
1729 /// </summary>
@@ -36,7 +48,31 @@ protected override void OnApplyTemplate()
3648
3749 _closeButton . Click -= CloseButton_Click ;
3850 _closeButton . Click += CloseButton_Click ;
51+
52+ _bladeResizer = GetTemplateChild ( "BladeResizer" ) as Border ;
53+
54+ if ( _bladeResizer != null )
55+ {
56+ _bladeResizer . ManipulationStarted -= BladeResizer_ManipulationStarted ;
57+ _bladeResizer . ManipulationStarted += BladeResizer_ManipulationStarted ;
58+
59+ _bladeResizer . ManipulationDelta -= BladeResizer_ManipulationDelta ;
60+ _bladeResizer . ManipulationDelta += BladeResizer_ManipulationDelta ;
61+
62+ _bladeResizer . ManipulationCompleted -= BladeResizer_ManipulationCompleted ;
63+ _bladeResizer . ManipulationCompleted += BladeResizer_ManipulationCompleted ;
64+
65+ _bladeResizer . PointerEntered -= BladeResizer_PointerEntered ;
66+ _bladeResizer . PointerEntered += BladeResizer_PointerEntered ;
67+
68+ _bladeResizer . PointerExited -= BladeResizer_PointerExited ;
69+ _bladeResizer . PointerExited += BladeResizer_PointerExited ;
70+
71+ _bladeResizer . DoubleTapped -= BladeResizer_DoubleTapped ;
72+ _bladeResizer . DoubleTapped += BladeResizer_DoubleTapped ;
73+ }
3974 }
75+
4076 /// <summary>
4177 /// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>)
4278 /// </summary>
@@ -50,5 +86,166 @@ private void CloseButton_Click(object sender, RoutedEventArgs e)
5086 {
5187 IsOpen = false ;
5288 }
89+
90+ private void BladeResizer_ManipulationStarted ( object sender , ManipulationStartedRoutedEventArgs e )
91+ {
92+ _draggingSidebarResizer = true ;
93+ _preManipulationSidebarWidth = ActualWidth ;
94+ VisualStateManager . GoToState ( this , "ResizerPressed" , true ) ;
95+ e . Handled = true ;
96+ }
97+
98+ private void BladeResizer_ManipulationDelta ( object sender , ManipulationDeltaRoutedEventArgs e )
99+ {
100+ var newWidth = _preManipulationSidebarWidth + e . Cumulative . Translation . X ;
101+ if ( newWidth < MINIMUM_WIDTH )
102+ newWidth = MINIMUM_WIDTH ;
103+
104+ Width = newWidth ;
105+ e . Handled = true ;
106+ }
107+
108+ private void BladeResizer_ManipulationCompleted ( object sender , ManipulationCompletedRoutedEventArgs e )
109+ {
110+ _draggingSidebarResizer = false ;
111+ VisualStateManager . GoToState ( this , "ResizerNormal" , true ) ;
112+ e . Handled = true ;
113+ }
114+
115+ private void BladeResizer_DoubleTapped ( object sender , DoubleTappedRoutedEventArgs e )
116+ {
117+ var optimalWidth = CalculateOptimalWidth ( ) ;
118+ if ( optimalWidth > 0 )
119+ {
120+ Width = Math . Max ( optimalWidth , MINIMUM_WIDTH ) ;
121+ }
122+ else
123+ {
124+ // Fallback to default width if calculation fails
125+ Width = DEFAULT_WIDTH ;
126+ }
127+
128+ e . Handled = true ;
129+ }
130+
131+ private double CalculateOptimalWidth ( )
132+ {
133+ try
134+ {
135+ // Look for any ListView within this BladeItem that contains text content
136+ var listView = this . FindDescendant < ListView > ( ) ;
137+ if ( listView ? . Items == null || ! listView . Items . Any ( ) )
138+ return 0 ;
139+
140+ // Calculate the maximum width needed by measuring text content
141+ var maxTextWidth = MeasureContentWidth ( listView ) ;
142+
143+ // Add padding for icon, margins, and other UI elements
144+ // Icon width (32) + margins (24) + padding (24) + chevron/tags (40) = 120
145+ var totalPadding = 120 ;
146+
147+ return maxTextWidth + totalPadding ;
148+ }
149+ catch ( Exception )
150+ {
151+ return 0 ;
152+ }
153+ }
154+
155+ private double MeasureContentWidth ( ListView listView )
156+ {
157+ try
158+ {
159+ double maxWidth = 0 ;
160+
161+ // Find all TextBlocks in the ListView using visual tree walking
162+ var textBlocks = GetTextBlocksFromVisualTree ( listView ) ;
163+
164+ if ( textBlocks . Any ( ) )
165+ {
166+ // Measure each TextBlock and find the widest one
167+ foreach ( var textBlock in textBlocks )
168+ {
169+ if ( string . IsNullOrEmpty ( textBlock . Text ) )
170+ continue ;
171+
172+ // Create a measuring TextBlock with the same properties
173+ var measuringBlock = new TextBlock
174+ {
175+ Text = textBlock . Text ,
176+ FontSize = textBlock . FontSize ,
177+ FontFamily = textBlock . FontFamily ,
178+ FontWeight = textBlock . FontWeight ,
179+ FontStyle = textBlock . FontStyle
180+ } ;
181+
182+ measuringBlock . Measure ( new Size ( double . PositiveInfinity , double . PositiveInfinity ) ) ;
183+ maxWidth = Math . Max ( maxWidth , measuringBlock . DesiredSize . Width ) ;
184+ }
185+ }
186+ else
187+ {
188+ // Fallback: estimate based on item count and average text width
189+ var itemCount = listView . Items . Count ;
190+ if ( itemCount > 0 )
191+ {
192+ // Estimate average filename length and multiply by character width
193+ var estimatedCharWidth = 8 ; // Approximate pixel width per character
194+ var estimatedMaxLength = Math . Min ( 50 , Math . Max ( 20 , itemCount * 2 ) ) ; // Heuristic
195+ maxWidth = estimatedCharWidth * estimatedMaxLength ;
196+ }
197+ }
198+
199+ return maxWidth ;
200+ }
201+ catch ( Exception )
202+ {
203+ // Fallback calculation
204+ return 200 ; // Default reasonable width
205+ }
206+ }
207+
208+ private List < TextBlock > GetTextBlocksFromVisualTree ( DependencyObject parent )
209+ {
210+ var textBlocks = new List < TextBlock > ( ) ;
211+
212+ if ( parent == null )
213+ return textBlocks ;
214+
215+ var childrenCount = VisualTreeHelper . GetChildrenCount ( parent ) ;
216+ for ( int i = 0 ; i < childrenCount ; i ++ )
217+ {
218+ var child = VisualTreeHelper . GetChild ( parent , i ) ;
219+
220+ if ( child is TextBlock textBlock )
221+ {
222+ textBlocks . Add ( textBlock ) ;
223+ }
224+
225+ // Recursively search child elements
226+ textBlocks . AddRange ( GetTextBlocksFromVisualTree ( child ) ) ;
227+ }
228+
229+ return textBlocks ;
230+ }
231+
232+ private void BladeResizer_PointerEntered ( object sender , PointerRoutedEventArgs e )
233+ {
234+ var sidebarResizer = ( FrameworkElement ) sender ;
235+ sidebarResizer . ChangeCursor ( InputSystemCursor . Create ( InputSystemCursorShape . SizeWestEast ) ) ;
236+ VisualStateManager . GoToState ( this , "ResizerPointerOver" , true ) ;
237+ e . Handled = true ;
238+ }
239+
240+ private void BladeResizer_PointerExited ( object sender , PointerRoutedEventArgs e )
241+ {
242+ if ( _draggingSidebarResizer )
243+ return ;
244+
245+ var sidebarResizer = ( FrameworkElement ) sender ;
246+ sidebarResizer . ChangeCursor ( InputSystemCursor . Create ( InputSystemCursorShape . Arrow ) ) ;
247+ VisualStateManager . GoToState ( this , "ResizerNormal" , true ) ;
248+ e . Handled = true ;
249+ }
53250 }
54251}
0 commit comments