This commit is contained in:
TheXamlGuy
2024-10-10 23:03:07 +01:00
parent b1e239ab04
commit 4439e399fd
4 changed files with 227 additions and 237 deletions
@@ -0,0 +1,127 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Toolkit.UI.Controls.Avalonia">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="ContentCropperThumbBackground" ResourceKey="AccentFillColorDefaultBrush" />
<StaticResource x:Key="ContentCropperThumbBrush" ResourceKey="ControlElevationBorderBrush" />
<StaticResource x:Key="ContentCropperOuterThumbBackground" ResourceKey="ControlSolidFillColorDefaultBrush" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<StaticResource x:Key="ContentCropperThumbBackground" ResourceKey="AccentFillColorDefaultBrush" />
<StaticResource x:Key="ContentCropperThumbBrush" ResourceKey="ControlElevationBorderBrush" />
<StaticResource x:Key="ContentCropperOuterThumbBackground" ResourceKey="ControlSolidFillColorDefaultBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<CornerRadius x:Key="ContentCropperThumbCornerRadius">10</CornerRadius>
<x:Double x:Key="ContentCropperInnerThumbWidth">12</x:Double>
<x:Double x:Key="ContentCropperInnerThumbHeight">12</x:Double>
<x:Double x:Key="ContentCropperThumbWidth">18</x:Double>
<x:Double x:Key="ContentCropperThumbHeight">18</x:Double>
<ControlTheme x:Key="ContentCropperThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Background" Value="{DynamicResource ContentCropperThumbBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ContentCropperThumbBrush}" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border
Margin="-2"
Background="{DynamicResource ContentCropperOuterThumbBackground}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ContentCropperOuterThumbCornerRadius}">
<Ellipse
Name="InnerThumb"
Width="{DynamicResource ContentCropperInnerThumbWidth}"
Height="{DynamicResource ContentCropperInnerThumbHeight}"
Fill="{TemplateBinding Background}"
RenderTransform="scaleX(0.86) scaleY(0.86)">
<Ellipse.Transitions>
<Transitions>
<TransformOperationsTransition
Easing="0,0 0,1"
Property="RenderTransform"
Duration="00:00:00.167" />
</Transitions>
</Ellipse.Transitions>
</Ellipse>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style Selector="^:pointerover /template/ Ellipse#InnerThumb">
<Setter Property="RenderTransform" Value="scaleX(1.167) scaleY(1.167)" />
</Style>
<Style Selector="^:pressed /template/ Ellipse#InnerThumb">
<Setter Property="RenderTransform" Value="scaleX(0.71) scaleY(0.71)" />
</Style>
<Style Selector="^:disabled /template/ Ellipse#InnerThumb">
<Setter Property="RenderTransform" Value="scaleX(1.167) scaleY(1.167)" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type ContentCropper}" TargetType="ContentCropper">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<ZoomBorder
x:Name="ZoomBorder"
ClipToBounds="True"
PanButton="Left">
<ContentPresenter
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</ZoomBorder>
<Canvas x:Name="Canvas">
<Rectangle
x:Name="RectangleLeft"
Fill="{DynamicResource SmokeFillColorDefaultBrush}"
UseLayoutRounding="True" />
<Rectangle
x:Name="RectangleTop"
Fill="{DynamicResource SmokeFillColorDefaultBrush}"
UseLayoutRounding="True" />
<Rectangle
x:Name="RectangleRight"
Fill="{DynamicResource SmokeFillColorDefaultBrush}"
UseLayoutRounding="True" />
<Rectangle
x:Name="RectangleBottom"
Fill="{DynamicResource SmokeFillColorDefaultBrush}"
UseLayoutRounding="True" />
<Border
x:Name="Border"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="2"
Cursor="SizeAll" />
<Thumb
x:Name="TopLeftButton"
Width="{DynamicResource ContentCropperThumbWidth}"
Height="{DynamicResource ContentCropperThumbHeight}"
Theme="{StaticResource ContentCropperThumbStyle}" />
<Thumb
x:Name="TopRightButton"
Width="{DynamicResource ContentCropperThumbWidth}"
Height="{DynamicResource ContentCropperThumbHeight}"
Theme="{StaticResource ContentCropperThumbStyle}" />
<Thumb
x:Name="BottomLeftButton"
Width="{DynamicResource ContentCropperThumbWidth}"
Height="{DynamicResource ContentCropperThumbHeight}"
Theme="{StaticResource ContentCropperThumbStyle}" />
<Thumb
x:Name="BottomRightButton"
Width="{DynamicResource ContentCropperThumbWidth}"
Height="{DynamicResource ContentCropperThumbHeight}"
Theme="{StaticResource ContentCropperThumbStyle}" />
</Canvas>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>
@@ -0,0 +1,401 @@
using Avalonia.Controls.Primitives;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia;
using Avalonia.Controls.Shapes;
namespace Toolkit.UI.Controls.Avalonia;
public class ContentCropper : ContentControl
{
public static readonly StyledProperty<Rect> CropRectangleProperty =
AvaloniaProperty.Register<ContentCropper, Rect>(nameof(CropRectangle));
public static readonly StyledProperty<bool> IsRatioScaleProperty =
AvaloniaProperty.Register<ContentCropper, bool>(nameof(IsRatioScale));
public static readonly StyledProperty<double> RectScaleProperty =
AvaloniaProperty.Register<ContentCropper, double>(nameof(RectScale), 0.5);
public static readonly StyledProperty<Size> ScaleSizeProperty =
AvaloniaProperty.Register<ContentCropper, Size>(nameof(ScaleSize), new Size(2, 1));
private Border? border;
private Thumb? bottomLeftButton;
private Thumb? bottomRightButton;
private Canvas? canvas;
private double cropHeightRatio;
private double cropLeftRatio;
private double cropTopRatio;
private double cropWidthRatio;
private bool isDragging;
private double offsetX;
private double offsetY;
private Rectangle? rectangleBottom;
private Rectangle? rectangleLeft;
private Rectangle? rectangleRight;
private Rectangle? rectangleTop;
private Thumb? topLeftButton;
private Thumb? topRightButton;
static ContentCropper()
{
AffectsRender<ContentCropper>(RectScaleProperty, ContentProperty);
}
public Rect CropRectangle
{
get => GetValue(CropRectangleProperty);
private set => SetValue(CropRectangleProperty, value);
}
public bool IsRatioScale
{
get => GetValue(IsRatioScaleProperty);
set => SetValue(IsRatioScaleProperty, value);
}
public double RectScale
{
get => GetValue(RectScaleProperty);
set => SetValue(RectScaleProperty, value);
}
public Size ScaleSize
{
get => GetValue(ScaleSizeProperty);
set => SetValue(ScaleSizeProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs args)
{
base.OnApplyTemplate(args);
canvas = args.NameScope.Find<Canvas>("Canvas");
rectangleLeft = args.NameScope.Find<Rectangle>("RectangleLeft");
rectangleTop = args.NameScope.Find<Rectangle>("RectangleTop");
rectangleRight = args.NameScope.Find<Rectangle>("RectangleRight");
rectangleBottom = args.NameScope.Find<Rectangle>("RectangleBottom");
border = args.NameScope.Find<Border>("Border");
topLeftButton = args.NameScope.Find<Thumb>("TopLeftButton");
if (topLeftButton is not null)
{
topLeftButton.DragDelta += OnThumbDragDelta;
}
topRightButton = args.NameScope.Find<Thumb>("TopRightButton");
if (topRightButton is not null)
{
topRightButton.DragDelta += OnThumbDragDelta;
}
bottomLeftButton = args.NameScope.Find<Thumb>("BottomLeftButton");
if (bottomLeftButton is not null)
{
bottomLeftButton.DragDelta += OnThumbDragDelta;
}
bottomRightButton = args.NameScope.Find<Thumb>("BottomRightButton");
if (bottomRightButton is not null)
{
bottomRightButton.DragDelta += OnThumbDragDelta;
}
}
protected override void OnLoaded(RoutedEventArgs args)
{
base.OnLoaded(args);
InitializeCropRect();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsRatioScaleProperty ||
change.Property == RectScaleProperty ||
change.Property == ContentProperty)
{
InitializeCropRect();
}
}
protected override void OnSizeChanged(SizeChangedEventArgs args)
{
base.OnSizeChanged(args);
if (canvas is null || border is null)
{
return;
}
double newContentWidth = Bounds.Width;
double newContentHeight = Bounds.Height;
canvas.Width = newContentWidth;
canvas.Height = newContentHeight;
// Calculate new positions and sizes based on updated ratios
double newCropLeft = cropLeftRatio * newContentWidth;
double newCropTop = cropTopRatio * newContentHeight;
double newCropWidth = cropWidthRatio * newContentWidth;
double newCropHeight = cropHeightRatio * newContentHeight;
// Check if the crop rectangle was resized or moved, and update accordingly
if (border.Width != newCropWidth || border.Height != newCropHeight)
{
border.Width = newCropWidth;
border.Height = newCropHeight;
Canvas.SetLeft(border, newCropLeft);
Canvas.SetTop(border, newCropTop);
}
PositionThumbs();
RenderOverLays();
}
private void InitializeCropRect()
{
if (canvas is null || Content is not Control content)
{
return;
}
double maxWidth = Bounds.Width;
double maxHeight = Bounds.Height;
double contentWidth = content.Bounds.Width > 0 ? content.Bounds.Width : maxWidth * 0.5;
double contentHeight = content.Bounds.Height > 0 ? content.Bounds.Height : maxHeight * 0.5;
double scaleFactor = Math.Min(maxWidth / contentWidth, maxHeight / contentHeight);
double width = contentWidth * scaleFactor;
double height = contentHeight * scaleFactor;
canvas.Width = width;
canvas.Height = height;
UpdateCropArea(width, height);
RenderOverLays();
}
private void OnBorderPointerMoved(object? sender, PointerEventArgs args)
{
if (!isDragging || canvas is null || border is null)
{
return;
}
Point position = args.GetPosition(this);
double newX = Math.Clamp(position.X - offsetX, 0, canvas.Bounds.Width - border.Bounds.Width);
double newY = Math.Clamp(position.Y - offsetY, 0, canvas.Bounds.Height - border.Bounds.Height);
Canvas.SetLeft(border, newX);
Canvas.SetTop(border, newY);
PositionThumbs();
RenderOverLays();
}
private void OnBorderPointerPressed(object? sender, PointerPressedEventArgs args)
{
if (!isDragging && border is not null)
{
isDragging = true;
Point position = args.GetPosition(this);
offsetX = position.X - Canvas.GetLeft(border);
offsetY = position.Y - Canvas.GetTop(border);
}
}
private void OnBorderPointerReleased(object? sender, PointerReleasedEventArgs args)
{
isDragging = false;
UpdateCropRatios();
}
private void OnThumbDragDelta(object? sender, VectorEventArgs args)
{
if (canvas is null || border is null || sender is not Thumb thumb)
{
return;
}
double deltaX = args.Vector.X;
double deltaY = args.Vector.Y;
double leftPosition = Canvas.GetLeft(border);
double topPosition = Canvas.GetTop(border);
double newWidth = border.Width;
double newHeight = border.Height;
switch (thumb.Name)
{
case "TopLeftButton":
newWidth = Math.Max(0, border.Width - deltaX);
newHeight = Math.Max(0, border.Height - deltaY);
if (newWidth > 0) leftPosition += deltaX;
if (newHeight > 0) topPosition += deltaY;
break;
case "TopRightButton":
newWidth = Math.Max(0, border.Width + deltaX);
newHeight = Math.Max(0, border.Height - deltaY);
if (newHeight > 0) topPosition += deltaY;
break;
case "BottomLeftButton":
newWidth = Math.Max(0, border.Width - deltaX);
newHeight = Math.Max(0, border.Height + deltaY);
if (newWidth > 0) leftPosition += deltaX;
break;
case "BottomRightButton":
newWidth = Math.Max(0, border.Width + deltaX);
newHeight = Math.Max(0, border.Height + deltaY);
break;
}
border.Width = newWidth;
border.Height = newHeight;
Canvas.SetLeft(border, leftPosition);
Canvas.SetTop(border, topPosition);
UpdateCropRatios();
PositionThumbs();
RenderOverLays();
}
private void PositionThumbs()
{
if (border == null ||
canvas == null)
{
return;
}
double borderLeft = Canvas.GetLeft(border);
double borderTop = Canvas.GetTop(border);
double borderWidth = border.Width;
double borderHeight = border.Height;
if (topLeftButton is not null)
{
Canvas.SetLeft(topLeftButton, borderLeft - (topLeftButton.Width / 2));
Canvas.SetTop(topLeftButton, borderTop - (topLeftButton.Height / 2));
}
if (topRightButton is not null)
{
Canvas.SetLeft(topRightButton, borderLeft + borderWidth - (topRightButton.Width / 2));
Canvas.SetTop(topRightButton, borderTop - (topRightButton.Height / 2));
}
if (bottomLeftButton is not null)
{
Canvas.SetLeft(bottomLeftButton, borderLeft - (bottomLeftButton.Width / 2));
Canvas.SetTop(bottomLeftButton, borderTop + borderHeight - (bottomLeftButton.Height / 2));
}
if (bottomRightButton is not null)
{
Canvas.SetLeft(bottomRightButton, borderLeft + borderWidth - (bottomRightButton.Width / 2));
Canvas.SetTop(bottomRightButton, borderTop + borderHeight - (bottomRightButton.Height / 2));
}
}
private void RenderOverLays()
{
if (canvas == null ||
border == null ||
rectangleLeft == null ||
rectangleTop == null ||
rectangleRight == null ||
rectangleBottom == null)
{
return;
}
double borderTop = Canvas.GetTop(border);
double borderLeft = Canvas.GetLeft(border);
rectangleLeft.Width = Math.Max(0, borderLeft);
rectangleLeft.Height = Math.Max(0, border.Height);
Canvas.SetTop(rectangleLeft, borderTop);
rectangleTop.Width = Math.Max(0, canvas.Width);
rectangleTop.Height = Math.Max(0, borderTop - 0.5);
double rightX = borderLeft + border.Width;
rectangleRight.Width = Math.Max(0, canvas.Width - rightX);
rectangleRight.Height = Math.Max(0, border.Height);
Canvas.SetLeft(rectangleRight, rightX);
Canvas.SetTop(rectangleRight, borderTop);
double bottomY = borderTop + border.Height;
rectangleBottom.Width = Math.Max(0, canvas.Width);
rectangleBottom.Height = Math.Max(0, canvas.Height - bottomY);
Canvas.SetTop(rectangleBottom, bottomY);
}
private void UpdateCropRatios()
{
if (canvas == null || border == null)
{
return;
}
cropLeftRatio = Canvas.GetLeft(border) / canvas.Width;
cropTopRatio = Canvas.GetTop(border) / canvas.Height;
cropWidthRatio = border.Width / canvas.Width;
cropHeightRatio = border.Height / canvas.Height;
}
private void UpdateCropArea(double width, double height)
{
if (canvas == null || border == null)
{
return;
}
if (IsRatioScale && ScaleSize.Width > 0 && ScaleSize.Height > 0)
{
if (ScaleSize.Width > ScaleSize.Height)
{
border.Width = width * RectScale;
border.Height = border.Width / ScaleSize.Width;
}
else
{
border.Height = height * RectScale;
border.Width = border.Height * ScaleSize.Height;
}
}
else
{
border.Width = width * RectScale;
border.Height = height * RectScale;
}
double centreX = (canvas.Width - border.Width) / 2;
double centreY = (canvas.Height - border.Height) / 2;
Canvas.SetLeft(border, centreX);
Canvas.SetTop(border, centreY);
PositionThumbs();
border.PointerPressed -= OnBorderPointerPressed;
border.PointerPressed += OnBorderPointerPressed;
border.PointerMoved -= OnBorderPointerMoved;
border.PointerMoved += OnBorderPointerMoved;
border.PointerReleased -= OnBorderPointerReleased;
border.PointerReleased += OnBorderPointerReleased;
}
}