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,