From a70f7205e9bd944875bdc819d216a82443038712 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 17 Sep 2024 21:10:54 +0800 Subject: [PATCH 01/11] feat: support dialog resize. --- src/Ursa.Themes.Semi/Controls/Dialog.axaml | 1 + src/Ursa.Themes.Semi/Controls/Resizer.axaml | 26 +++ .../OverlayShared/OverlayFeedbackElement.cs | 180 ++++++++++++++---- src/Ursa/Controls/Resizers/DialogResizer.cs | 19 ++ .../Controls/Resizers/DialogResizerThumb.cs | 48 +++++ src/Ursa/Controls/Resizers/ResizeDirection.cs | 1 + 6 files changed, 239 insertions(+), 36 deletions(-) create mode 100644 src/Ursa/Controls/Resizers/DialogResizer.cs create mode 100644 src/Ursa/Controls/Resizers/DialogResizerThumb.cs diff --git a/src/Ursa.Themes.Semi/Controls/Dialog.axaml b/src/Ursa.Themes.Semi/Controls/Dialog.axaml index d096126..cebe482 100644 --- a/src/Ursa.Themes.Semi/Controls/Dialog.axaml +++ b/src/Ursa.Themes.Semi/Controls/Dialog.axaml @@ -233,6 +233,7 @@ Content="{DynamicResource STRING_MENU_DIALOG_OK}" Theme="{DynamicResource SolidButton}" /> + diff --git a/src/Ursa.Themes.Semi/Controls/Resizer.axaml b/src/Ursa.Themes.Semi/Controls/Resizer.axaml index 59e5ced..e349066 100644 --- a/src/Ursa.Themes.Semi/Controls/Resizer.axaml +++ b/src/Ursa.Themes.Semi/Controls/Resizer.axaml @@ -12,6 +12,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs b/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs index 50cc65a..cb11324 100644 --- a/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs +++ b/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs @@ -1,90 +1,198 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; +using Avalonia.VisualTree; using Irihi.Avalonia.Shared.Contracts; +using Irihi.Avalonia.Shared.Helpers; using Ursa.EventArgs; namespace Ursa.Controls.OverlayShared; -public abstract class OverlayFeedbackElement: ContentControl +public abstract class OverlayFeedbackElement : ContentControl { public static readonly StyledProperty IsClosedProperty = - AvaloniaProperty.Register(nameof(IsClosed), defaultValue: true); + AvaloniaProperty.Register(nameof(IsClosed), true); + + public static readonly RoutedEvent ClosedEvent = + RoutedEvent.Register( + nameof(Closed), RoutingStrategies.Bubble); + + private bool _resizeDragging; + private bool _moveDragging; + + private Panel? _containerPanel; + private Rect _resizeDragStartBounds; + private Point _resizeDragStartPoint; + + private WindowEdge? _windowEdge; + + static OverlayFeedbackElement() + { + FocusableProperty.OverrideDefaultValue(false); + DataContextProperty.Changed.AddClassHandler((o, e) => + o.OnDataContextChange(e)); + ClosedEvent.AddClassHandler((o, e) => o.OnClosed(e)); + } public bool IsClosed { get => GetValue(IsClosedProperty); set => SetValue(IsClosedProperty, value); } - - static OverlayFeedbackElement() - { - FocusableProperty.OverrideDefaultValue(false); - DataContextProperty.Changed.AddClassHandler((o, e) => o.OnDataContextChange(e)); - ClosedEvent.AddClassHandler((o,e)=>o.OnClosed(e)); - } private void OnClosed(ResultEventArgs _) { - SetCurrentValue(IsClosedProperty,true); + SetCurrentValue(IsClosedProperty, true); } - public static readonly RoutedEvent ClosedEvent = RoutedEvent.Register( - nameof(Closed), RoutingStrategies.Bubble); - public event EventHandler Closed { add => AddHandler(ClosedEvent, value); remove => RemoveHandler(ClosedEvent, value); } - + private void OnDataContextChange(AvaloniaPropertyChangedEventArgs args) { - if (args.OldValue.Value is IDialogContext oldContext) - { - oldContext.RequestClose -= OnContextRequestClose; - } - if (args.NewValue.Value is IDialogContext newContext) - { - newContext.RequestClose += OnContextRequestClose; - } + if (args.OldValue.Value is IDialogContext oldContext) oldContext.RequestClose -= OnContextRequestClose; + if (args.NewValue.Value is IDialogContext newContext) newContext.RequestClose += OnContextRequestClose; } - + protected virtual void OnElementClosing(object? sender, object? args) { RaiseEvent(new ResultEventArgs(ClosedEvent, args)); } - + private void OnContextRequestClose(object? sender, object? args) { RaiseEvent(new ResultEventArgs(ClosedEvent, args)); } - + public Task ShowAsync(CancellationToken? token = default) - { + { var tcs = new TaskCompletionSource(); - token?.Register(() => - { - Dispatcher.UIThread.Invoke(Close); - }); - + token?.Register(() => { Dispatcher.UIThread.Invoke(Close); }); + void OnCloseHandler(object? sender, ResultEventArgs? args) { if (args?.Result is T result) - { tcs.SetResult(result); - } else - { tcs.SetResult(default); - } RemoveHandler(ClosedEvent, OnCloseHandler); } AddHandler(ClosedEvent, OnCloseHandler); return tcs.Task; } - + public abstract void Close(); + + internal void BeginResizeDrag(WindowEdge windowEdge, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; + _resizeDragging = true; + _resizeDragStartPoint = e.GetPosition(this); + _resizeDragStartBounds = Bounds; + _windowEdge = windowEdge; + } + + internal void BeginMoveDrag(PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; + _resizeDragging = true; + _resizeDragStartPoint = e.GetPosition(this); + _resizeDragStartBounds = Bounds; + _windowEdge = null; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _containerPanel = this.FindAncestorOfType(); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + _resizeDragging = false; + } + + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + base.OnPointerCaptureLost(e); + _resizeDragging = false; + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + if (!_resizeDragging || _windowEdge is null) return; + var point = e.GetPosition(this); + var diff = point - _resizeDragStartPoint; + var left = Canvas.GetLeft(this); + var top = Canvas.GetTop(this); + var width = _windowEdge is WindowEdge.West or WindowEdge.NorthWest or WindowEdge.SouthWest + ? Bounds.Width : _resizeDragStartBounds.Width; + var height = _windowEdge is WindowEdge.North or WindowEdge.NorthEast or WindowEdge.NorthWest + ? Bounds.Height : _resizeDragStartBounds.Height; + var newBounds = CalculateNewBounds(left, top, width, height, diff, _containerPanel?.Bounds, _windowEdge.Value); + Canvas.SetLeft(this, newBounds.Left); + Canvas.SetTop(this, newBounds.Top); + SetCurrentValue(WidthProperty, newBounds.Width); + SetCurrentValue(HeightProperty, newBounds.Height); + } + + private Rect CalculateNewBounds(double left, double top, double width, double height, Point diff, Rect? containerBounds, + WindowEdge windowEdge) + { + if (containerBounds is not null) + { + var minX = -left; + var minY = -top; + var maxX = containerBounds.Value.Width - left - width; + var maxY = containerBounds.Value.Height - top - height; + diff = new Point(MathHelpers.SafeClamp(diff.X, minX, maxX), MathHelpers.SafeClamp(diff.Y, minY, maxY)); + } + switch (windowEdge) + { + case WindowEdge.North: + top += diff.Y; + height -= diff.Y; + top = Math.Max(0, top); + break; + case WindowEdge.NorthEast: + top += diff.Y; + width += diff.X; + height -= diff.Y; + break; + case WindowEdge.East: + width += diff.X; + break; + case WindowEdge.SouthEast: + width += diff.X; + height += diff.Y; + break; + case WindowEdge.South: + height += diff.Y; + break; + case WindowEdge.SouthWest: + left += diff.X; + width -= diff.X; + height += diff.Y; + break; + case WindowEdge.West: + left += diff.X; + width -= diff.X; + break; + case WindowEdge.NorthWest: + left += diff.X; + top += diff.Y; + width -= diff.X; + height -= diff.Y; + break; + } + return new Rect(left, top, width, height); + } } \ No newline at end of file diff --git a/src/Ursa/Controls/Resizers/DialogResizer.cs b/src/Ursa/Controls/Resizers/DialogResizer.cs new file mode 100644 index 0000000..d6e136c --- /dev/null +++ b/src/Ursa/Controls/Resizers/DialogResizer.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls.Primitives; + +namespace Ursa.Controls; + +public class DialogResizer: TemplatedControl +{ + public static readonly StyledProperty ResizeDirectionProperty = AvaloniaProperty.Register( + nameof(ResizeDirection)); + + /// + /// Defines what direction the dialog is allowed to be resized. + /// + public ResizeDirection ResizeDirection + { + get => GetValue(ResizeDirectionProperty); + set => SetValue(ResizeDirectionProperty, value); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Resizers/DialogResizerThumb.cs b/src/Ursa/Controls/Resizers/DialogResizerThumb.cs new file mode 100644 index 0000000..6face14 --- /dev/null +++ b/src/Ursa/Controls/Resizers/DialogResizerThumb.cs @@ -0,0 +1,48 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Ursa.Controls.OverlayShared; + +namespace Ursa.Controls; + +public class DialogResizerThumb: Thumb +{ + private OverlayFeedbackElement? _dialog; + + public static readonly StyledProperty ResizeDirectionProperty = AvaloniaProperty.Register( + nameof(ResizeDirection)); + + public ResizeDirection ResizeDirection + { + get => GetValue(ResizeDirectionProperty); + set => SetValue(ResizeDirectionProperty, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _dialog = this.FindLogicalAncestorOfType(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (_dialog is null) return; + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; + var windowEdge = ResizeDirection switch + { + ResizeDirection.Top => WindowEdge.North, + ResizeDirection.TopRight => WindowEdge.NorthEast, + ResizeDirection.Right => WindowEdge.East, + ResizeDirection.BottomRight => WindowEdge.SouthEast, + ResizeDirection.Bottom => WindowEdge.South, + ResizeDirection.BottomLeft => WindowEdge.SouthWest, + ResizeDirection.Left => WindowEdge.West, + ResizeDirection.TopLeft => WindowEdge.NorthWest, + _ => throw new ArgumentOutOfRangeException() + }; + _dialog.BeginResizeDrag(windowEdge, e); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Resizers/ResizeDirection.cs b/src/Ursa/Controls/Resizers/ResizeDirection.cs index f6b5899..2b7f422 100644 --- a/src/Ursa/Controls/Resizers/ResizeDirection.cs +++ b/src/Ursa/Controls/Resizers/ResizeDirection.cs @@ -1,5 +1,6 @@ namespace Ursa.Controls; +[Flags] public enum ResizeDirection { Top, From c512cb6e13fc78149530cb1008ee93b43af8ef3e Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 17 Sep 2024 22:44:35 +0800 Subject: [PATCH 02/11] Add resize functionality and improve dialog controls This commit introduces the ability to resize dialogs by adding `CanResize` properties to dialog options and control classes. It also refines dialog controls' behavior and layout, ensuring consistent resizing capabilities across different dialog types. Additionally, it enhances the overlay feedback element's positioning logic and updates the resizer's appearance and visibility handling. --- src/Ursa.Themes.Semi/Controls/Dialog.axaml | 319 +++++++++--------- src/Ursa.Themes.Semi/Controls/Resizer.axaml | 6 +- src/Ursa/Controls/Dialog/Dialog.cs | 4 + src/Ursa/Controls/Dialog/DialogControlBase.cs | 9 + src/Ursa/Controls/Dialog/DialogWindow.cs | 15 +- .../Controls/Dialog/Options/DialogOptions.cs | 2 + .../Dialog/Options/OverlayDialogOptions.cs | 2 + src/Ursa/Controls/Dialog/OverlayDialog.cs | 2 + .../OverlayShared/OverlayFeedbackElement.cs | 12 +- src/Ursa/Controls/Resizers/DialogResizer.cs | 55 +++ src/Ursa/Controls/Resizers/ResizeDirection.cs | 20 +- 11 files changed, 278 insertions(+), 168 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/Dialog.axaml b/src/Ursa.Themes.Semi/Controls/Dialog.axaml index cebe482..8d35c05 100644 --- a/src/Ursa.Themes.Semi/Controls/Dialog.axaml +++ b/src/Ursa.Themes.Semi/Controls/Dialog.axaml @@ -7,68 +7,72 @@ + + + - + - + - - - - - - -