91
src/Ursa.Themes.Semi/Controls/Anchor.axaml
Normal file
91
src/Ursa.Themes.Semi/Controls/Anchor.axaml
Normal file
@@ -0,0 +1,91 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:Ursa.Themes.Semi.Converters"
|
||||
xmlns:iri="https://irihi.tech/shared"
|
||||
xmlns:u="https://irihi.tech/ursa">
|
||||
<converters:TreeLevelToPaddingConverter x:Key="LevelToPaddingConverter" />
|
||||
<ControlTheme x:Key="{x:Type u:Anchor}" TargetType="{x:Type u:Anchor}">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Panel>
|
||||
<Rectangle
|
||||
Width="{DynamicResource AnchorPipeWidth}"
|
||||
Name="PART_Pipe"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Fill="{DynamicResource AnchorPipeBackground}" />
|
||||
<ItemsPresenter
|
||||
Name="PART_ItemsPresenter"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||
</Panel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^.Muted /template/ Rectangle#PART_Pipe">
|
||||
<Setter Property="Fill" Value="Transparent" />
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
|
||||
<ControlTheme x:Key="{x:Type u:AnchorItem}" TargetType="u:AnchorItem">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="iri:ClassHelper.ClassSource" Value="{Binding $parent[u:Anchor]}" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:AnchorItem">
|
||||
<StackPanel>
|
||||
<Panel Background="{TemplateBinding Background}">
|
||||
<Border
|
||||
Name="PART_Pipe"
|
||||
Width="{DynamicResource AnchorPipeWidth}"
|
||||
CornerRadius="{DynamicResource AnchorPipeCornerRadius}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch" />
|
||||
<Panel>
|
||||
<ContentPresenter
|
||||
Name="{x:Static iri:PartNames.PART_HeaderPresenter}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource AnchorForeground}"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentTemplate="{TemplateBinding HeaderTemplate}">
|
||||
<ContentPresenter.Padding>
|
||||
<MultiBinding Converter="{StaticResource LevelToPaddingConverter}">
|
||||
<Binding Path="Level" RelativeSource="{RelativeSource AncestorType={x:Type u:AnchorItem}}" />
|
||||
<DynamicResource ResourceKey="AnchorIndent" />
|
||||
</MultiBinding>
|
||||
</ContentPresenter.Padding>
|
||||
</ContentPresenter>
|
||||
</Panel>
|
||||
</Panel>
|
||||
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
|
||||
</StackPanel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
<Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter">
|
||||
<Setter Property="MinHeight" Value="{DynamicResource AnchorDefaultHeight}" />
|
||||
</Style>
|
||||
<Style Selector="^.Small /template/ ContentPresenter#PART_HeaderPresenter">
|
||||
<Setter Property="MinHeight" Value="{DynamicResource AnchorSmallHeight}" />
|
||||
<Setter Property="FontSize" Value="{DynamicResource AnchorSmallFontSize}" />
|
||||
</Style>
|
||||
<Style Selector="^:selected">
|
||||
<Style Selector="^ /template/ ContentPresenter#PART_HeaderPresenter">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AnchorSelectedForeground}" />
|
||||
</Style>
|
||||
<Style Selector="^ /template/ Border#PART_Pipe">
|
||||
<Setter Property="Background" Value="{DynamicResource AnchorPipeSelectedBackground}" />
|
||||
</Style>
|
||||
<Style Selector="^.Primary /template/ Border#PART_Pipe">
|
||||
<Setter Property="Background" Value="{DynamicResource AnchorPipeSelectedPrimaryBackground}" />
|
||||
</Style>
|
||||
<Style Selector="^.Tertiary /template/ Border#PART_Pipe">
|
||||
<Setter Property="Background" Value="{DynamicResource AnchorPipeSelectedTertiaryBackground}" />
|
||||
</Style>
|
||||
<Style Selector="^.Muted /template/ Border#PART_Pipe">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -1,5 +1,6 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="Anchor.axaml" />
|
||||
<ResourceInclude Source="AutoCompleteBox.axaml" />
|
||||
<ResourceInclude Source="Avatar.axaml" />
|
||||
<ResourceInclude Source="Badge.axaml" />
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace Ursa.Themes.Semi.Converters;
|
||||
|
||||
public class NavigationMenuItemLevelToMarginConverter: IMultiValueConverter
|
||||
{
|
||||
public int Indent { get; set; }
|
||||
|
||||
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (values[0] is int i && values[1] is bool b)
|
||||
{
|
||||
if (b)
|
||||
{
|
||||
return new Thickness();
|
||||
}
|
||||
return new Thickness((i-1) * Indent, 0, 0, 0);
|
||||
}
|
||||
return new Thickness();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace Ursa.Themes.Semi.Converters;
|
||||
|
||||
public class TreeLevelToPaddingConverter : IMultiValueConverter
|
||||
{
|
||||
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (values[0] is int i && values[1] is Thickness indent)
|
||||
{
|
||||
return new Thickness(Math.Max(i, 0) * indent.Left, indent.Top, indent.Right, indent.Bottom);
|
||||
}
|
||||
|
||||
return new Thickness();
|
||||
}
|
||||
}
|
||||
8
src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml
Normal file
8
src/Ursa.Themes.Semi/Themes/Dark/Anchor.axaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<StaticResource x:Key="AnchorPipeBackground" ResourceKey="SemiColorBorder" />
|
||||
<StaticResource x:Key="AnchorForeground" ResourceKey="SemiColorText2" />
|
||||
<StaticResource x:Key="AnchorSelectedForeground" ResourceKey="SemiColorText0" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedBackground" ResourceKey="SemiColorPrimary" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedPrimaryBackground" ResourceKey="SemiColorPrimary" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedTertiaryBackground" ResourceKey="SemiColorTertiary" />
|
||||
</ResourceDictionary>
|
||||
@@ -1,6 +1,7 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="Avatar.axaml" />
|
||||
<ResourceInclude Source="Anchor.axaml" />
|
||||
<ResourceInclude Source="Badge.axaml" />
|
||||
<ResourceInclude Source="Banner.axaml" />
|
||||
<ResourceInclude Source="Breadcrumb.axaml" />
|
||||
|
||||
7
src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml
Normal file
7
src/Ursa.Themes.Semi/Themes/HighContrast/Anchor.axaml
Normal file
@@ -0,0 +1,7 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<StaticResource x:Key="AnchorPipeBackground" ResourceKey="GrayTextColor" />
|
||||
<StaticResource x:Key="AnchorForeground" ResourceKey="WindowTextColor" />
|
||||
<StaticResource x:Key="AnchorSelectedForeground" ResourceKey="HotlightColor" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedBackground" ResourceKey="HotlightColor" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedPrimaryBackground" ResourceKey="HotlightColor" />
|
||||
</ResourceDictionary>
|
||||
@@ -1,5 +1,6 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="Anchor.axaml" />
|
||||
<ResourceInclude Source="Badge.axaml" />
|
||||
<ResourceInclude Source="Banner.axaml" />
|
||||
<ResourceInclude Source="Breadcrumb.axaml" />
|
||||
|
||||
8
src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml
Normal file
8
src/Ursa.Themes.Semi/Themes/Light/Anchor.axaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<StaticResource x:Key="AnchorPipeBackground" ResourceKey="SemiColorBorder" />
|
||||
<StaticResource x:Key="AnchorForeground" ResourceKey="SemiColorText2" />
|
||||
<StaticResource x:Key="AnchorSelectedForeground" ResourceKey="SemiColorText0" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedBackground" ResourceKey="SemiColorPrimary" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedPrimaryBackground" ResourceKey="SemiColorPrimary" />
|
||||
<StaticResource x:Key="AnchorPipeSelectedTertiaryBackground" ResourceKey="SemiColorTertiary" />
|
||||
</ResourceDictionary>
|
||||
@@ -1,6 +1,7 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="Avatar.axaml" />
|
||||
<ResourceInclude Source="Anchor.axaml" />
|
||||
<ResourceInclude Source="Badge.axaml" />
|
||||
<ResourceInclude Source="Banner.axaml" />
|
||||
<ResourceInclude Source="Breadcrumb.axaml" />
|
||||
|
||||
8
src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml
Normal file
8
src/Ursa.Themes.Semi/Themes/Shared/Anchor.axaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Thickness x:Key="AnchorIndent">8,4,0,4</Thickness>
|
||||
<x:Double x:Key="AnchorPipeWidth">2</x:Double>
|
||||
<CornerRadius x:Key="AnchorPipeCornerRadius">1</CornerRadius>
|
||||
<x:Double x:Key="AnchorDefaultHeight">20</x:Double>
|
||||
<x:Double x:Key="AnchorSmallHeight">16</x:Double>
|
||||
<x:Double x:Key="AnchorSmallFontSize">12</x:Double>
|
||||
</ResourceDictionary>
|
||||
@@ -1,6 +1,7 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="Avatar.axaml" />
|
||||
<ResourceInclude Source="Anchor.axaml" />
|
||||
<ResourceInclude Source="Badge.axaml" />
|
||||
<ResourceInclude Source="Banner.axaml" />
|
||||
<ResourceInclude Source="ButtonGroup.axaml" />
|
||||
|
||||
21
src/Ursa/Common/LogicalHelpers.cs
Normal file
21
src/Ursa/Common/LogicalHelpers.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Avalonia.LogicalTree;
|
||||
using Ursa.Controls;
|
||||
|
||||
namespace Ursa.Common;
|
||||
|
||||
public static class LogicalHelpers
|
||||
{
|
||||
public static int CalculateDistanceFromLogicalParent<T, TItem>(TItem? item, int defaultValue = -1)
|
||||
where T : class
|
||||
where TItem : ILogical
|
||||
{
|
||||
var result = 0;
|
||||
ILogical? logical = item;
|
||||
while (logical is not null and not T)
|
||||
{
|
||||
if (logical is TItem) result++;
|
||||
logical = logical.LogicalParent;
|
||||
}
|
||||
return item is not null ? result : defaultValue;
|
||||
}
|
||||
}
|
||||
14
src/Ursa/Common/VisualHelpers.cs
Normal file
14
src/Ursa/Common/VisualHelpers.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Ursa.Common;
|
||||
|
||||
public static class VisualHelpers
|
||||
{
|
||||
public static T? GetContainerFromEventSource<T>(this Visual? source) where T: Control
|
||||
{
|
||||
var item = source?.GetSelfAndVisualAncestors().OfType<T>().FirstOrDefault();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
221
src/Ursa/Controls/Anchor/Anchor.cs
Normal file
221
src/Ursa/Controls/Anchor/Anchor.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.VisualTree;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Some basic assumptions: This should not be a regular SelectingItemsControl, because it does not support multiple
|
||||
/// selections.
|
||||
/// Selection should not be exposed to the user, it is only used to determine which item is currently selected.
|
||||
/// The manipulation of container selection should be simplified.
|
||||
/// Scroll event of TargetContainer also triggers selection change.
|
||||
/// </summary>
|
||||
public class Anchor : ItemsControl
|
||||
{
|
||||
public static readonly StyledProperty<ScrollViewer?> TargetContainerProperty =
|
||||
AvaloniaProperty.Register<Anchor, ScrollViewer?>(
|
||||
nameof(TargetContainer));
|
||||
|
||||
public static readonly AttachedProperty<string?> IdProperty =
|
||||
AvaloniaProperty.RegisterAttached<Anchor, Visual, string?>("Id");
|
||||
|
||||
private CancellationTokenSource _cts = new();
|
||||
|
||||
private List<(string, double)> _positions = [];
|
||||
private bool _scrollingFromSelection;
|
||||
|
||||
private AnchorItem? _selectedContainer;
|
||||
|
||||
public ScrollViewer? TargetContainer
|
||||
{
|
||||
get => GetValue(TargetContainerProperty);
|
||||
set => SetValue(TargetContainerProperty, value);
|
||||
}
|
||||
|
||||
public static void SetId(Visual obj, string? value)
|
||||
{
|
||||
obj.SetValue(IdProperty, value);
|
||||
}
|
||||
|
||||
public static string? GetId(Visual obj)
|
||||
{
|
||||
return obj.GetValue(IdProperty);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> TopOffsetProperty = AvaloniaProperty.Register<Anchor, double>(
|
||||
nameof(TopOffset));
|
||||
|
||||
public double TopOffset
|
||||
{
|
||||
get => GetValue(TopOffsetProperty);
|
||||
set => SetValue(TopOffsetProperty, value);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainer<AnchorItem>(item, out recycleKey);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
var i = new AnchorItem();
|
||||
return i;
|
||||
}
|
||||
|
||||
private void ScrollToAnchor(Visual target)
|
||||
{
|
||||
if (TargetContainer is null)
|
||||
return;
|
||||
|
||||
var targetPosition = target.TranslatePoint(new Point(0, 0), TargetContainer);
|
||||
if (targetPosition.HasValue)
|
||||
{
|
||||
var from = TargetContainer.Offset.Y;
|
||||
var to = TargetContainer.Offset.Y + targetPosition.Value.Y - TopOffset;
|
||||
if (to > TargetContainer.Extent.Height - TargetContainer.Bounds.Height)
|
||||
to = TargetContainer.Extent.Height - TargetContainer.Bounds.Height;
|
||||
if (from == to) return;
|
||||
var animation = new Animation
|
||||
{
|
||||
Duration = TimeSpan.FromSeconds(0.3),
|
||||
Easing = new QuadraticEaseOut(),
|
||||
Children =
|
||||
{
|
||||
new KeyFrame
|
||||
{
|
||||
Setters = { new Setter(ScrollViewer.OffsetProperty, new Vector(0, from)) },
|
||||
Cue = new Cue(0.0)
|
||||
},
|
||||
new KeyFrame
|
||||
{
|
||||
Setters = { new Setter(ScrollViewer.OffsetProperty, new Vector(0, to)) },
|
||||
Cue = new Cue(1.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
token.Register(_ => _scrollingFromSelection = false, null);
|
||||
_scrollingFromSelection = true;
|
||||
animation.RunAsync(TargetContainer, token).ContinueWith(_ => _scrollingFromSelection = false, token);
|
||||
}
|
||||
}
|
||||
|
||||
public void InvalidatePositions()
|
||||
{
|
||||
InvalidateAnchorPositions();
|
||||
MarkSelectedContainerByPosition();
|
||||
}
|
||||
|
||||
internal void InvalidateAnchorPositions()
|
||||
{
|
||||
if (TargetContainer is null) return;
|
||||
var items = TargetContainer.GetVisualDescendants().Where(a => GetId(a) is not null);
|
||||
var positions = new List<(string, double)>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var anchorId = GetId(item);
|
||||
if (anchorId is null) continue;
|
||||
var position = item.TransformToVisual(TargetContainer)?.M32 + TargetContainer.Offset.Y;
|
||||
if (position.HasValue) positions.Add((anchorId, position.Value));
|
||||
}
|
||||
|
||||
positions.Sort((a, b) => a.Item2.CompareTo(b.Item2));
|
||||
_positions = positions;
|
||||
}
|
||||
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
var target = TargetContainer;
|
||||
if (target is null) return;
|
||||
TargetContainer?.AddHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged);
|
||||
TargetContainer?.AddHandler(LoadedEvent, OnTargetContainerLoaded);
|
||||
if (TargetContainer?.IsLoaded == true) InvalidateAnchorPositions();
|
||||
MarkSelectedContainerByPosition();
|
||||
}
|
||||
|
||||
private void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (_scrollingFromSelection) return;
|
||||
MarkSelectedContainerByPosition();
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
var source = (e.Source as Visual).GetContainerFromEventSource<AnchorItem>();
|
||||
if (source is null) return;
|
||||
MarkSelectedContainer(source);
|
||||
var target = TargetContainer?.GetVisualDescendants()
|
||||
.FirstOrDefault(a => GetId(a) == source.AnchorId);
|
||||
if (target is null) return;
|
||||
ScrollToAnchor(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is used to expose the protected CreateContainerForItemOverride method to the AnchorItem class.
|
||||
/// </summary>
|
||||
internal Control CreateContainerForItemOverrideInternal(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return CreateContainerForItemOverride(item, index, recycleKey);
|
||||
}
|
||||
|
||||
internal bool NeedsContainerOverrideInternal(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return NeedsContainerOverride(item, index, out recycleKey);
|
||||
}
|
||||
|
||||
internal void PrepareContainerForItemOverrideInternal(Control container, object? item, int index)
|
||||
{
|
||||
PrepareContainerForItemOverride(container, item, index);
|
||||
}
|
||||
|
||||
internal void ContainerForItemPreparedOverrideInternal(Control container, object? item, int index)
|
||||
{
|
||||
ContainerForItemPreparedOverride(container, item, index);
|
||||
}
|
||||
|
||||
internal void MarkSelectedContainer(AnchorItem? item)
|
||||
{
|
||||
var oldValue = _selectedContainer;
|
||||
var newValue = item;
|
||||
if (oldValue == newValue) return;
|
||||
_selectedContainer?.SetValue(AnchorItem.IsSelectedProperty, false);
|
||||
_selectedContainer = newValue;
|
||||
_selectedContainer?.SetValue(AnchorItem.IsSelectedProperty, true);
|
||||
}
|
||||
|
||||
internal void MarkSelectedContainerByPosition()
|
||||
{
|
||||
if (TargetContainer is null) return;
|
||||
var top = TargetContainer.Offset.Y + TopOffset;
|
||||
var topAnchorId = _positions.LastOrDefault(a => a.Item2 <= top).Item1;
|
||||
if (topAnchorId is null) return;
|
||||
var item = this.GetVisualDescendants().OfType<AnchorItem>()
|
||||
.FirstOrDefault(a => a.AnchorId == topAnchorId);
|
||||
if (item is null) return;
|
||||
MarkSelectedContainer(item);
|
||||
}
|
||||
|
||||
protected override void OnUnloaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnUnloaded(e);
|
||||
TargetContainer?.RemoveHandler(LoadedEvent, OnTargetContainerLoaded);
|
||||
TargetContainer?.RemoveHandler(ScrollViewer.ScrollChangedEvent, OnScrollChanged);
|
||||
}
|
||||
|
||||
private void OnTargetContainerLoaded(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
InvalidateAnchorPositions();
|
||||
}
|
||||
}
|
||||
91
src/Ursa/Controls/Anchor/AnchorItem.cs
Normal file
91
src/Ursa/Controls/Anchor/AnchorItem.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.LogicalTree;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class AnchorItem : HeaderedItemsControl, ISelectable
|
||||
{
|
||||
public static readonly StyledProperty<string?> AnchorIdProperty = AvaloniaProperty.Register<AnchorItem, string?>(
|
||||
nameof(AnchorId));
|
||||
|
||||
public static readonly StyledProperty<bool> IsSelectedProperty =
|
||||
SelectingItemsControl.IsSelectedProperty.AddOwner<AnchorItem>();
|
||||
|
||||
private static readonly FuncTemplate<Panel?> DefaultPanel =
|
||||
new(() => new StackPanel());
|
||||
|
||||
internal static readonly DirectProperty<AnchorItem, int> LevelProperty =
|
||||
AvaloniaProperty.RegisterDirect<AnchorItem, int>(
|
||||
nameof(Level), o => o.Level, (o, v) => o.Level = v);
|
||||
|
||||
private int _level;
|
||||
|
||||
private Anchor? _root;
|
||||
|
||||
static AnchorItem()
|
||||
{
|
||||
SelectableMixin.Attach<AnchorItem>(IsSelectedProperty);
|
||||
PressedMixin.Attach<AnchorItem>();
|
||||
ItemsPanelProperty.OverrideDefaultValue<AnchorItem>(DefaultPanel);
|
||||
}
|
||||
|
||||
public int Level
|
||||
{
|
||||
get => _level;
|
||||
set => SetAndRaise(LevelProperty, ref _level, value);
|
||||
}
|
||||
|
||||
public string? AnchorId
|
||||
{
|
||||
get => GetValue(AnchorIdProperty);
|
||||
set => SetValue(AnchorIdProperty, value);
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => GetValue(IsSelectedProperty);
|
||||
set => SetValue(IsSelectedProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_root = this.GetLogicalAncestors().OfType<Anchor>().FirstOrDefault();
|
||||
Level = LogicalHelpers.CalculateDistanceFromLogicalParent<Anchor, AnchorItem>(this);
|
||||
if (ItemTemplate is null && _root?.ItemTemplate is not null)
|
||||
SetCurrentValue(ItemTemplateProperty, _root.ItemTemplate);
|
||||
|
||||
if (ItemContainerTheme is null && _root?.ItemContainerTheme is not null)
|
||||
SetCurrentValue(ItemContainerThemeProperty, _root.ItemContainerTheme);
|
||||
}
|
||||
|
||||
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
|
||||
{
|
||||
return EnsureRoot().CreateContainerForItemOverrideInternal(item, index, recycleKey);
|
||||
}
|
||||
|
||||
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||
{
|
||||
return EnsureRoot().NeedsContainerOverrideInternal(item, index, out recycleKey);
|
||||
}
|
||||
|
||||
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||
{
|
||||
EnsureRoot().PrepareContainerForItemOverrideInternal(container, item, index);
|
||||
}
|
||||
|
||||
protected override void ContainerForItemPreparedOverride(Control container, object? item, int index)
|
||||
{
|
||||
EnsureRoot().ContainerForItemPreparedOverrideInternal(container, item, index);
|
||||
}
|
||||
|
||||
private Anchor EnsureRoot()
|
||||
{
|
||||
return _root ?? throw new InvalidOperationException("AnchorItem must be inside an Anchor control.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user