Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions samples/BehaviorsTestApplication/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@
<TabItem Header="Draggable">
<pages:DraggableView />
</TabItem>
<TabItem Header="Mouse Drag Behaviors">
<pages:MouseDragBehaviorView />
</TabItem>
<TabItem Header="IfElseTrigger">
<pages:IfElseTriggerView />
</TabItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<UserControl 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"
x:Class="BehaviorsTestApplication.Views.Pages.MouseDragBehaviorView"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450">
<StackPanel Margin="5" Spacing="12">
<TextBlock Text="MouseDragElementBehavior" HorizontalAlignment="Center"/>
<Canvas Width="300" Height="120" Background="LightGray">
<Rectangle Fill="Red" Width="40" Height="40" Canvas.Left="20" Canvas.Top="20">
<Interaction.Behaviors>
<MouseDragElementBehavior ConstrainToParentBounds="True" />
</Interaction.Behaviors>
</Rectangle>
<Rectangle Fill="Green" Width="40" Height="40" Canvas.Left="120" Canvas.Top="20">
<Interaction.Behaviors>
<MouseDragElementBehavior ConstrainToParentBounds="True" />
</Interaction.Behaviors>
</Rectangle>
</Canvas>

<TextBlock Text="MultiMouseDragElementBehavior" HorizontalAlignment="Center"/>
<Canvas Width="300" Height="120" Background="LightGray">
<Rectangle x:Name="MultiRect1" Fill="Blue" Width="40" Height="40" Canvas.Left="30" Canvas.Top="30">
<Interaction.Behaviors>
<MultiMouseDragElementBehavior x:Name="MultiBehavior" ConstrainToParentBounds="True" />
</Interaction.Behaviors>
</Rectangle>
<Rectangle x:Name="MultiRect2" Fill="Orange" Width="40" Height="40" Canvas.Left="90" Canvas.Top="40"/>
<Rectangle x:Name="MultiRect3" Fill="Purple" Width="40" Height="40" Canvas.Left="150" Canvas.Top="50"/>
</Canvas>
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Xaml.Interactivity;
using Avalonia.Xaml.Interactions.Draggable;

namespace BehaviorsTestApplication.Views.Pages;

public partial class MouseDragBehaviorView : UserControl
{
public MouseDragBehaviorView()
{
InitializeComponent();

var rect1 = this.FindControl<Control>("MultiRect1");
var rect2 = this.FindControl<Control>("MultiRect2");
var rect3 = this.FindControl<Control>("MultiRect3");

if (rect1 is not null && rect2 is not null && rect3 is not null)
{
var behavior = Interaction.GetBehaviors(rect1)
.OfType<MultiMouseDragElementBehavior>()
.FirstOrDefault();
if (behavior is not null)
{
behavior.TargetControls.Add(rect2);
behavior.TargetControls.Add(rect3);
}
}
}

private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
137 changes: 137 additions & 0 deletions src/Avalonia.Xaml.Interactions.Draggable/MouseDragElementBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Xaml.Interactivity;

namespace Avalonia.Xaml.Interactions.Draggable;

/// <summary>
/// Enables dragging of a control with the mouse using a <see cref="TranslateTransform"/>.
/// </summary>
public class MouseDragElementBehavior : StyledElementBehavior<Control>
{
/// <summary>
/// Identifies the <see cref="ConstrainToParentBounds"/> avalonia property.
/// </summary>
public static readonly StyledProperty<bool> ConstrainToParentBoundsProperty =
AvaloniaProperty.Register<MouseDragElementBehavior, bool>(nameof(ConstrainToParentBounds));

private bool _captured;
private Point _start;
private Control? _parent;
private TranslateTransform? _transform;

/// <summary>
/// Gets or sets whether dragging should be constrained to the bounds of the parent control.
/// </summary>
public bool ConstrainToParentBounds
{
get => GetValue(ConstrainToParentBoundsProperty);
set => SetValue(ConstrainToParentBoundsProperty, value);
}

/// <inheritdoc />
protected override void OnAttachedToVisualTree()
{
if (AssociatedObject is not null)
{
AssociatedObject.AddHandler(InputElement.PointerPressedEvent, Pressed, RoutingStrategies.Tunnel);
AssociatedObject.AddHandler(InputElement.PointerReleasedEvent, Released, RoutingStrategies.Tunnel);
AssociatedObject.AddHandler(InputElement.PointerMovedEvent, Moved, RoutingStrategies.Tunnel);
AssociatedObject.AddHandler(InputElement.PointerCaptureLostEvent, CaptureLost, RoutingStrategies.Tunnel);
}
}

/// <inheritdoc />
protected override void OnDetachedFromVisualTree()
{
if (AssociatedObject is not null)
{
AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, Pressed);
AssociatedObject.RemoveHandler(InputElement.PointerReleasedEvent, Released);
AssociatedObject.RemoveHandler(InputElement.PointerMovedEvent, Moved);
AssociatedObject.RemoveHandler(InputElement.PointerCaptureLostEvent, CaptureLost);
}
}

private void Pressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (properties.IsLeftButtonPressed && AssociatedObject?.Parent is Control parent)
{
_parent = parent;
_start = e.GetPosition(_parent);

if (AssociatedObject.RenderTransform is TranslateTransform tr)
{
_transform = tr;
}
else
{
_transform = new TranslateTransform();
AssociatedObject.RenderTransform = _transform;
}

_captured = true;
}
}

private void Released(object? sender, PointerReleasedEventArgs e)
{
if (_captured && e.InitialPressMouseButton == MouseButton.Left)
{
EndDrag();
}
}

private void CaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
if (_captured)
{
EndDrag();
}
}

private void Moved(object? sender, PointerEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (!_captured || !properties.IsLeftButtonPressed || _parent is null || _transform is null)
{
return;
}

var position = e.GetPosition(_parent);
var deltaX = position.X - _start.X;
var deltaY = position.Y - _start.Y;
_start = position;

var newX = _transform.X + deltaX;
var newY = _transform.Y + deltaY;

if (ConstrainToParentBounds && AssociatedObject is Control element)
{
var parentBounds = _parent.Bounds;
var elementBounds = element.Bounds;

var minX = -elementBounds.X;
var minY = -elementBounds.Y;
var maxX = parentBounds.Width - elementBounds.Width - elementBounds.X;
var maxY = parentBounds.Height - elementBounds.Height - elementBounds.Y;

newX = Math.Min(Math.Max(newX, minX), maxX);
newY = Math.Min(Math.Max(newY, minY), maxY);
}

_transform.X = newX;
_transform.Y = newY;
}

private void EndDrag()
{
_captured = false;
_parent = null;
_transform = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Xaml.Interactivity;

namespace Avalonia.Xaml.Interactions.Draggable;

/// <summary>
/// Enables dragging of multiple controls with the mouse using <see cref="TranslateTransform"/>.
/// </summary>
public class MultiMouseDragElementBehavior : StyledElementBehavior<Control>
{
private AvaloniaList<Control>? _targetControls;
private bool _captured;
private Point _start;
private readonly Dictionary<Control, TranslateTransform> _transforms = new();
private Control? _parent;

/// <summary>
/// Identifies the <see cref="TargetControls"/> avalonia property.
/// </summary>
public static readonly DirectProperty<MultiMouseDragElementBehavior, AvaloniaList<Control>> TargetControlsProperty =
AvaloniaProperty.RegisterDirect<MultiMouseDragElementBehavior, AvaloniaList<Control>>(nameof(TargetControls), b => b.TargetControls);

/// <summary>
/// Identifies the <see cref="ConstrainToParentBounds"/> avalonia property.
/// </summary>
public static readonly StyledProperty<bool> ConstrainToParentBoundsProperty =
AvaloniaProperty.Register<MultiMouseDragElementBehavior, bool>(nameof(ConstrainToParentBounds));

/// <summary>
/// Gets the collection of controls that should be dragged together. This is an avalonia property.
/// </summary>
[Content]
public AvaloniaList<Control> TargetControls => _targetControls ??= [];

/// <summary>
/// Gets or sets whether dragging should be constrained to the bounds of the parent control.
/// </summary>
public bool ConstrainToParentBounds
{
get => GetValue(ConstrainToParentBoundsProperty);
set => SetValue(ConstrainToParentBoundsProperty, value);
}

/// <inheritdoc />
protected override void OnAttachedToVisualTree()
{
if (AssociatedObject is not null)
{
AssociatedObject.AddHandler(InputElement.PointerPressedEvent, Pressed, RoutingStrategies.Tunnel);
AssociatedObject.AddHandler(InputElement.PointerReleasedEvent, Released, RoutingStrategies.Tunnel);
AssociatedObject.AddHandler(InputElement.PointerMovedEvent, Moved, RoutingStrategies.Tunnel);
AssociatedObject.AddHandler(InputElement.PointerCaptureLostEvent, CaptureLost, RoutingStrategies.Tunnel);
}
}

/// <inheritdoc />
protected override void OnDetachedFromVisualTree()
{
if (AssociatedObject is not null)
{
AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, Pressed);
AssociatedObject.RemoveHandler(InputElement.PointerReleasedEvent, Released);
AssociatedObject.RemoveHandler(InputElement.PointerMovedEvent, Moved);
AssociatedObject.RemoveHandler(InputElement.PointerCaptureLostEvent, CaptureLost);
}
}

private void Pressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (properties.IsLeftButtonPressed && AssociatedObject?.Parent is Control parent)
{
_parent = parent;
_start = e.GetPosition(_parent);
_transforms.Clear();

AddTransform(AssociatedObject);
foreach (var control in TargetControls)
{
AddTransform(control);
}

_captured = true;
}
}

private void Released(object? sender, PointerReleasedEventArgs e)
{
if (_captured && e.InitialPressMouseButton == MouseButton.Left)
{
EndDrag();
}
}

private void CaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
if (_captured)
{
EndDrag();
}
}

private void Moved(object? sender, PointerEventArgs e)
{
var properties = e.GetCurrentPoint(AssociatedObject).Properties;
if (!_captured || !properties.IsLeftButtonPressed || _parent is null)
{
return;
}

var position = e.GetPosition(_parent);
var dx = position.X - _start.X;
var dy = position.Y - _start.Y;
_start = position;

foreach (var kvp in _transforms)
{
var element = kvp.Key;
var transform = kvp.Value;

var newX = transform.X + dx;
var newY = transform.Y + dy;

if (ConstrainToParentBounds && element.Parent is Control p)
{
var parentBounds = p.Bounds;
var bounds = element.Bounds;

var minX = -bounds.X;
var minY = -bounds.Y;
var maxX = parentBounds.Width - bounds.Width - bounds.X;
var maxY = parentBounds.Height - bounds.Height - bounds.Y;

newX = Math.Min(Math.Max(newX, minX), maxX);
newY = Math.Min(Math.Max(newY, minY), maxY);
}

transform.X = newX;
transform.Y = newY;
}
}

private void AddTransform(Control? element)
{
if (element is null)
{
return;
}

if (element.RenderTransform is TranslateTransform tr)
{
_transforms[element] = tr;
}
else
{
var t = new TranslateTransform();
element.RenderTransform = t;
_transforms[element] = t;
}
}

private void EndDrag()
{
_captured = false;
_parent = null;
_transforms.Clear();
}
}
Loading