using Avalonia.Controls.Primitives; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia; using Avalonia.Media; using Path = Avalonia.Controls.Shapes.Path; using Avalonia.Media.Imaging; namespace Toolkit.UI.Controls.Avalonia; public class ContentCropper : ContentControl { public static readonly DirectProperty CroppedBitmapProperty = AvaloniaProperty.RegisterDirect(nameof(CroppedBitmap), o => o.CroppedBitmap); public static readonly StyledProperty CropRectangleProperty = AvaloniaProperty.Register(nameof(CropRectangle)); public static readonly StyledProperty IsRatioScaleProperty = AvaloniaProperty.Register(nameof(IsRatioScale)); public static readonly StyledProperty RectScaleProperty = AvaloniaProperty.Register(nameof(RectScale), 0.5); public static readonly StyledProperty ScaleSizeProperty = AvaloniaProperty.Register(nameof(ScaleSize), new Size(2, 1)); private Border? border; private Thumb? bottomButton; private Thumb? bottomLeftButton; private Thumb? bottomRightButton; private Canvas? canvas; private double cropHeightRatio; private double cropLeftRatio; private Bitmap? croppedBitmap; private double cropTopRatio; private double cropWidthRatio; private bool isDragging; private Thumb? leftButton; private double offsetX; private double offsetY; private Path? overlayPath; private Thumb? rightButton; private Thumb? topButton; private Thumb? topLeftButton; private Thumb? topRightButton; static ContentCropper() { AffectsRender(RectScaleProperty, ContentProperty); } public Bitmap? CroppedBitmap { get => croppedBitmap; private set => SetAndRaise(CroppedBitmapProperty, ref croppedBitmap, value); } 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"); overlayPath = args.NameScope.Find("OverlayPath"); border = args.NameScope.Find("Border"); topLeftButton = args.NameScope.Find("TopLeftButton"); if (topLeftButton is not null) { topLeftButton.DragDelta += OnThumbDragDelta; } topRightButton = args.NameScope.Find("TopRightButton"); if (topRightButton is not null) { topRightButton.DragDelta += OnThumbDragDelta; } bottomLeftButton = args.NameScope.Find("BottomLeftButton"); if (bottomLeftButton is not null) { bottomLeftButton.DragDelta += OnThumbDragDelta; } bottomRightButton = args.NameScope.Find("BottomRightButton"); if (bottomRightButton is not null) { bottomRightButton.DragDelta += OnThumbDragDelta; } leftButton = args.NameScope.Find("LeftButton"); if (leftButton is not null) { leftButton.DragDelta += OnThumbDragDelta; } rightButton = args.NameScope.Find("RightButton"); if (rightButton is not null) { rightButton.DragDelta += OnThumbDragDelta; } topButton = args.NameScope.Find("TopButton"); if (topButton is not null) { topButton.DragDelta += OnThumbDragDelta; } bottomButton = args.NameScope.Find("BottomButton"); if (bottomButton is not null) { bottomButton.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; if (border.Width > 0 && border.Height > 0) { double newCropLeft = cropLeftRatio * newContentWidth; double newCropTop = cropTopRatio * newContentHeight; double newCropWidth = cropWidthRatio * newContentWidth; double newCropHeight = cropHeightRatio * newContentHeight; border.Width = newCropWidth; border.Height = newCropHeight; Canvas.SetLeft(border, newCropLeft); Canvas.SetTop(border, newCropTop); } else { InitializeCropRect(); } UpdateCropRectangle(); 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); UpdateCropRatios(); UpdateCropRectangle(); PositionThumbs(); 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); UpdateCropRectangle(); 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 minimumWidth = 20; double minimumHeight = 20; 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; bool canResizeWidth = true; bool canResizeHeight = true; switch (thumb.Name) { case "TopLeftButton": if (border.Width - deltaX < minimumWidth) { canResizeWidth = false; } if (border.Height - deltaY < minimumHeight) { canResizeHeight = false; } if (canResizeWidth) { newWidth = border.Width - deltaX; leftPosition += deltaX; } if (canResizeHeight) { newHeight = border.Height - deltaY; topPosition += deltaY; } break; case "TopRightButton": if (border.Height - deltaY < minimumHeight) { canResizeHeight = false; } newWidth = border.Width + deltaX; if (canResizeHeight) { newHeight = border.Height - deltaY; topPosition += deltaY; } break; case "BottomLeftButton": if (border.Width - deltaX < minimumWidth) { canResizeWidth = false; } newWidth = border.Width - deltaX; if (canResizeWidth) { leftPosition += deltaX; } newHeight = border.Height + deltaY; break; case "BottomRightButton": newWidth = border.Width + deltaX; newHeight = border.Height + deltaY; break; case "TopButton": if (border.Height - deltaY < minimumHeight) { canResizeHeight = false; } if (canResizeHeight) { newHeight = border.Height - deltaY; topPosition += deltaY; } break; case "BottomButton": newHeight = border.Height + deltaY; break; case "LeftButton": if (border.Width - deltaX < minimumWidth) { canResizeWidth = false; } if (canResizeWidth) { newWidth = border.Width - deltaX; leftPosition += deltaX; } break; case "RightButton": newWidth = border.Width + deltaX; break; } if (leftPosition < 0 || leftPosition + newWidth > canvas.Width || topPosition < 0 || topPosition + newHeight > canvas.Height || newWidth < minimumWidth || newHeight < minimumHeight) { return; } border.Width = newWidth; border.Height = newHeight; Canvas.SetLeft(border, leftPosition); Canvas.SetTop(border, topPosition); UpdateCropRatios(); UpdateCropRectangle(); 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); Canvas.SetTop(topLeftButton, borderTop); } if (topRightButton is not null) { Canvas.SetLeft(topRightButton, borderLeft + borderWidth - topRightButton.Width); Canvas.SetTop(topRightButton, borderTop); } if (bottomLeftButton is not null) { Canvas.SetLeft(bottomLeftButton, borderLeft); Canvas.SetTop(bottomLeftButton, borderTop + borderHeight - bottomLeftButton.Height); } if (bottomRightButton is not null) { Canvas.SetLeft(bottomRightButton, borderLeft + borderWidth - bottomRightButton.Width); Canvas.SetTop(bottomRightButton, borderTop + borderHeight - bottomRightButton.Height); } if (leftButton is not null) { Canvas.SetLeft(leftButton, borderLeft); Canvas.SetTop(leftButton, borderTop + borderHeight / 2 - leftButton.Height / 2); } if (rightButton is not null) { Canvas.SetLeft(rightButton, borderLeft + borderWidth - rightButton.Width); Canvas.SetTop(rightButton, borderTop + borderHeight / 2 - rightButton.Height / 2); } if (topButton is not null) { Canvas.SetLeft(topButton, borderLeft + borderWidth / 2 - topButton.Width / 2); Canvas.SetTop(topButton, borderTop); } if (bottomButton is not null) { Canvas.SetLeft(bottomButton, borderLeft + borderWidth / 2 - bottomButton.Width / 2); Canvas.SetTop(bottomButton, borderTop + borderHeight - bottomButton.Height); } } private void RenderOverLays() { if (canvas == null || border == null || overlayPath == null) { return; } double borderTop = Canvas.GetTop(border); double borderLeft = Canvas.GetLeft(border); double borderWidth = border.Width; double borderHeight = border.Height; RectangleGeometry outerRect = new(new Rect(0, 0, canvas.Width, canvas.Height)); RectangleGeometry innerRect = new(new Rect(borderLeft, borderTop, borderWidth, borderHeight)); CombinedGeometry punchThroughGeometry = new(GeometryCombineMode.Exclude, outerRect, innerRect); overlayPath.Data = punchThroughGeometry; } 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; } private void UpdateCroppedBitmap(Visual visual, double centreX, double centreY) { int width = (int?)border?.Width ?? 0; int height = (int?)border?.Height ?? 0; double x = Math.Max(centreX - width / 2, 0); double y = Math.Max(centreY - height / 2, 0); x = Math.Min(x, visual.Bounds.Width - width); y = Math.Min(y, visual.Bounds.Height - height); PixelSize pixelSize = new(width, height); RenderTargetBitmap renderTarget = new(pixelSize); using (DrawingContext drawingContext = renderTarget.CreateDrawingContext()) { drawingContext.PushClip(new Rect(0, 0, width, height)); drawingContext.FillRectangle(new VisualBrush(visual), new Rect(-x, -y, visual.Bounds.Width, visual.Bounds.Height)); } CroppedBitmap = renderTarget; } 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 UpdateCropRectangle() { if (canvas is null || border is null) { return; } double left = Canvas.GetLeft(border); double top = Canvas.GetTop(border); CropRectangle = new Rect(left, top, border.Width, border.Height); UpdateCroppedBitmap(canvas, border.Width, border.Height); } }