feat: basic position.

This commit is contained in:
rabbitism
2024-02-05 19:40:11 +08:00
parent 8e085bc264
commit fd85efd895
13 changed files with 222 additions and 46 deletions

View File

@@ -5,11 +5,13 @@
mc:Ignorable="d" d:DesignWidth="800" mc:Ignorable="d" d:DesignWidth="800"
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo" xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo"
xmlns:u="https://irihi.tech/ursa" xmlns:u="https://irihi.tech/ursa"
xmlns:common="clr-namespace:Ursa.Common;assembly=Ursa"
x:DataType="vm:DrawerDemoViewModel" x:DataType="vm:DrawerDemoViewModel"
x:CompileBindings="True" x:CompileBindings="True"
d:DesignHeight="450" d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.DrawerDemo"> x:Class="Ursa.Demo.Pages.DrawerDemo">
<Canvas HorizontalAlignment="Left" Width="500"> <StackPanel HorizontalAlignment="Left" >
<u:EnumSelector EnumType="{x:Type common:Position}" Value="{Binding SelectedPosition}"></u:EnumSelector>
<Button Content="Call Drawer" HorizontalAlignment="Stretch" Command="{Binding OpenDrawerCommand}"></Button> <Button Content="Call Drawer" HorizontalAlignment="Stretch" Command="{Binding OpenDrawerCommand}"></Button>
</Canvas> </StackPanel>
</UserControl> </UserControl>

View File

@@ -3,13 +3,18 @@ using System.Windows.Input;
using Avalonia.Controls; using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Ursa.Common;
using Ursa.Controls; using Ursa.Controls;
using Ursa.Controls.Options;
namespace Ursa.Demo.ViewModels; namespace Ursa.Demo.ViewModels;
public class DrawerDemoViewModel: ObservableObject public partial class DrawerDemoViewModel: ObservableObject
{ {
public ICommand OpenDrawerCommand { get; set; } public ICommand OpenDrawerCommand { get; set; }
[ObservableProperty] private Position _selectedPosition;
public DrawerDemoViewModel() public DrawerDemoViewModel()
{ {
@@ -18,6 +23,6 @@ public class DrawerDemoViewModel: ObservableObject
private async Task OpenDrawer() private async Task OpenDrawer()
{ {
await Drawer.Show<Calendar, string, bool>("Hello World"); await Drawer.ShowCustom<Calendar, string, bool>("Hello World", new CustomDrawerOptions() { Position = SelectedPosition, MinWidth = 400 });
} }
} }

View File

@@ -3,16 +3,17 @@
xmlns:u="https://irihi.tech/ursa"> xmlns:u="https://irihi.tech/ursa">
<ControlTheme TargetType="u:CustomDrawerControl" x:Key="{x:Type u:CustomDrawerControl}"> <ControlTheme TargetType="u:CustomDrawerControl" x:Key="{x:Type u:CustomDrawerControl}">
<Setter Property="VerticalAlignment" Value="Stretch"></Setter> <Setter Property="VerticalAlignment" Value="Stretch"></Setter>
<Setter Property="MinWidth" Value="400"></Setter> <Setter Property="HorizontalAlignment" Value="Stretch"></Setter>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="u:CustomDrawerControl"> <ControlTemplate TargetType="u:CustomDrawerControl">
<Border Margin="8 0 0 0" <Border Margin="8 -1 -1 -1"
Padding="0" Padding="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Classes="Shadow" Classes="Shadow"
ClipToBounds="False" ClipToBounds="False"
CornerRadius="12 0 0 12" CornerRadius="12 0 0 12"
BorderThickness="1 0 0 0"
IsHitTestVisible="True" IsHitTestVisible="True"
Theme="{DynamicResource CardBorder}"> Theme="{DynamicResource CardBorder}">
<Border ClipToBounds="True" CornerRadius="12 0 0 12"> <Border ClipToBounds="True" CornerRadius="12 0 0 12">

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@@ -21,6 +22,33 @@ public class DefaultDrawerControl: DrawerControlBase
private Button? _noButton; private Button? _noButton;
private Button? _okButton; private Button? _okButton;
private Button? _cancelButton; private Button? _cancelButton;
public static readonly StyledProperty<DialogButton> ButtonsProperty = AvaloniaProperty.Register<DefaultDrawerControl, DialogButton>(
nameof(Buttons), DialogButton.OKCancel);
public DialogButton Buttons
{
get => GetValue(ButtonsProperty);
set => SetValue(ButtonsProperty, value);
}
public static readonly StyledProperty<DialogMode> ModeProperty = AvaloniaProperty.Register<DefaultDrawerControl, DialogMode>(
nameof(Mode), DialogMode.None);
public DialogMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
public static readonly StyledProperty<string?> TitleProperty = AvaloniaProperty.Register<DefaultDrawerControl, string?>(
nameof(Title));
public string? Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {

View File

@@ -1,11 +1,27 @@
using Avalonia.Controls; using Avalonia.Controls;
using Ursa.Common; using Ursa.Common;
using Ursa.Controls.Options;
namespace Ursa.Controls; namespace Ursa.Controls;
public static class Drawer public static class Drawer
{ {
public static Task<TResult?> Show<TView, TViewModel, TResult>(TViewModel vm, Position position = Position.Right) public static Task<TResult?> Show<TView, TViewModel, TResult>(TViewModel vm, DefaultDrawerOptions? options = null)
where TView: Control, new()
{
var host = OverlayDialogManager.GetHost(null);
if (host is null) return Task.FromResult(default(TResult));
var drawer = new DefaultDrawerControl()
{
Content = new TView(),
DataContext = vm,
};
ConfigureDefaultDrawer(drawer, options);
host.AddDrawer(drawer);
return drawer.ShowAsync<TResult>();
}
public static Task<TResult?> ShowCustom<TView, TViewModel, TResult>(TViewModel vm, CustomDrawerOptions? options = null)
where TView: Control, new() where TView: Control, new()
{ {
var host = OverlayDialogManager.GetHost(null); var host = OverlayDialogManager.GetHost(null);
@@ -14,9 +30,51 @@ public static class Drawer
{ {
Content = new TView(), Content = new TView(),
DataContext = vm, DataContext = vm,
Position = position,
}; };
ConfigureCustomDrawer(dialog, options);
host.AddDrawer(dialog); host.AddDrawer(dialog);
return dialog.ShowAsync<TResult>(); return dialog.ShowAsync<TResult>();
} }
private static void ConfigureCustomDrawer(CustomDrawerControl drawer, CustomDrawerOptions? options)
{
options ??= CustomDrawerOptions.Default;
drawer.Position = options.Position;
drawer.CanClickOnMaskToClose = options.CanClickOnMaskToClose;
drawer.IsCloseButtonVisible = options.IsCloseButtonVisible;
drawer.ShowMask = options.ShowMask;
drawer.CanLightDismiss = options.CanLightDismiss;
if (options.Position == Position.Left || options.Position == Position.Right)
{
drawer.MinWidth = options.MinWidth ?? 0.0;
drawer.MaxWidth = options.MaxWidth ?? double.PositiveInfinity;
}
if (options.Position is Position.Top or Position.Bottom)
{
drawer.MinHeight = options.MinHeight ?? 0.0;
drawer.MaxHeight = options.MaxHeight ?? double.PositiveInfinity;
}
}
private static void ConfigureDefaultDrawer(DefaultDrawerControl drawer, DefaultDrawerOptions? options)
{
options ??= DefaultDrawerOptions.Default;
drawer.Position = options.Position;
drawer.CanClickOnMaskToClose = options.CanClickOnMaskToClose;
drawer.IsCloseButtonVisible = options.IsCloseButtonVisible;
drawer.Buttons = options.Buttons;
drawer.Title = options.Title;
drawer.ShowMask = options.ShowMask;
drawer.CanLightDismiss = options.CanLightDismiss;
if (options.Position == Position.Left || options.Position == Position.Right)
{
drawer.MinWidth = options.MinWidth ?? 0.0;
drawer.MaxWidth = options.MaxWidth ?? double.PositiveInfinity;
}
if (options.Position is Position.Top or Position.Bottom)
{
drawer.MinHeight = options.MinHeight ?? 0.0;
drawer.MaxHeight = options.MaxHeight ?? double.PositiveInfinity;
}
}
} }

View File

@@ -16,12 +16,12 @@ public abstract class DrawerControlBase: OverlayFeedbackElement
public const string PART_CloseButton = "PART_CloseButton"; public const string PART_CloseButton = "PART_CloseButton";
internal bool CanClickOnMaskToClose { get; set; } internal bool CanClickOnMaskToClose { get; set; }
internal bool ShowCloseButton { get; set; }
protected internal Button? _closeButton; protected internal Button? _closeButton;
public static readonly StyledProperty<Position> PositionProperty = AvaloniaProperty.Register<DrawerControlBase, Position>( public static readonly StyledProperty<Position> PositionProperty =
nameof(Position)); AvaloniaProperty.Register<DrawerControlBase, Position>(
nameof(Position), defaultValue: Position.Right);
public Position Position public Position Position
{ {
@@ -48,6 +48,9 @@ public const string PART_CloseButton = "PART_CloseButton";
set => SetValue(IsCloseButtonVisibleProperty, value); set => SetValue(IsCloseButtonVisibleProperty, value);
} }
protected internal bool ShowMask { get; set; }
protected internal bool CanLightDismiss { get; set; }
static DrawerControlBase() static DrawerControlBase()
{ {
DataContextProperty.Changed.AddClassHandler<DrawerControlBase, object?>((o, e) => o.OnDataContextChange(e)); DataContextProperty.Changed.AddClassHandler<DrawerControlBase, object?>((o, e) => o.OnDataContextChange(e));

View File

@@ -0,0 +1,17 @@
using Ursa.Common;
namespace Ursa.Controls.Options;
public class CustomDrawerOptions
{
internal static CustomDrawerOptions Default => new ();
public Position Position { get; set; } = Position.Right;
public bool CanClickOnMaskToClose { get; set; } = true;
public bool CanLightDismiss { get; set; } = false;
public bool ShowMask { get; set; } = true;
public bool IsCloseButtonVisible { get; set; } = true;
public double? MinWidth { get; set; } = null;
public double? MinHeight { get; set; } = null;
public double? MaxWidth { get; set; } = null;
public double? MaxHeight { get; set; } = null;
}

View File

@@ -0,0 +1,19 @@
using Ursa.Common;
namespace Ursa.Controls.Options;
public class DefaultDrawerOptions
{
internal static DefaultDrawerOptions Default => new ();
public Position Position { get; set; } = Position.Right;
public bool CanClickOnMaskToClose { get; set; } = true;
public bool CanLightDismiss { get; set; } = false;
public bool ShowMask { get; set; } = true;
public bool IsCloseButtonVisible { get; set; } = true;
public double? MinWidth { get; set; } = null;
public double? MinHeight { get; set; } = null;
public double? MaxWidth { get; set; } = null;
public double? MaxHeight { get; set; } = null;
public DialogButton Buttons { get; set; } = DialogButton.OKCancel;
public string? Title { get; set; }
}

View File

@@ -21,8 +21,7 @@ public partial class OverlayDialogHost
public Thickness SnapThickness { get; set; } = new Thickness(0); public Thickness SnapThickness { get; set; } = new Thickness(0);
private static void ResetDialogPosition(DialogControlBase control, Size newSize)
private void ResetDialogPosition(DialogControlBase control, Size newSize)
{ {
var width = newSize.Width - control.Bounds.Width; var width = newSize.Width - control.Bounds.Width;
var height = newSize.Height - control.Bounds.Height; var height = newSize.Height - control.Bounds.Height;
@@ -77,7 +76,7 @@ public partial class OverlayDialogHost
{ {
if (e.Source is DialogControlBase item) if (e.Source is DialogControlBase item)
{ {
AnchorDialog(item); AnchorAndUpdatePositionInfo(item);
} }
} }
@@ -183,10 +182,10 @@ public partial class OverlayDialogHost
double top = GetTopPosition(control); double top = GetTopPosition(control);
SetLeft(control, left); SetLeft(control, left);
SetTop(control, top); SetTop(control, top);
AnchorDialog(control); AnchorAndUpdatePositionInfo(control);
} }
private void AnchorDialog(DialogControlBase control) private void AnchorAndUpdatePositionInfo(DialogControlBase control)
{ {
control.ActualHorizontalAnchor = HorizontalPosition.Center; control.ActualHorizontalAnchor = HorizontalPosition.Center;
control.ActualVerticalAnchor = VerticalPosition.Center; control.ActualVerticalAnchor = VerticalPosition.Center;

View File

@@ -3,7 +3,9 @@ using Avalonia.Animation;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Styling; using Avalonia.Styling;
using Ursa.Common;
using Ursa.Controls.OverlayShared; using Ursa.Controls.OverlayShared;
using Ursa.Controls.Shapes;
using Ursa.EventArgs; using Ursa.EventArgs;
namespace Ursa.Controls; namespace Ursa.Controls;
@@ -12,64 +14,106 @@ public partial class OverlayDialogHost
{ {
internal async void AddDrawer(DrawerControlBase control) internal async void AddDrawer(DrawerControlBase control)
{ {
var mask = CreateOverlayMask(true, false); PureRectangle? mask = null;
mask.Opacity = 0; if (control.ShowMask == false && control.CanLightDismiss)
{
mask = CreateOverlayMask(false, true);
}
else if (control.ShowMask)
{
mask = CreateOverlayMask(control.ShowMask, control.CanClickOnMaskToClose);
}
_layers.Add(new DialogPair(mask, control)); _layers.Add(new DialogPair(mask, control));
ResetZIndices(); ResetZIndices();
this.Children.Add(mask); if(mask is not null)this.Children.Add(mask);
this.Children.Add(control); this.Children.Add(control);
control.Measure(this.Bounds.Size); control.Measure(this.Bounds.Size);
control.Arrange(new Rect(control.DesiredSize).WithHeight(this.Bounds.Height)); control.Arrange(new Rect(control.DesiredSize));
// control.Height = this.Bounds.Height; SetDrawerPosition(control);
control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDrawerControlClosing); control.AddHandler(OverlayFeedbackElement.ClosedEvent, OnDrawerControlClosing);
var animation = CreateAnimation(control.Bounds.Width); var animation = CreateAnimation(control.Bounds.Size, control.Position, true);
var animation2 = CreateOpacityAnimation(); await Task.WhenAll(animation.RunAsync(control), _maskAppearAnimation.RunAsync(mask));
await Task.WhenAll(animation.RunAsync(control), animation2.RunAsync(mask));
} }
private Animation CreateAnimation(double width) private void SetDrawerPosition(DrawerControlBase control)
{ {
if(control.Position is Position.Left or Position.Right)
{
control.Height = this.Bounds.Height;
}
if(control.Position is Position.Top or Position.Bottom)
{
control.Width = this.Bounds.Width;
}
}
private Animation CreateAnimation(Size elementBounds, Position position, bool appear = true)
{
// left or top.
double source = 0;
double target = 0;
if (position == Position.Left)
{
source = appear ? -elementBounds.Width : 0;
target = appear ? 0 : -elementBounds.Width;
}
if (position == Position.Right)
{
source = appear ? Bounds.Width : Bounds.Width - elementBounds.Width;
target = appear ? Bounds.Width - elementBounds.Width : Bounds.Width;
}
if (position == Position.Top)
{
source = appear ? -elementBounds.Height : 0;
target = appear ? 0 : -elementBounds.Height;
}
if (position == Position.Bottom)
{
source = appear ? Bounds.Height : Bounds.Height - elementBounds.Height;
target = appear ? Bounds.Height - elementBounds.Height : Bounds.Height;
}
var targetProperty = position==Position.Left || position==Position.Right ? Canvas.LeftProperty : Canvas.TopProperty;
var animation = new Animation(); var animation = new Animation();
animation.Easing = new CubicEaseOut(); animation.Easing = new CubicEaseOut();
animation.FillMode = FillMode.Forward; animation.FillMode = FillMode.Forward;
var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) }; var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) };
keyFrame1.Setters.Add(new Setter() { Property = Canvas.LeftProperty, Value = Bounds.Width }); keyFrame1.Setters.Add(new Setter()
{ Property = targetProperty, Value = source });
var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) }; var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) };
keyFrame2.Setters.Add(new Setter() { Property = Canvas.LeftProperty, Value = Bounds.Width - width }); keyFrame2.Setters.Add(new Setter()
{ Property = targetProperty, Value = target });
animation.Children.Add(keyFrame1); animation.Children.Add(keyFrame1);
animation.Children.Add(keyFrame2); animation.Children.Add(keyFrame2);
animation.Duration = TimeSpan.FromSeconds(0.3); animation.Duration = TimeSpan.FromSeconds(0.3);
return animation; return animation;
} }
private Animation CreateOpacityAnimation() private async void OnDrawerControlClosing(object sender, ResultEventArgs e)
{
var animation = new Animation();
animation.FillMode = FillMode.Forward;
var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) };
keyFrame1.Setters.Add(new Setter(){ Property = OpacityProperty, Value = 0.0});
var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) };
keyFrame2.Setters.Add(new Setter() { Property = OpacityProperty, Value = 1.0 });
animation.Children.Add(keyFrame1);
animation.Children.Add(keyFrame2);
animation.Duration = TimeSpan.FromSeconds(0.3);
return animation;
}
private void OnDrawerControlClosing(object sender, ResultEventArgs e)
{ {
if (sender is DrawerControlBase control) if (sender is DrawerControlBase control)
{ {
var layer = _layers.FirstOrDefault(a => a.Element == control); var layer = _layers.FirstOrDefault(a => a.Element == control);
if(layer is null) return; if(layer is null) return;
_layers.Remove(layer); _layers.Remove(layer);
control.RemoveHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
if (layer.Mask is not null) if (layer.Mask is not null)
{ {
layer.Mask.RemoveHandler(PointerPressedEvent, ClickMaskToCloseDialog); layer.Mask.RemoveHandler(PointerPressedEvent, ClickMaskToCloseDialog);
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
await Task.WhenAll(disappearAnimation.RunAsync(control), _maskDisappearAnimation.RunAsync(layer.Mask));
Children.Remove(layer.Mask);
}
else
{
var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false);
await disappearAnimation.RunAsync(control);
} }
Children.Remove(control); Children.Remove(control);
control.RemoveHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing);
control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged);
ResetZIndices(); ResetZIndices();
} }
} }