feat(ui): folders slide open and closed, rows below follow
This commit is contained in:
@@ -867,19 +867,6 @@
|
|||||||
<Setter Property="BorderBrush" Value="Transparent" />
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
</Style>
|
</Style>
|
||||||
<!-- Folder tree rows sit tighter than the smart lists — a deep tree gets long fast. -->
|
<!-- 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">
|
<Style Selector="Border.list-item.tree-row">
|
||||||
<Setter Property="Padding" Value="4,4,10,4" />
|
<Setter Property="Padding" Value="4,4,10,4" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ public sealed partial class ListFolderNavItemViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string _iconKey = "Folder";
|
[ObservableProperty] private string _iconKey = "Folder";
|
||||||
/// <summary>Hex colour for name + guide line; null = theme default.</summary>
|
/// <summary>Hex colour for name + guide line; null = theme default.</summary>
|
||||||
[ObservableProperty] private string? _color;
|
[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]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(ShowCount))]
|
[NotifyPropertyChangedFor(nameof(ShowCount))]
|
||||||
private bool _isCollapsed;
|
private bool _isCollapsed;
|
||||||
|
|||||||
@@ -730,7 +730,6 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
private void ToggleFolder(ListFolderNavItemViewModel? folder)
|
private void ToggleFolder(ListFolderNavItemViewModel? folder)
|
||||||
{
|
{
|
||||||
if (folder is null) return;
|
if (folder is null) return;
|
||||||
folder.AnimateExpand = true;
|
|
||||||
folder.IsCollapsed = !folder.IsCollapsed;
|
folder.IsCollapsed = !folder.IsCollapsed;
|
||||||
if (_folderCollapseCts.TryGetValue(folder.Id, out var previous)) previous.Cancel();
|
if (_folderCollapseCts.TryGetValue(folder.Id, out var previous)) previous.Cancel();
|
||||||
var cts = new CancellationTokenSource();
|
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:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||||
xmlns:converters="using:ClaudeDo.Ui.Converters"
|
xmlns:converters="using:ClaudeDo.Ui.Converters"
|
||||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
|
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||||
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
|
x:Class="ClaudeDo.Ui.Views.Islands.ListsIslandView"
|
||||||
x:DataType="vm:ListsIslandViewModel">
|
x:DataType="vm:ListsIslandViewModel">
|
||||||
<!-- Implicit, type-matched templates: the sidebar's top level is one mixed ItemsControl over
|
<!-- 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
|
<!-- 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. -->
|
glyph); children start past it so their highlight never touches the line. -->
|
||||||
<Panel Classes="folder-children" Classes.animate="{Binding AnimateExpand}"
|
<ctl:ExpandPanel IsExpanded="{Binding !IsCollapsed}">
|
||||||
IsVisible="{Binding !IsCollapsed}">
|
<Panel>
|
||||||
<Border Width="1" HorizontalAlignment="Left" Margin="8,0,0,4"
|
<Border Width="1" HorizontalAlignment="Left" Margin="8,0,0,4"
|
||||||
Background="{Binding Color, Converter={x:Static converters:HexBrushConverter.Instance}, ConverterParameter=LineBrush}"
|
Background="{Binding Color, Converter={x:Static converters:HexBrushConverter.Instance}, ConverterParameter=LineBrush}"
|
||||||
IsHitTestVisible="False"/>
|
IsHitTestVisible="False"/>
|
||||||
<ItemsControl ItemsSource="{Binding Children}" Margin="14,0,0,0"/>
|
<ItemsControl ItemsSource="{Binding Children}" Margin="14,0,0,0"/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
</ctl:ExpandPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
|
<Border Grid.Row="2" Height="2" VerticalAlignment="Center" Margin="4,0"
|
||||||
|
|||||||
Reference in New Issue
Block a user