Merge pull request #141 from irihitech/top

Scroll To Button
This commit is contained in:
Zhang Dian
2024-03-06 21:06:36 +08:00
committed by GitHub
15 changed files with 433 additions and 53 deletions

View File

@@ -1,37 +0,0 @@
namespace Ursa.Demo;
public static class MenuKeys
{
public const string MenuKeyIntroduction = "Introduction";
public const string MenuKeyBadge = "Badge";
public const string MenuKeyBanner = "Banner";
public const string MenuKeyButtonGroup = "ButtonGroup";
public const string MenuKeyBreadcrumb = "Breadcrumb";
public const string MenuKeyClassInput = "Class Input";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDisableContainer = "DisableContainer";
public const string MenuKeyDrawer = "Drawer";
public const string MenuKeyDualBadge = "DualBadge";
public const string MenuKeyEnumSelector = "EnumSelector";
public const string MenuKeyForm = "Form";
public const string MenuKeyImageViewer = "ImageViewer";
public const string MenuKeyIpBox = "IPv4Box";
public const string MenuKeyIconButton = "IconButton";
public const string MenuKeyKeyGestureInput = "KeyGestureInput";
public const string MenuKeyLoading = "Loading";
public const string MenuKeyMessageBox = "MessageBox";
public const string MenuKeyNavMenu = "NavMenu";
public const string MenuKeyNumberDisplayer = "NumberDisplayer";
public const string MenuKeyNumericUpDown = "NumericUpDown";
public const string MenuKeyPagination = "Pagination";
public const string MenuKeyRangeSlider = "RangeSlider";
public const string MenuKeySelectionList = "SelectionList";
public const string MenuKeyTagInput = "TagInput";
public const string MenuKeySkeleton = "Skeleton";
public const string MenuKeyTimeline = "Timeline";
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
public const string MenuKeyThemeToggler = "ThemeToggler";
public const string MenuKeyToolBar = "ToolBar";
}

View File

@@ -8,7 +8,7 @@
d:DesignHeight="850"
d:DesignWidth="850"
mc:Ignorable="d">
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<ScrollViewer HorizontalScrollBarVisibility="Auto" u:ScrollTo.Direction="Bottom">
<StackPanel
Margin="24"
HorizontalAlignment="Left"

View File

@@ -0,0 +1,77 @@
<UserControl
x:Class="Ursa.Demo.Pages.ScrollToButtonDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vm:ScrollToButtonDemoViewModel"
mc:Ignorable="d">
<UserControl.Styles>
<Style Selector="ScrollViewer">
<Setter Property="Margin" Value="8" />
</Style>
<Style Selector="ScrollViewer Border.Content">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0, 0" EndPoint="2000, 2000">
<GradientStop Color="{DynamicResource SemiPurple9Color}" Offset="0.0"></GradientStop>
<GradientStop Color="{DynamicResource SemiPurple5Color}" Offset="0.5"></GradientStop>
<GradientStop Color="{DynamicResource SemiPurple1Color}" Offset="1.0"></GradientStop>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="*, *, *, *, *" RowDefinitions="Auto, *">
<TextBlock Grid.Row="0" Grid.Column="0">Scroll To Top</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="1">Scroll To Bottom</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="2">Scroll To Left</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="3">Scroll To Right</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="4">Scroll To Top</TextBlock>
<ScrollViewer
Grid.Row="1"
Grid.Column="0"
u:ScrollTo.Direction="Top"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="Content" Height="2000" HorizontalAlignment="Stretch" />
</ScrollViewer>
<ScrollViewer
Grid.Row="1"
Grid.Column="1"
u:ScrollTo.Direction="Bottom"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="Content" Height="2000" HorizontalAlignment="Stretch" />
</ScrollViewer>
<ScrollViewer
Grid.Row="1"
Grid.Column="2"
u:ScrollTo.Direction="Left"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<Border Classes="Content" Width="2000" VerticalAlignment="Stretch" />
</ScrollViewer>
<ScrollViewer
Grid.Row="1"
Grid.Column="3"
u:ScrollTo.Direction="Right"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<Border Classes="Content" Width="2000" VerticalAlignment="Stretch" />
</ScrollViewer>
<ListBox
Grid.Row="1"
Grid.Column="4"
u:ScrollTo.Direction="Top"
u:ScrollTo.ButtonTheme="{DynamicResource PrimaryScrollToButton}"
ItemsSource="{Binding Items}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class ScrollToButtonDemo : UserControl
{
public ScrollToButtonDemo()
{
InitializeComponent();
}
}

View File

@@ -32,4 +32,8 @@
<DependentUpon>SkeletonDemo.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>
</Project>

View File

@@ -48,6 +48,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
MenuKeys.MenuKeyScrollToButton => new ScrollToButtonDemoViewModel(),
MenuKeys.MenuKeySelectionList => new SelectionListDemoViewModel(),
MenuKeys.MenuKeySkeleton => new SkeletonDemoViewModel(),
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),

View File

@@ -35,6 +35,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider },
new() { MenuHeader = "Scroll To", Key = MenuKeys.MenuKeyScrollToButton, Status = "New" },
new() { MenuHeader = "Selection List", Key = MenuKeys.MenuKeySelectionList, Status = "New" },
new() { MenuHeader = "Skeleton", Key = MenuKeys.MenuKeySkeleton, Status = "New" },
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
@@ -44,4 +45,41 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar, Status = "New" }
};
}
}
public static class MenuKeys
{
public const string MenuKeyIntroduction = "Introduction";
public const string MenuKeyBadge = "Badge";
public const string MenuKeyBanner = "Banner";
public const string MenuKeyButtonGroup = "ButtonGroup";
public const string MenuKeyBreadcrumb= "Breadcrumb";
public const string MenuKeyClassInput = "Class Input";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDisableContainer = "DisableContainer";
public const string MenuKeyDrawer = "Drawer";
public const string MenuKeyDualBadge = "DualBadge";
public const string MenuKeyEnumSelector = "EnumSelector";
public const string MenuKeyForm = "Form";
public const string MenuKeyImageViewer = "ImageViewer";
public const string MenuKeyIpBox = "IPv4Box";
public const string MenuKeyIconButton = "IconButton";
public const string MenuKeyKeyGestureInput = "KeyGestureInput";
public const string MenuKeyLoading = "Loading";
public const string MenuKeyMessageBox = "MessageBox";
public const string MenuKeyNavMenu = "NavMenu";
public const string MenuKeyNumberDisplayer = "NumberDisplayer";
public const string MenuKeyNumericUpDown = "NumericUpDown";
public const string MenuKeyPagination = "Pagination";
public const string MenuKeyRangeSlider = "RangeSlider";
public const string MenuKeyScrollToButton = "ScrollToButton";
public const string MenuKeySelectionList = "SelectionList";
public const string MenuKeyTagInput = "TagInput";
public const string MenuKeySkeleton = "Skeleton";
public const string MenuKeyTimeline = "Timeline";
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
public const string MenuKeyThemeToggler = "ThemeToggler";
public const string MenuKeyToolBar = "ToolBar";
}

View File

@@ -0,0 +1,15 @@
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ursa.Demo.ViewModels;
public class ScrollToButtonDemoViewModel: ObservableObject
{
public ObservableCollection<string> Items { get; set; }
public ScrollToButtonDemoViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(0, 1000).Select(a => "Item " + a));
}
}

View File

@@ -0,0 +1,92 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:ScrollToButton}" TargetType="u:ScrollToButton">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="Cursor" Value="Hand"></Setter>
<Setter Property="Margin" Value="0, 0, 16, 16" />
<Setter Property="Template">
<ControlTemplate TargetType="u:ScrollToButton">
<Border
Name="PART_Background"
Background="{DynamicResource ButtonDefaultBackground}"
CornerRadius="{DynamicResource ButtonCornerRadius}">
<PathIcon
Name="PART_Icon"
Margin="8"
Width="12"
Height="12"
Data="{DynamicResource ScrollToButtonIconGlyph}"
Foreground="{DynamicResource ButtonDefaultPrimaryForeground}" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pressed">
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="^:pointerover /template/ Border#PART_Background">
<Setter Property="BorderBrush" Value="{DynamicResource ButtonDefaultPointeroverBorderBrush}" />
<Setter Property="Background" Value="{DynamicResource ButtonDefaultPointeroverBackground}" />
</Style>
<Style Selector="^:pressed /template/ Border#PART_Background">
<Setter Property="BorderBrush" Value="{DynamicResource ButtonDefaultPressedBorderBrush}" />
<Setter Property="Background" Value="{DynamicResource ButtonDefaultPressedBackground}" />
</Style>
<Style Selector="^[Direction=Right] /template/ PathIcon#PART_Icon">
<Setter Property="RenderTransform" Value="rotate(90deg)"></Setter>
</Style>
<Style Selector="^[Direction=Bottom] /template/ PathIcon#PART_Icon">
<Setter Property="RenderTransform" Value="rotate(180deg)"></Setter>
</Style>
<Style Selector="^[Direction=Left] /template/ PathIcon#PART_Icon">
<Setter Property="RenderTransform" Value="rotate(270deg)"></Setter>
</Style>
</ControlTheme>
<ControlTheme x:Key="PrimaryScrollToButton" TargetType="u:ScrollToButton">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="Cursor" Value="Hand"></Setter>
<Setter Property="Margin" Value="0, 0, 16, 16" />
<Setter Property="Template">
<ControlTemplate TargetType="u:ScrollToButton">
<Border
Name="PART_Background"
Background="{DynamicResource ButtonSolidPrimaryBackground}"
CornerRadius="{DynamicResource ButtonCornerRadius}">
<PathIcon
Name="PART_Icon"
Margin="8"
Width="12"
Height="12"
Data="{DynamicResource ScrollToButtonIconGlyph}"
Foreground="{DynamicResource ButtonSolidForeground}" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pressed">
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="^:pointerover /template/ Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource ButtonSolidPrimaryPointeroverBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonSolidPrimaryPointeroverBorderBrush}" />
</Style>
<Style Selector="^:pressed /template/ Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource ButtonSolidPrimaryPressedBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonSolidPrimaryPressedBorderBrush}" />
</Style>
<Style Selector="^[Direction=Right] /template/ PathIcon#PART_Icon">
<Setter Property="RenderTransform" Value="rotate(90deg)"></Setter>
</Style>
<Style Selector="^[Direction=Bottom] /template/ PathIcon#PART_Icon">
<Setter Property="RenderTransform" Value="rotate(180deg)"></Setter>
</Style>
<Style Selector="^[Direction=Left] /template/ PathIcon#PART_Icon">
<Setter Property="RenderTransform" Value="rotate(270deg)"></Setter>
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -25,6 +25,7 @@
<ResourceInclude Source="NumberDisplayer.axaml" />
<ResourceInclude Source="Pagination.axaml" />
<ResourceInclude Source="RangeSlider.axaml" />
<ResourceInclude Source="ScrollToButton.axaml" />
<ResourceInclude Source="SelectionList.axaml" />
<ResourceInclude Source="TagInput.axaml" />
<ResourceInclude Source="ThemeSelector.axaml" />

View File

@@ -0,0 +1,5 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<StreamGeometry x:Key="ScrollToButtonIconGlyph">M19.637 16.4369C19.0513 17.0227 18.1015 17.0227 17.5157 16.4369L11.8589 10.7801L6.20202 16.4369C5.61623 17.0227 4.66648 17.0227 4.0807 16.4369C3.49491 15.8511 3.49491 14.9014 4.0807 14.3156L10.7982 7.59809C11.384 7.01231 12.3337 7.01231 12.9195 7.59809L19.637 14.3156C20.2228 14.9014 20.2228 15.8511 19.637 16.4369Z</StreamGeometry>
</ResourceDictionary>

View File

@@ -13,6 +13,7 @@
<MergeResourceInclude Source="MessageBox.axaml" />
<MergeResourceInclude Source="NavigationMenu.axaml" />
<MergeResourceInclude Source="Pagination.axaml" />
<MergeResourceInclude Source="ScrollToButton.axaml" />
<MergeResourceInclude Source="TagInput.axaml" />
<MergeResourceInclude Source="Skeleton.axaml" />
<MergeResourceInclude Source="ThemeSelector.axaml" />

View File

@@ -1,15 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
namespace Ursa.Controls.BackTop;
public class BackTop: Control
{
public static readonly AttachedProperty<bool> AttachProperty =
AvaloniaProperty.RegisterAttached<BackTop, Control, bool>("Attach");
public static void SetAttach(Control obj, bool value) => obj.SetValue(AttachProperty, value);
public static bool GetAttach(Control obj) => obj.GetValue(AttachProperty);
}

View File

@@ -0,0 +1,60 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using Ursa.Common;
namespace Ursa.Controls;
public class ScrollTo
{
public static readonly AttachedProperty<Position?> DirectionProperty =
AvaloniaProperty.RegisterAttached<ScrollTo, Control, Position?>("Direction");
public static void SetDirection(Control obj, Position value) => obj.SetValue(DirectionProperty, value);
public static Position? GetDirection(Control obj) => obj.GetValue(DirectionProperty);
public static readonly AttachedProperty<ControlTheme?> ButtonThemeProperty =
AvaloniaProperty.RegisterAttached<ScrollTo, Control, ControlTheme?>("ButtonTheme");
public static void SetButtonTheme(Control obj, ControlTheme? value) => obj.SetValue(ButtonThemeProperty, value);
public static ControlTheme? GetButtonTheme(Control obj) => obj.GetValue(ButtonThemeProperty);
static ScrollTo()
{
DirectionProperty.Changed.AddClassHandler<Control, Position?>(OnDirectionChanged);
ButtonThemeProperty.Changed.AddClassHandler<Control, ControlTheme?>(OnButtonThemeChanged);
}
private static void OnButtonThemeChanged(Control arg1, AvaloniaPropertyChangedEventArgs<ControlTheme?> arg2)
{
var button = EnsureButtonInAdorner(arg1);
if (button is null) return;
button.SetCurrentValue(StyledElement.ThemeProperty, arg2.NewValue.Value);
}
private static void OnDirectionChanged(Control control, AvaloniaPropertyChangedEventArgs<Position?> args)
{
if (args.NewValue.Value is null) return;
var button = EnsureButtonInAdorner(control);
if (button is null) return;
button.SetCurrentValue(ScrollToButton.DirectionProperty, args.NewValue.Value);
}
private static ScrollToButton? EnsureButtonInAdorner(Control control)
{
var adorner = AdornerLayer.GetAdorner(control);
if (adorner is not ScrollToButton button)
{
button = new ScrollToButton();
AdornerLayer.SetAdorner(control, button);
}
button.SetCurrentValue(ScrollToButton.TargetProperty, control);
button.SetCurrentValue(ScrollToButton.DirectionProperty, GetDirection(control));
if ( GetButtonTheme(control) is { } theme)
{
button.SetCurrentValue(StyledElement.ThemeProperty, theme);
}
return button;
}
}

View File

@@ -0,0 +1,125 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.VisualTree;
using Irihi.Avalonia.Shared.Helpers;
using Ursa.Common;
namespace Ursa.Controls;
public class ScrollToButton: Button
{
private ScrollViewer? _scroll;
private IDisposable? _disposable;
public static readonly StyledProperty<Control> TargetProperty = AvaloniaProperty.Register<ScrollToButton, Control>(
nameof(Target));
public Control Target
{
get => GetValue(TargetProperty);
set => SetValue(TargetProperty, value);
}
public static readonly StyledProperty<Position> DirectionProperty = AvaloniaProperty.Register<ScrollToButton, Position>(
nameof(Direction));
public Position Direction
{
get => GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
static ScrollToButton()
{
TargetProperty.Changed.AddClassHandler<ScrollToButton, Control>((o,e)=>o.OnTargetChanged(e));
DirectionProperty.Changed.AddClassHandler<ScrollToButton, Position>((o,e)=>o.OnDirectionChanged(e));
}
private void OnDirectionChanged(AvaloniaPropertyChangedEventArgs<Position> avaloniaPropertyChangedEventArgs)
{
if (_scroll is null) return;
SetVisibility(avaloniaPropertyChangedEventArgs.NewValue.Value, _scroll.Offset);
}
private void OnTargetChanged(AvaloniaPropertyChangedEventArgs<Control> arg2)
{
_disposable?.Dispose();
if (arg2.NewValue.Value is { } newValue)
{
var scroll = newValue.GetSelfAndVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (_scroll is not null)
{
_disposable?.Dispose();
_scroll = null;
}
_scroll = scroll;
_disposable = ScrollViewer.OffsetProperty.Changed.AddClassHandler<ScrollViewer, Vector>(OnScrollChanged);
SetVisibility(Direction, _scroll?.Offset);
}
}
protected override async void OnClick()
{
if (_scroll is null) return;
var vector = Direction switch
{
Position.Top => new Vector(0, double.NegativeInfinity),
Position.Bottom => new Vector(0, double.PositiveInfinity),
Position.Left => new Vector(double.NegativeInfinity, 0),
Position.Right => new Vector(double.PositiveInfinity, 0),
_ => new Vector(0, 0)
};
_scroll.SetCurrentValue(ScrollViewer.OffsetProperty, vector);
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
var scroll = Target.GetSelfAndVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (_scroll is not null)
{
_disposable?.Dispose();
_scroll = null;
}
_scroll = scroll;
_disposable = ScrollViewer.OffsetProperty.Changed.AddClassHandler<ScrollViewer, Vector>(OnScrollChanged);
SetVisibility(Direction, _scroll?.Offset);
}
private void OnScrollChanged(ScrollViewer arg1, AvaloniaPropertyChangedEventArgs<Vector> arg2)
{
if (arg1 != _scroll) return;
SetVisibility(Direction, arg2.NewValue.Value);
}
private void SetVisibility(Position direction, Vector? vector)
{
if (vector is null || _scroll is null) return;
if (direction == Position.Bottom && vector.Value.Y < _scroll.Extent.Height - _scroll.Bounds.Height)
{
IsVisible = true;
}
else if (direction == Position.Top && vector.Value.Y > 0)
{
IsVisible = true;
}
else if (direction == Position.Left && vector.Value.X > 0)
{
IsVisible = true;
}
else if (direction == Position.Right && vector.Value.X < _scroll.Extent.Width - _scroll.Bounds.Width)
{
IsVisible = true;
}
else
{
IsVisible = false;
}
}
}