implement animated selection indicator.. still kinda shoddy but it works :)
MODIFY
WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorState.cs
WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorState.cs
using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+using JetBrains.Annotations;
+using WalletWasabi.Logging;
namespace WalletWasabi.Fluent.Behaviors;
public class NavBarSelectedIndicatorState : IDisposable
{
+ public readonly ConcurrentDictionary<int, Control> ScopeChildren = new();
+ public Rectangle? PreviousIndicator { get; set; }
+ public NavBarSelectionIndicatorAdorner AdornerControl { get; set; }
+
+ public Orientation NavItemsOrientation { get; set; } = Orientation.Vertical;
+
public void Dispose()
{
+ ScopeChildren?.Clear();
+ }
+ public void AddChild(Control associatedObject)
+ {
+ if (ScopeChildren.ContainsKey(associatedObject.GetHashCode()))
+ {
+ return;
}
+
+ ScopeChildren.TryAdd(associatedObject.GetHashCode(),
+ associatedObject);
+ }
+
+ public void Animate(Rectangle NextIndicator)
+ {
+ // For Debouncing.
+ if (PreviousIndicator == NextIndicator)
+ {
+ return;
+ }
+
+ var root = PreviousIndicator.GetVisualAncestors().OfType<VisualLayerManager>().FirstOrDefault();
+
+ var prevVector = PreviousIndicator.TranslatePoint(new Point(), root) ?? new Point();
+ var nextVector = NextIndicator.TranslatePoint(new Point(), root) ?? new Point();
+
+ _currentAnimationCts?.Cancel();
+ AdornerControl.AnimateIndicators(PreviousIndicator, prevVector, NextIndicator, nextVector,
+ _currentAnimationCts.Token, NavItemsOrientation);
+ _currentAnimationCts = new();
+
+ PreviousIndicator = NextIndicator;
+ }
+
+ private CancellationTokenSource _currentAnimationCts = new();
}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/Behaviors/NavigationSelectedItemAdornerBehavior.cs
WalletWasabi.Fluent/Behaviors/NavigationSelectedItemAdornerBehavior.cs
using System;
+using System.Linq;
using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Sources;
using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Threading;
+using Avalonia.Controls.Shapes;
+using Avalonia.Layout;
+using ReactiveUI;
+using WalletWasabi.Logging;
+using Pen = Avalonia.Media.Pen;
namespace WalletWasabi.Fluent.Behaviors;
@@ -14,7 +26,9 @@ public class NavBarSelectedIndicatorParentBehavior : AttachedToVisualTreeBehavio
public static readonly AttachedProperty<NavBarSelectedIndicatorState>
ParentStateProperty =
- AvaloniaProperty.RegisterAttached<Control, Control, NavBarSelectedIndicatorState>("ParentState",
+ AvaloniaProperty
+ .RegisterAttached<NavBarSelectedIndicatorParentBehavior, Control, NavBarSelectedIndicatorState>(
+ "ParentState",
inherits: true);
public static NavBarSelectedIndicatorState GetParentState(Control element)
@@ -32,9 +46,8 @@ public class NavBarSelectedIndicatorParentBehavior : AttachedToVisualTreeBehavio
var k = new NavBarSelectedIndicatorState();
SetParentState(AssociatedObject, k);
+ var z = new NavBarSelectionIndicatorAdorner(AssociatedObject, k);
- var z = new NavBarSelectionIndicatorAdorner(AssociatedObject);
- Dispatcher.UIThread.Post(z.InvalidateVisual, DispatcherPriority.Loaded);
disposables.Add(k);
disposables.Add(z);
@@ -42,26 +55,137 @@ public class NavBarSelectedIndicatorParentBehavior : AttachedToVisualTreeBehavio
}
}
+public class NavBarSelectedIndicatorChildBehavior : AttachedToVisualTreeBehavior<Rectangle>
+{
+ public static readonly AttachedProperty<bool>
+ IsSelectedProperty =
+ AvaloniaProperty.RegisterAttached<NavBarSelectedIndicatorChildBehavior, Rectangle, bool>("IsSelected",
+ inherits: true);
+
+ public static bool GetIsSelected(Control element)
+ {
+ return element.GetValue(IsSelectedProperty);
+ }
+
+ public static void SetIsSelected(Control element, bool value)
+ {
+ element.SetValue(IsSelectedProperty, value);
+ }
+
+
+ public static readonly AttachedProperty<Control>
+ NavBarItemParentProperty =
+ AvaloniaProperty.RegisterAttached<NavBarSelectedIndicatorChildBehavior, Control, Control>(
+ "NavBarItemParent");
+
+ public static Control GetNavBarItemParent(Control element)
+ {
+ return element.GetValue(NavBarItemParentProperty);
+ }
+
+ public static void SetNavBarItemParent(Control element, Control value)
+ {
+ element.SetValue(NavBarItemParentProperty, value);
+ }
+
+
+ private NavBarSelectedIndicatorState GetSharedState =>
+ NavBarSelectedIndicatorParentBehavior.GetParentState(AssociatedObject);
+
+
+ protected override void OnAttachedToVisualTree()
+ {
+ if (GetSharedState is null)
+ {
+ Detach();
+ return;
+ }
+
+ GetSharedState.AddChild(AssociatedObject);
+
+ AssociatedObject.DetachedFromVisualTree += delegate
+ {
+ GetSharedState.ScopeChildren.TryRemove(AssociatedObject.GetHashCode(), out _);
+ };
+
+ var parent = GetNavBarItemParent(AssociatedObject);
+
+ if (parent is null)
+ {
+ Logger.LogError(
+ $"NavBarItem Selection Indicator's parent is null, cannot continue with indicator animations.");
+ return;
+ }
+
+ if (parent.Classes.Contains(":selected"))
+ {
+ GetSharedState.PreviousIndicator = AssociatedObject;
+ }
+
+ AssociatedObject.GetPropertyChangedObservable(IsSelectedProperty)
+ .DistinctUntilChanged()
+ .Subscribe(x =>
+ {
+ var parent = GetNavBarItemParent(AssociatedObject);
+
+ if ((bool)x.NewValue && (GetNavBarItemParent(AssociatedObject)?.Classes.Contains(":selected") ?? false))
+ {
+ GetSharedState.Animate(AssociatedObject);
+ }
+ });
+
+ AssociatedObject.Opacity = 0;
+ }
+}
+
public class NavBarSelectionIndicatorAdorner : Control, IDisposable
{
private readonly AdornerLayer _layer;
private readonly Control _target;
+ private readonly NavBarSelectedIndicatorState _sharedState;
+ private bool _isDispose;
+ private Rectangle newRect = new Rectangle();
+
- public NavBarSelectionIndicatorAdorner(Control element)
+ public NavBarSelectionIndicatorAdorner(Control element, NavBarSelectedIndicatorState sharedState)
{
_target = element;
+ _sharedState = sharedState;
_layer = AdornerLayer.GetAdornerLayer(_target);
IsHitTestVisible = false;
+ IsVisible = false;
if (_layer is null)
{
return;
}
+ Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Loaded);
+
+ _sharedState.AdornerControl = this;
_layer.Children.Add(this);
+ _layer.Children.Add(newRect);
+
+ newRect.VerticalAlignment = VerticalAlignment.Top;
+ newRect.HorizontalAlignment = HorizontalAlignment.Left;
+
+ if (Application.Current.Styles.TryGetResource("FluentEasing", out object? resource) &&
+ resource is Easing outEasing)
+ {
+ newRect.Transitions = new Transitions()
+ {
+ new ThicknessTransition()
+ {
+ Property = Rectangle.MarginProperty,
+ Duration = TimeSpan.FromSeconds(0.30),
+ Easing = outEasing
+ }
+ };
+ }
}
+#if DEBUG
public override void Render(DrawingContext context)
{
@@ -77,11 +201,26 @@ public class NavBarSelectionIndicatorAdorner : Control, IDisposable
context.DrawRectangle(renderBrush, renderPen, adornedElementRect);
}
+#endif
+
public void Dispose()
{
- if (_layer is not null)
+ _isDispose = true;
+ _layer?.Children.Remove(this);
+ _layer?.Children.Remove(newRect);
+ }
+
+ public void AnimateIndicators(Rectangle previousIndicator, Point prevVector, Rectangle nextIndicator,
+ Point nextVector, CancellationToken token, Orientation navItemsOrientation)
{
- _layer.Children.Remove(this);
+ if (_isDispose)
+ {
+ return;
}
+
+ newRect.Width = previousIndicator.Bounds.Width;
+ newRect.Height = previousIndicator.Bounds.Height;
+ newRect.Fill = previousIndicator.Fill;
+ newRect.Margin = new Thickness(nextVector.X, nextVector.Y, 0, 0);
}
}
\ No newline at end of file

MODIFY
WalletWasabi.Fluent/Controls/NavBarItem.axaml
WalletWasabi.Fluent/Controls/NavBarItem.axaml
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:c="using:WalletWasabi.Fluent.Controls">
+ xmlns:c="using:WalletWasabi.Fluent.Controls"
+ xmlns:id="using:Avalonia.Xaml.Interactivity"
+ xmlns:behaviors="using:WalletWasabi.Fluent.Behaviors">
<Design.PreviewWith>
<Border Padding="0">
- <c:NavBarItem />
+ <c:NavBarItem>
+ Test Header
+ </c:NavBarItem>
</Border>
</Design.PreviewWith>
<Styles.Resources>
<Setter Property="Template">
<ControlTemplate>
<Panel>
- <Border CornerRadius="3" x:Name="Indicator_Backdrop" Margin="4 0">
- <Border x:Name="Indicator"
- CornerRadius="6">
- <Border.Transitions>
- <Transitions>
- <DoubleTransition Property="Opacity" Duration="0:0:0.175" />
- <TransformOperationsTransition Property="RenderTransform" Duration="0:0:.135"
- Easing="QuadraticEaseIn" />
- </Transitions>
- </Border.Transitions>
- </Border>
- </Border>
-
+ <Rectangle Name="SelectionIndicator" />
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
</Setter>
</Style>
- <Style Selector="c|NavBarItem:vertical /template/ Border#Indicator">
+ <Style Selector="c|NavBarItem /template/ Rectangle#SelectionIndicator" x:DataType="Rectangle" x:CompileBindings="True">
+ <Setter Property="(id:Interaction.Behaviors)">
+ <id:BehaviorCollectionTemplate>
+ <id:BehaviorCollection>
+ <behaviors:NavBarSelectedIndicatorChildBehavior />
+ </id:BehaviorCollection>
+ </id:BehaviorCollectionTemplate>
+ </Setter>
+ <Setter Property="(behaviors:NavBarSelectedIndicatorChildBehavior.IsSelected)" Value="False"/>
+ <Setter Property="(behaviors:NavBarSelectedIndicatorChildBehavior.NavBarItemParent)" Value="{Binding $parent[c:NavBarItem], Mode=OneWay}"/>
+ </Style>
+
+ <Style Selector="c|NavBarItem:vertical /template/ Rectangle#SelectionIndicator">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Width" Value="3" />
<Setter Property="Height" Value="NaN" />
</Style>
- <Style Selector="c|NavBarItem:horizontal /template/ Border#Indicator">
+ <Style Selector="c|NavBarItem:horizontal /template/ Rectangle#SelectionIndicator">
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Width" Value="NaN" />
<Setter Property="Height" Value="3" />
</Style>
- <Style Selector="c|NavBarItem /template/ Border#Indicator">
- <Setter Property="Background" Value="{DynamicResource NavBarAccentColor}" />
+ <Style Selector="c|NavBarItem /template/ Rectangle#SelectionIndicator">
+ <Setter Property="Fill" Value="{DynamicResource NavBarAccentColor}" />
<Setter Property="Opacity" Value="0" />
<Setter Property="RenderTransformOrigin" Value="50%,0%" />
<Setter Property="RenderTransform" Value="scaleY(0)" />
<Style Selector="c|NavBarItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource NavBarItemPointerOverBrush}" />
</Style>
- <Style Selector="c|NavBarItem:pointerover /template/ Border#Indicator_Backdrop">
- <Setter Property="Background" Value="{DynamicResource NavBarItemPointerOverBackgroundBrush}" />
- </Style>
<!-- Pressed State -->
- <Style Selector="c|NavBarItem:pressed /template/ Border#Indicator">
- <Setter Property="Background" Value="{DynamicResource NavBarItemPressedAccentColor}" />
+ <Style Selector="c|NavBarItem:pressed /template/ Rectangle#SelectionIndicator">
+ <Setter Property="Fill" Value="{DynamicResource NavBarItemPressedAccentColor}" />
</Style>
<Style Selector="c|NavBarItem:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource NavBarItemPressedBrush}" />
</Style>
<!-- Selected State -->
- <Style Selector="c|NavBarItem:selected /template/ Border#Indicator">
+ <Style Selector="c|NavBarItem:selected /template/ Rectangle#SelectionIndicator">
<Setter Property="Opacity" Value="1" />
<Setter Property="RenderTransformOrigin" Value="50%,100%" />
<Setter Property="RenderTransform" Value="scaleY(1)" />
+ <Setter Property="(behaviors:NavBarSelectedIndicatorChildBehavior.IsSelected)" Value="True"/>
</Style>
<Style Selector="c|NavBarItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource NavBarItemSelectedBrush}" />

MODIFY
WalletWasabi.Fluent/Views/NavBar/NavBar.axaml
WalletWasabi.Fluent/Views/NavBar/NavBar.axaml
mc:Ignorable="d" d:DesignWidth="280" d:DesignHeight="459"
x:Class="WalletWasabi.Fluent.Views.NavBar.NavBar" UseLayoutRounding="False"
Name="NavBarUserControl">
- <i:Interaction.Behaviors>
- <behaviors:NavBarSelectedIndicatorParentBehavior/>
- </i:Interaction.Behaviors>
<UserControl.Styles>
<Style Selector="PathIcon">
<Setter Property="Margin" Value="13 0 0 0" />
<c:NavBarListBox Items="{Binding TopItems}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
Name="TopListBox"
- Classes="vertical">
+ Classes="vertical">z
<c:NavBarListBox.ItemTemplate>
<DataTemplate x:DataType="vm:NavBarItemViewModel" x:CompileBindings="False">
<Panel>
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
Classes="horizontal"
Name="BottomListBox">
+
<i:Interaction.Behaviors>
<ia:DataTriggerBehavior Binding="{Binding IsOpen}" ComparisonCondition="Equal" Value="True">
<iac:AddClassAction StyledElement="{Binding #BottomListBox}" ClassName="horizontal" RemoveIfExists="True" />
</StackPanel>
<!-- Items List -->
<DockPanel Background="Transparent">
+ <i:Interaction.Behaviors>
+ <behaviors:NavBarSelectedIndicatorParentBehavior/>
+ </i:Interaction.Behaviors>
+
<TextBlock Classes="headerText" DockPanel.Dock="Top" Text="Wallets" IsVisible="{Binding IsOpen}" />
<c:NavBarListBox Items="{Binding Wallets}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
Classes="vertical draggable narrowItemPadding"
RenderTransformOrigin="0%,50%"
VerticalAlignment="Stretch">
+
<c:NavBarListBox.DataTemplates>
<DataTemplate DataType="vmw:WalletViewModelBase">
<Ellipse Fill="{StaticResource OrangeCoinColor}" Width="9" Height="9" Margin="15,0,0,-13" IsVisible="{Binding IsLoggedIn}" />
</Panel>
+
+
<!-- Title & status text -->
<DockPanel Margin="10 0 15 0" DockPanel.Dock="Left" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" TextTrimming="CharacterEllipsis" DockPanel.Dock="Top" />

implement animated selection indicator.. still kinda shoddy but it works :)