feat(ui): folders slide open and closed, rows below follow
This commit is contained in:
@@ -867,19 +867,6 @@
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
</Style>
|
||||
<!-- Folder tree rows sit tighter than the smart lists — a deep tree gets long fast. -->
|
||||
<!-- Folder expand: children drop down from under the header. Collapse stays instant. -->
|
||||
<Style Selector="Panel.folder-children">
|
||||
<Setter Property="RenderTransformOrigin" Value="50%,0%" />
|
||||
<Setter Property="RenderTransform" Value="scaleY(1)" />
|
||||
</Style>
|
||||
<Style Selector="Panel.folder-children.animate[IsVisible=True]">
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:0.16" Easing="CubicEaseOut" FillMode="Forward">
|
||||
<KeyFrame Cue="0%"> <Setter Property="ScaleTransform.ScaleY" Value="0.4"/><Setter Property="Opacity" Value="0"/></KeyFrame>
|
||||
<KeyFrame Cue="100%"><Setter Property="ScaleTransform.ScaleY" Value="1"/><Setter Property="Opacity" Value="1"/></KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
<Style Selector="Border.list-item.tree-row">
|
||||
<Setter Property="Padding" Value="4,4,10,4" />
|
||||
</Style>
|
||||
|
||||
@@ -18,8 +18,6 @@ public sealed partial class ListFolderNavItemViewModel : ViewModelBase
|
||||
[ObservableProperty] private string _iconKey = "Folder";
|
||||
/// <summary>Hex colour for name + guide line; null = theme default.</summary>
|
||||
[ObservableProperty] private string? _color;
|
||||
/// <summary>Set on the first user toggle so the expand animation skips folders that load already open.</summary>
|
||||
[ObservableProperty] private bool _animateExpand;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowCount))]
|
||||
private bool _isCollapsed;
|
||||
|
||||
@@ -730,7 +730,6 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
private void ToggleFolder(ListFolderNavItemViewModel? folder)
|
||||
{
|
||||
if (folder is null) return;
|
||||
folder.AnimateExpand = true;
|
||||
folder.IsCollapsed = !folder.IsCollapsed;
|
||||
if (_folderCollapseCts.TryGetValue(folder.Id, out var previous)) previous.Cancel();
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Accordion host: animates its height between 0 and the child's natural height, so content
|
||||
/// below slides instead of jumping. The first value (from the binding) applies without animation.
|
||||
/// </summary>
|
||||
public sealed class ExpandPanel : Decorator
|
||||
{
|
||||
private static readonly TimeSpan Duration = TimeSpan.FromMilliseconds(180);
|
||||
|
||||
public static readonly StyledProperty<bool> IsExpandedProperty =
|
||||
AvaloniaProperty.Register<ExpandPanel, bool>(nameof(IsExpanded));
|
||||
|
||||
/// <summary>0 = collapsed, 1 = fully open; drives the measured height.</summary>
|
||||
public static readonly StyledProperty<double> ProgressProperty =
|
||||
AvaloniaProperty.Register<ExpandPanel, double>(nameof(Progress));
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _arranged;
|
||||
|
||||
static ExpandPanel()
|
||||
{
|
||||
AffectsMeasure<ExpandPanel>(ProgressProperty);
|
||||
ClipToBoundsProperty.OverrideDefaultValue<ExpandPanel>(true);
|
||||
}
|
||||
|
||||
/// <summary>Whether the content is shown.</summary>
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => GetValue(IsExpandedProperty);
|
||||
set => SetValue(IsExpandedProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Current open fraction, animated between 0 and 1.</summary>
|
||||
public double Progress
|
||||
{
|
||||
get => GetValue(ProgressProperty);
|
||||
set => SetValue(ProgressProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == IsExpandedProperty)
|
||||
Animate(IsExpanded ? 1 : 0);
|
||||
else if (change.Property == ProgressProperty && Child is { } child)
|
||||
child.IsVisible = Progress > 0;
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
if (Child is { } child) child.IsVisible = Progress > 0;
|
||||
}
|
||||
|
||||
private void Animate(double to)
|
||||
{
|
||||
_cts?.Cancel();
|
||||
var from = Progress;
|
||||
Progress = to;
|
||||
if (!_arranged || from == to) return;
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
var animation = new Animation
|
||||
{
|
||||
Duration = Duration,
|
||||
Easing = new CubicEaseOut(),
|
||||
Children =
|
||||
{
|
||||
new KeyFrame { Cue = new Cue(0), Setters = { new Setter(ProgressProperty, from) } },
|
||||
new KeyFrame { Cue = new Cue(1), Setters = { new Setter(ProgressProperty, to) } },
|
||||
},
|
||||
};
|
||||
_ = animation.RunAsync(this, _cts.Token);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
if (Child is not { } child) return default;
|
||||
child.Measure(availableSize.WithHeight(double.PositiveInfinity));
|
||||
return new Size(child.DesiredSize.Width, child.DesiredSize.Height * Progress);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
_arranged = true;
|
||||
// Full natural height, top-aligned; ClipToBounds cuts off what isn't revealed yet.
|
||||
Child?.Arrange(new Rect(0, 0, finalSize.Width, Child.DesiredSize.Height));
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||
xmlns:converters="using:ClaudeDo.Ui.Converters"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
|
||||
x:DataType="vm:ListsIslandViewModel">
|
||||
<!-- Implicit, type-matched templates: the sidebar's top level is one mixed ItemsControl over
|
||||
@@ -162,13 +163,14 @@
|
||||
|
||||
<!-- Guide line drops from under the chevron's centre (tree-row padding 4 + half the 9px
|
||||
glyph); children start past it so their highlight never touches the line. -->
|
||||
<Panel Classes="folder-children" Classes.animate="{Binding AnimateExpand}"
|
||||
IsVisible="{Binding !IsCollapsed}">
|
||||
<Border Width="1" HorizontalAlignment="Left" Margin="8,0,0,4"
|
||||
Background="{Binding Color, Converter={x:Static converters:HexBrushConverter.Instance}, ConverterParameter=LineBrush}"
|
||||
IsHitTestVisible="False"/>
|
||||
<ItemsControl ItemsSource="{Binding Children}" Margin="14,0,0,0"/>
|
||||
</Panel>
|
||||
<ctl:ExpandPanel IsExpanded="{Binding !IsCollapsed}">
|
||||
<Panel>
|
||||
<Border Width="1" HorizontalAlignment="Left" Margin="8,0,0,4"
|
||||
Background="{Binding Color, Converter={x:Static converters:HexBrushConverter.Instance}, ConverterParameter=LineBrush}"
|
||||
IsHitTestVisible="False"/>
|
||||
<ItemsControl ItemsSource="{Binding Children}" Margin="14,0,0,0"/>
|
||||
</Panel>
|
||||
</ctl:ExpandPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
|
||||
|
||||
Reference in New Issue
Block a user