feat: support dialog resize.
This commit is contained in:
@@ -233,6 +233,7 @@
|
|||||||
Content="{DynamicResource STRING_MENU_DIALOG_OK}"
|
Content="{DynamicResource STRING_MENU_DIALOG_OK}"
|
||||||
Theme="{DynamicResource SolidButton}" />
|
Theme="{DynamicResource SolidButton}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<u:DialogResizer Grid.Row="0" Grid.RowSpan="3"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -12,6 +12,32 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</ControlTheme>
|
</ControlTheme>
|
||||||
|
|
||||||
|
<ControlTheme TargetType="u:DialogResizerThumb" x:Key="{x:Type u:DialogResizerThumb}">
|
||||||
|
<Setter Property="Background" Value="Red" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:DialogResizerThumb">
|
||||||
|
<iri:PureRectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{TemplateBinding Background}"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</ControlTheme>
|
||||||
|
|
||||||
|
<ControlTheme TargetType="u:DialogResizer" x:Key="{x:Type u:DialogResizer}">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:WindowResizer">
|
||||||
|
<Grid RowDefinitions="Auto, *, Auto" ColumnDefinitions="Auto, *, Auto" >
|
||||||
|
<u:DialogResizerThumb Grid.Row="0" Grid.Column="0" ResizeDirection="TopLeft" Cursor="TopLeftCorner" />
|
||||||
|
<u:DialogResizerThumb Grid.Row="0" Grid.Column="1" ResizeDirection="Top" Cursor="TopSide" Height="{DynamicResource ResizerThumbHeight}" />
|
||||||
|
<u:DialogResizerThumb Grid.Row="0" Grid.Column="2" ResizeDirection="TopRight" Cursor="TopRightCorner" />
|
||||||
|
<u:DialogResizerThumb Grid.Row="1" Grid.Column="0" ResizeDirection="Left" Cursor="LeftSide" Width="{DynamicResource ResizerThumbWidth}" />
|
||||||
|
<u:DialogResizerThumb Grid.Row="1" Grid.Column="2" ResizeDirection="Right" Cursor="RightSide" Width="{DynamicResource ResizerThumbWidth}" />
|
||||||
|
<u:DialogResizerThumb Grid.Row="2" Grid.Column="0" ResizeDirection="BottomLeft" Cursor="BottomLeftCorner" />
|
||||||
|
<u:DialogResizerThumb Grid.Row="2" Grid.Column="1" ResizeDirection="Bottom" Cursor="BottomSide" Height="{DynamicResource ResizerThumbHeight}" />
|
||||||
|
<u:DialogResizerThumb Grid.Row="2" Grid.Column="2" ResizeDirection="BottomRight" Cursor="BottomRightCorner" />
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</ControlTheme>
|
||||||
|
|
||||||
<ControlTheme TargetType="u:WindowResizer" x:Key="{x:Type u:WindowResizer}">
|
<ControlTheme TargetType="u:WindowResizer" x:Key="{x:Type u:WindowResizer}">
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<ControlTemplate TargetType="u:WindowResizer">
|
<ControlTemplate TargetType="u:WindowResizer">
|
||||||
|
|||||||
@@ -1,16 +1,40 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
using Irihi.Avalonia.Shared.Contracts;
|
using Irihi.Avalonia.Shared.Contracts;
|
||||||
|
using Irihi.Avalonia.Shared.Helpers;
|
||||||
using Ursa.EventArgs;
|
using Ursa.EventArgs;
|
||||||
|
|
||||||
namespace Ursa.Controls.OverlayShared;
|
namespace Ursa.Controls.OverlayShared;
|
||||||
|
|
||||||
public abstract class OverlayFeedbackElement: ContentControl
|
public abstract class OverlayFeedbackElement : ContentControl
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<bool> IsClosedProperty =
|
public static readonly StyledProperty<bool> IsClosedProperty =
|
||||||
AvaloniaProperty.Register<OverlayFeedbackElement, bool>(nameof(IsClosed), defaultValue: true);
|
AvaloniaProperty.Register<OverlayFeedbackElement, bool>(nameof(IsClosed), true);
|
||||||
|
|
||||||
|
public static readonly RoutedEvent<ResultEventArgs> ClosedEvent =
|
||||||
|
RoutedEvent.Register<DrawerControlBase, ResultEventArgs>(
|
||||||
|
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<OverlayFeedbackElement>(false);
|
||||||
|
DataContextProperty.Changed.AddClassHandler<OverlayFeedbackElement, object?>((o, e) =>
|
||||||
|
o.OnDataContextChange(e));
|
||||||
|
ClosedEvent.AddClassHandler<OverlayFeedbackElement>((o, e) => o.OnClosed(e));
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsClosed
|
public bool IsClosed
|
||||||
{
|
{
|
||||||
@@ -18,21 +42,11 @@ public abstract class OverlayFeedbackElement: ContentControl
|
|||||||
set => SetValue(IsClosedProperty, value);
|
set => SetValue(IsClosedProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static OverlayFeedbackElement()
|
|
||||||
{
|
|
||||||
FocusableProperty.OverrideDefaultValue<OverlayFeedbackElement>(false);
|
|
||||||
DataContextProperty.Changed.AddClassHandler<OverlayFeedbackElement, object?>((o, e) => o.OnDataContextChange(e));
|
|
||||||
ClosedEvent.AddClassHandler<OverlayFeedbackElement>((o,e)=>o.OnClosed(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClosed(ResultEventArgs _)
|
private void OnClosed(ResultEventArgs _)
|
||||||
{
|
{
|
||||||
SetCurrentValue(IsClosedProperty,true);
|
SetCurrentValue(IsClosedProperty, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly RoutedEvent<ResultEventArgs> ClosedEvent = RoutedEvent.Register<DrawerControlBase, ResultEventArgs>(
|
|
||||||
nameof(Closed), RoutingStrategies.Bubble);
|
|
||||||
|
|
||||||
public event EventHandler<ResultEventArgs> Closed
|
public event EventHandler<ResultEventArgs> Closed
|
||||||
{
|
{
|
||||||
add => AddHandler(ClosedEvent, value);
|
add => AddHandler(ClosedEvent, value);
|
||||||
@@ -41,14 +55,8 @@ public abstract class OverlayFeedbackElement: ContentControl
|
|||||||
|
|
||||||
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
|
private void OnDataContextChange(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||||
{
|
{
|
||||||
if (args.OldValue.Value is IDialogContext oldContext)
|
if (args.OldValue.Value is IDialogContext oldContext) oldContext.RequestClose -= OnContextRequestClose;
|
||||||
{
|
if (args.NewValue.Value is IDialogContext newContext) newContext.RequestClose += OnContextRequestClose;
|
||||||
oldContext.RequestClose -= OnContextRequestClose;
|
|
||||||
}
|
|
||||||
if (args.NewValue.Value is IDialogContext newContext)
|
|
||||||
{
|
|
||||||
newContext.RequestClose += OnContextRequestClose;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnElementClosing(object? sender, object? args)
|
protected virtual void OnElementClosing(object? sender, object? args)
|
||||||
@@ -64,21 +72,14 @@ public abstract class OverlayFeedbackElement: ContentControl
|
|||||||
public Task<T?> ShowAsync<T>(CancellationToken? token = default)
|
public Task<T?> ShowAsync<T>(CancellationToken? token = default)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<T?>();
|
var tcs = new TaskCompletionSource<T?>();
|
||||||
token?.Register(() =>
|
token?.Register(() => { Dispatcher.UIThread.Invoke(Close); });
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Invoke(Close);
|
|
||||||
});
|
|
||||||
|
|
||||||
void OnCloseHandler(object? sender, ResultEventArgs? args)
|
void OnCloseHandler(object? sender, ResultEventArgs? args)
|
||||||
{
|
{
|
||||||
if (args?.Result is T result)
|
if (args?.Result is T result)
|
||||||
{
|
|
||||||
tcs.SetResult(result);
|
tcs.SetResult(result);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
tcs.SetResult(default);
|
tcs.SetResult(default);
|
||||||
}
|
|
||||||
RemoveHandler(ClosedEvent, OnCloseHandler);
|
RemoveHandler(ClosedEvent, OnCloseHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,4 +88,111 @@ public abstract class OverlayFeedbackElement: ContentControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract void Close();
|
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<Panel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
19
src/Ursa/Controls/Resizers/DialogResizer.cs
Normal file
19
src/Ursa/Controls/Resizers/DialogResizer.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
public class DialogResizer: TemplatedControl
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<ResizeDirection> ResizeDirectionProperty = AvaloniaProperty.Register<DialogResizer, ResizeDirection>(
|
||||||
|
nameof(ResizeDirection));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines what direction the dialog is allowed to be resized.
|
||||||
|
/// </summary>
|
||||||
|
public ResizeDirection ResizeDirection
|
||||||
|
{
|
||||||
|
get => GetValue(ResizeDirectionProperty);
|
||||||
|
set => SetValue(ResizeDirectionProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Ursa/Controls/Resizers/DialogResizerThumb.cs
Normal file
48
src/Ursa/Controls/Resizers/DialogResizerThumb.cs
Normal file
@@ -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<ResizeDirection> ResizeDirectionProperty = AvaloniaProperty.Register<DialogResizerThumb, ResizeDirection>(
|
||||||
|
nameof(ResizeDirection));
|
||||||
|
|
||||||
|
public ResizeDirection ResizeDirection
|
||||||
|
{
|
||||||
|
get => GetValue(ResizeDirectionProperty);
|
||||||
|
set => SetValue(ResizeDirectionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate(e);
|
||||||
|
_dialog = this.FindLogicalAncestorOfType<OverlayFeedbackElement>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
namespace Ursa.Controls;
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
public enum ResizeDirection
|
public enum ResizeDirection
|
||||||
{
|
{
|
||||||
Top,
|
Top,
|
||||||
|
|||||||
Reference in New Issue
Block a user